Skip to content

BentoML SSRF 漏洞(CVE-2025-54381) YASA 检测复现报告

约 1509 字大约 5 分钟

Python

2025-12-18

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 done

YASA 成功检测到了 2 条 SSRF 的 Findings。这些发现涵盖了从直接的请求体利用到复杂的跨函数数据流传递。下面的表格汇总了这 2 条漏洞的关键信息:

序号文件名函数名行号污点源 (Source)污点汇 (Sink)风险描述
1serde.pyparse_request169request.body()client.get(url)攻击者控制请求体导致的直接 SSRF
2serde.pyparse_request195form.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 的核心逻辑。

  1. Source: 污点始于第 210 行的 form.getlist(k)。这是处理 multipart/form-data 请求的标准方式,用户可控。
  2. Propagation: 污点数据 v 被传递给了辅助方法 self.ensure_file(v)。YASA 的 PythonAnalyzer 成功追踪了这次跨函数的参数传递。
  3. Sink: 在 ensure_file 内部,传入的参数被重命名为 obj。经过简单的去引号处理(第 193 行)后,直接传入了 client.get(obj)(第 195 行)。

风险描述: 此路径比第一种更隐蔽。开发者可能认为 ensure_file 只是处理文件逻辑,忽略了它内部会根据输入内容发起网络请求。攻击者通过构造恶意的 Multipart 表单字段,诱导服务器去请求恶意 URL。YASA 通过 fregex 成功匹配了 client.get 这一动态调用,并准确还原了从表单列表到内部函数调用的完整数据流。

4. 总结

通过对 BentoML serde.py 的扫描与分析,得出以下结论:

  1. 复杂数据流追踪能力验证:YASA 成功复现了 CVE 报告中的攻击路径,证明了其在处理 Python 列表推导式和跨函数调用时的污点追踪能力。
  2. 正则匹配的有效性:针对 Python 中常见的动态类型问题(如 httpx.AsyncClient 实例),通过配置 fregex: "client\\.get", 成功捕获了静态类型分析可能遗漏的 Sink 点。
  3. 发现额外风险:除了复现已知的 CVE 路径外,还发现了另一处基于 Request Body 的直接 SSRF 风险(Findings #1)