đź§ Gunicorn - Worker Timeout
What is it?
- Concept: An exploitation technique that leverages the internal lifecycle management of Gunicorn’s pre-fork architecture. By intentionally causing a worker thread to freeze (e.g., via a hanging socket connection), an attacker forces the Gunicorn master process to terminate and respawn the worker.
- Impact: Primarily Denial of Service (DoS). However, when chained with an Arbitrary File Write, it acts as the trigger for Remote Code Execution (RCE) via Python - C-Extension Precedence Hijacking.
How it works
To understand the attack, you have to understand the architecture:
- The Pre-fork Model: Gunicorn operates on a pre-fork worker model. A central “Master” process manages the server, but it doesn’t handle web requests itself. Instead, it forks multiple “Worker” processes. These workers execute the actual Python application (like Flask or Django) and process incoming HTTP traffic.
- The Heartbeat: Workers must constantly prove to the Master that they are healthy. They do this by periodically updating a temporary file (a “heartbeat”).
- The Freeze: If an attacker opens a connection and hangs it (e.g., declaring a large
Content-Lengthbut sending nothing), the worker initiates a synchronous by default (if gunicorn was run with--worker-class gevent, meaning it will be asynchronous, opening connection would not be enough), blockingread()on the socket. Because the worker is frozen waiting for data, it stops updating its heartbeat. - The Execution (SIGKILL & Respawn): Gunicorn has a default
--timeoutof 30 seconds. When the Master process notices a worker hasn’t heartbeated in 30 seconds, it assumes the worker is deadlocked. The Master sends aSIGKILLto violently terminate the worker, and immediately forks a fresh worker to replace it.
You can read more about this here with the keyword timeout
The Connection to Python - C-Extension Precedence Hijacking
Forcing a worker restart is useless on its own unless the environment has been pre-poisoned.
When the Master forks a new worker, that worker must initialize the Python web application from scratch. During initialization, Python reads import statements and searches the sys.path. Python’s internal module resolution strictly prioritizes compiled C extensions (.so) over standard source files (.py).
If an attacker has previously used a file upload vulnerability to drop a malicious helpers.so into a directory containing a legitimate helpers.py, the old workers wouldn’t notice—they already loaded the legitimate module into memory hours ago. However, the newly spawned worker evaluates the directory fresh. It sees the .so, prioritizes it, and executes the attacker’s compiled payload during the application startup sequence.
Exploitation
Prerequisites:
- Direct network access to the Gunicorn port (bypassing reverse proxies like Nginx, which would normally buffer requests and drop hanging sockets).
- A pre-staged payload (e.g., a malicious
.soor.pthfile) dropped via Arbitrary File Write.
Attack Vectors
The exploit relies on tying up the socket to trigger the timeout. While Slowloris trickles data, a direct Gunicorn connection just needs to hang.
# Theoretical Payload Concept (Socket Hang)
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((TARGET_IP, 5000))
# Send incomplete headers to freeze the worker's read() operation
s.send(b"POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 9999999\r\n\r\n")
# Do nothing. Wait 30+ seconds for the Gunicorn master to kill the worker.
# The replacement worker will trigger the staged module hijack.Mitigation
Reverse Proxies: Never expose Gunicorn directly to the internet. Always place it behind Nginx or HAProxy. These proxies handle the raw TCP connections, buffer the slow requests, and only pass fully formed HTTP requests to the Gunicorn workers, completely neutralizing socket-hang attacks.
Read-Only Filesystems: Run the Docker container with a read-only root filesystem (read_only: true in docker-compose) to prevent attackers from staging the .so file in the first place.
Related Usage
TABLE creation_date AS "Created"
FROM "05 - Content"
WHERE contains(techniques, this.file.link) AND contains(tags, "đźš©")
SORT file.name ASC