BentoML SSRF 漏洞(CVE-2025-54381) YASA 检测复现报告
BentoML SSRF 漏洞 YASA 检测复现报告
1. 漏洞发现过程与扫描命令说明
首先,准备好目标代码库并安装配置 YASA 工具。在本次扫描中,我们尝试针对 BentoML 框架中的 _bentoml_impl\serde.py 文件进行了分析。该文件负责处理请求的反序列化逻辑,其中包含一些发起 HTTP 请求的调用,可能会引起 SSRF
为了能够尽可能捕捉到调用链(如 httpx.AsyncClient 的链式调用) ,这次使用 fregex 去匹配,并略微扩展了 Source 的定义,rule_config_ssrf.json如下
[
{
"checkerIds": [
"taint_flow_python_input"
],
"entryPointMode": "test",
"entryPoints": [
{
"className": "MultipartSerde",
"functionName": "parse_request"
},
{
"className": "JSONSerde",
"functionName": "parse_request"
},
{
"className": "GenericSerde",
"functionName": "deserialize"
}
],
"sources": {
"TaintSource": [
{
"path": "request",
"scopeFile": "serde.py",
"scopeFunc": "parse_request"
}
],
"FuncCallReturnValueTaintSource": [
{
"attribute": "StarletteForm",
"calleeType": "",
"fsig": "form"
},
{
"attribute": "FormDataGetList",
"calleeType": "",
"fsig": "getlist"
},
{
"attribute": "RequestHeaders",
"calleeType": "",
"fsig": "headers"
},
{
"attribute": "RequestQueryParams",
"calleeType": "",
"fsig": "query_params"
},
{
"attribute": "RequestJson",
"calleeType": "",
"fsig": "json"
},
{
"attribute": "RequestBody",
"calleeType": "",
"fsig": "body"
},
{
"attribute": "DictGet",
"calleeType": "",
"fsig": "get",
"values": ["0"]
}
]
},
"sinks": {
"FuncCallTaintSink": [
{
"attribute": "SSRF_Httpx_Regex",
"fregex": ".*Client.*\\.(get|post|put|delete|request)",
"args": ["0"]
},
{
"attribute": "SSRF_Requests",
"calleeType": "requests",
"fsig": "get",
"args": ["0"]
},
{
"attribute": "SSRF_Urllib",
"calleeType": "urllib.request",
"fsig": "urlopen",
"args": ["0"]
},
{
"attribute": "FileAccess",
"fsig": "open",
"args": ["0"]
},
{
"attribute": "EnsureFile_Internal",
"fsig": "ensure_file",
"args": ["0"]
}
]
}
}
]执行的扫描命令如下:
/YASA/YASA-Engine/yasa-engine-linux-x64 \
--checkerPackIds "taint-flow-python-default" \
--analyzer "PythonAnalyzer" \
--ruleConfigFile "/YASA/examples/python-demo/rule_config_ssrf.json" \
--sourcePath "/YASA/examples/python-demo/serde.py" \
--single \
--uastSDKPath "/YASA/uast4py-linux-amd64" \
--report "/YASA/examples/python-demo/CVE-2025-54381.json"2. 输出结果分析
====================== Analysis Overview =====================
Language : python
Files analyzed : 1
Lines of code : 275
Total time : 474ms
Total instruction : 1033
Executed instruction : 1033
Execution count : 2223
Sources configured : 9
Sinks configured : 5
Valid entrypoints : 16
Avg execution time per instruction : 0.00ms
Avg instruction execution count : 2.15
Execution time 70%/99%/100% : 0.00ms/0.00ms/0.00ms
Execution times 70%/99%/100% : 2.00/4.00/57.00
================================================================
=================== Performance Statistics ===================
total cost: 474ms
preProcess cost: 403ms
startAnalyze cost: 17ms
makeFullCallGraph(BySymbolInterpret) cost: 16ms
symbolInterpret cost: 53ms
================================================================
Found 3 potential output strategy files
Registered strategy: callgraph from callgraph-output-strategy.js
Registered strategy: interactive from interactive-output-strategy.js
Registered strategy: taintflow from taint-output-strategy.js
Successfully registered 3 output strategies
======================= outputFindings =======================
======================== Findings ========================
------------- 1 : taint_flow_python_input -------------
Description: Python污点分析checker,会使用CallGraph边界制作entrypoint
File: /serde.py
Line 169: client.get(url)
SINK RULE: undefined
SINK Attribute: SSRF_Httpx_Regex
entrypoint:
{
filePath: '/serde.py',
functionName: 'parse_request',
attribute: 'fullCallGraphMade',
type: 'functionCall',
packageName: undefined,
funcReceiverType: ''
}
Trace:
/serde.py
AffectedNodeName: request.body()
164: SOURCE: body = await request.body()
/serde.py
AffectedNodeName: body
164: Var Pass: body = await request.body()
/serde.py
AffectedNodeName: url
166: Var Pass: if is_http_url(url := body.decode("utf-8", "ignore")):
/serde.py
AffectedNodeName: client.get
169: SINK: resp = await client.get(url)
------------- 2 : taint_flow_python_input -------------
Description: Python污点分析checker,会使用CallGraph边界制作entrypoint
File: /serde.py
Line 195: client.get(obj)
SINK RULE: undefined
SINK Attribute: SSRF_Httpx_Regex
entrypoint:
{
filePath: '/serde.py',
functionName: 'parse_request',
attribute: 'fullCallGraphMade',
type: 'functionCall',
packageName: undefined,
funcReceiverType: ''
}
Trace:
/serde.py
AffectedNodeName: form.getlist(k)
210: SOURCE: value = [await self.ensure_file(v) for v in form.getlist(k)]
/serde.py
AffectedNodeName: obj
193: Var Pass: obj = obj.strip("\"'") # The url may be JSON encoded
/serde.py
AffectedNodeName: client.get
195: SINK: resp = await client.get(obj)
==========================================================
# Total-findings : 2
==========================================================
report is write to /YASA/examples/python-demo/CVE-2025-54381.json/report.sarif
================================================================
analyze doneYASA 成功检测到了 2 条 SSRF 的 Findings。这些发现涵盖了从直接的请求体利用到复杂的跨函数数据流传递。下面的表格汇总了这 2 条漏洞的关键信息:
| 序号 | 文件名 | 函数名 | 行号 | 污点源 (Source) | 污点汇 (Sink) | 风险描述 |
|---|---|---|---|---|---|---|
| 1 | serde.py | parse_request | 169 | request.body() | client.get(url) | 攻击者控制请求体导致的直接 SSRF |
| 2 | serde.py | parse_request | 195 | form.getlist(k) | client.get(obj) | [CVE 复现] 经由 ensure_file 传递的 Multipart SSRF |
3. 源代码片段审查与漏洞路径追踪分析
根据扫描日志提供的污点传播路径,结合 serde.py 源代码片段,对每条漏洞进行简要分析。
(1)基于 Request Body 的直接 SSRF
定位:serde.py 中的 parse_request 函数,第 169 行发生敏感操作 client.get(url)。
关键代码:
164: body = await request.body() # Source: 获取原始请求体
166: if is_http_url(url := body.decode(...)): # Pass: 解码并赋值给 url
169: resp = await client.get(url) # Sink: 发起 HTTP 请求漏洞路径分析: YASA 识别出 request.body() 是一个不可信的输入源。数据流非常直接:攻击者发送一个 HTTP 请求,其 Body 内容为一个恶意的 URL(例如内网地址)。代码在第 166 行简单解码后,直接在第 169 行使用 client.get 发起请求。虽然有 is_http_url 检查,但该检查仅验证格式而不验证目标地址的安全性,则 SSRF 成立。
风险描述: 这是最典型的 SSRF 模式。攻击者可以直接利用服务器作为代理,扫描内网端口或攻击内网服务。
(2)基于 Multipart Form 的间接 SSRF (CVE 核心路径)
定位:serde.py 中的 parse_request 函数调用了 ensure_file,最终在第 195 行触发 client.get(obj)。
关键代码:
# 在 parse_request 函数中
210: value = [await self.ensure_file(v) for v in form.getlist(k)] # Source: 遍历表单数据
# 进入 ensure_file 函数
193: obj = obj.strip("\"'") # Pass: 简单的字符串处理
195: resp = await client.get(obj) # Sink: 对处理后的数据发起请求漏洞路径分析: 这是本次分析的重点,也是 BentoML 相关 CVE 的核心逻辑。
- Source: 污点始于第 210 行的
form.getlist(k)。这是处理multipart/form-data请求的标准方式,用户可控。 - Propagation: 污点数据
v被传递给了辅助方法self.ensure_file(v)。YASA 的 PythonAnalyzer 成功追踪了这次跨函数的参数传递。 - Sink: 在
ensure_file内部,传入的参数被重命名为obj。经过简单的去引号处理(第 193 行)后,直接传入了client.get(obj)(第 195 行)。
风险描述: 此路径比第一种更隐蔽。开发者可能认为 ensure_file 只是处理文件逻辑,忽略了它内部会根据输入内容发起网络请求。攻击者通过构造恶意的 Multipart 表单字段,诱导服务器去请求恶意 URL。YASA 通过 fregex 成功匹配了 client.get 这一动态调用,并准确还原了从表单列表到内部函数调用的完整数据流。
4. 总结
通过对 BentoML serde.py 的扫描与分析,得出以下结论:
- 复杂数据流追踪能力验证:YASA 成功复现了 CVE 报告中的攻击路径,证明了其在处理 Python 列表推导式和跨函数调用时的污点追踪能力。
- 正则匹配的有效性:针对 Python 中常见的动态类型问题(如
httpx.AsyncClient实例),通过配置fregex: "client\\.get", 成功捕获了静态类型分析可能遗漏的 Sink 点。 - 发现额外风险:除了复现已知的 CVE 路径外,还发现了另一处基于 Request Body 的直接 SSRF 风险(Findings #1)