November 5, 2026
Remote Code Execution via Polyglot Web Shell Upload
Not disguising a PHP file as an image, but building a file that is honestly both at once

By Kaustubh Asthana
5 min read
Every upload filter in this category so far has been reading the filename and nothing else. Rename the extension, slip in a null byte, and the server never actually looked hard enough to notice. This lab breaks that pattern. It doesn't care what the filename claims. It opens the file and checks whether the bytes inside are actually a real image, which means every trick that worked before gets rejected here on the first attempt.
Establishing the Baseline
Same starting point as always. I logged in as wiener and uploaded a plain PNG to the avatar field, to confirm uploads still land at /files/avatars/ before trying anything adversarial.
Hitting a Filter That Actually Checks
I uploaded exploit.php directly, the same single-line payload used throughout this category.
That error message is the whole story. It isn't complaining about the extension, it's telling me the content itself failed an image check. The earlier labs in this category only ever inspected the filename, so I went back to the two tricks that worked before to see if either still applied here.
Neither one even got partway there. The null byte and the double extension both change how the filename reads, but they do nothing to the actual bytes sitting inside the file, and this filter never looks at the filename at all. What I needed wasn't a cleverer name. It was a file that genuinely passes as an image when something actually opens it and checks.
Building a File That's Both at Once
A polyglot file is one that's simultaneously valid in two different formats, not because one is disguised as the other, but because both formats can coexist in the same bytes without conflicting. A PNG has room for metadata that image viewers don't treat as pixel data, and PHP only cares about what's sitting between its own opening and closing tags, ignoring everything else in a file as plain output. Those two facts leave just enough space to build a file that's a real image and a real PHP script at the same time.
I used exiftool, a command-line tool built for reading and writing the metadata fields inside media files, things like a PNG's Comment field, which normally just holds a caption or copyright line. exiftool doesn't validate what you put in a text field, it writes whatever string you give it.
exiftool -Comment="" medium-cover-obfuscated-file-extension.png -o polyglot.php
Breaking that down: the -Comment flag sets the image's Comment metadata to whatever string follows it, and here that string is a complete, real PHP statement rather than ordinary caption text. The PHP itself reads carlos's secret file and echoes it back wrapped between the literal words START and END, which matters later for picking the actual value out of a response that's otherwise full of unreadable binary noise. The input file, medium-cover-obfuscated-file-extension.png, is an existing valid PNG, the same cover image from the previous lab's article, reused here purely as a convenient real image to build from. The -o polyglot.php flag tells exiftool to write its output to a new file under that name instead of overwriting the original. exiftool doesn't care that the output filename ends in .php, it just writes out the same PNG structure, with the same header and pixel data, under whatever name it's told to use. The result is a file that is, byte for byte, a completely valid PNG, with one small corner of its metadata holding real PHP source instead of a caption.
Uploading It
I uploaded polyglot.php the same way as every other avatar.
It went straight through, no rejection this time. The content check that blocked exploit.php, exploit.php%00.jpg, and exploit.php.jpg has nothing to object to here, because this file really is a valid image all the way through. The filter was never fooled. It was checking something true.
Reading the Output
I fetched the file directly to see what came back.
The response is mostly unreadable binary, exactly what you'd expect from a raw PNG, and that's the point. Apache is serving polyglot.php through the PHP interpreter because its real extension is .php, and PHP's rule for any file it runs is simple: everything outside an opening and closing PHP tag gets printed exactly as it already was, and only what's between those tags gets executed. Nearly this entire file sits outside any PHP tag, so almost all of it passes through untouched as literal binary output. The one piece that was written between inside the Comment field is the only part that actually ran, and in its place sits the string I built it to produce, the literal word START, the secret token itself, and the literal word END. Those markers are the only reason the token is easy to find at all in a response that's otherwise solid noise.
I copied the token and submitted it.
Why This Matters
A content-based image check is a real improvement over anything that only reads a filename, and nothing about this attack defeats that check through trickery. The file that gets uploaded genuinely is a valid image. The vulnerability isn't in the validation being wrong, it's in the assumption that a file can only honestly belong to one format at a time. A PNG's metadata fields and a PHP file's untagged regions are both spaces built to hold whatever the format doesn't actively police, and a polyglot just finds the overlap between the two.
Takeaways From This Lab
- Validating that an upload is a genuine image closes off every filename-based bypass at once, but it only guarantees the file is a real image, not that it's exclusively an image and nothing else.
- A polyglot works because image formats and PHP both leave certain regions of a file unexamined, metadata fields on one side, anything outside a PHP tag on the other, and a file can satisfy both sets of rules in the same bytes without either one noticing the other.
- Wrapping extracted output in clear markers like START and END earns its keep the moment the response comes back as raw binary, since picking a short token out of genuine noise by eye is far harder than searching for two words.