sid6224/CVE-2025-66249-POC
GitHub: sid6224/CVE-2025-66249-POC
Apache Livy路径穿越白名单绕过漏洞(CVE-2025-66249)的概念验证项目,提供Docker复现环境与自动化验证脚本。
Stars: 0 | Forks: 0
# CVE-2025-66249 — Apache Livy 路径穿越白名单绕过








## 概述
| 字段 | 详情 |
|-----------|--------|
| CVE ID | CVE-2025-66249 |
| 严重程度 | Important (CVSS N/A — 截至 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` 检查可以通过路径穿越 Payload 绕过。
示例:如果 `livy.file.local-dir-whitelist = /opt/safe-data`
```
/opt/safe-data/../sensitive/secret.txt
```
- v0.8.0: `"/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data")` → **true** (已绕过)
- v0.9.0: `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
```
## 攻击向量摘要
```
Attacker (authenticated REST/JDBC user)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← path starts with whitelisted prefix — String.startsWith() passes
← but resolves OUTSIDE the directory via ../ traversal
}
}
│
▼
Livy 0.8.0 — whitelist check bypassed (raw startsWith, no normalisation)
│
▼
Spark reads the file and distributes it to executors
│
▼
Attacker retrieves file contents via job output / logs
```
## 测试环境
本 PoC 中的所有步骤均在以下系统上执行和验证:
| 组件 | 详情 |
|----------------------------------|--------|
| 主机操作系统 | Ubuntu 24.04.4 LTS (Noble Numbat) |
| 内核 | 6.17.0-14-generic x86\_64 |
| 架构 | x86\_64 |
| 总内存 | 15 GiB |
| Docker Engine | 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/ → image: cve-2025-66249-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → image: cve-2025-66249-fixed (Livy 0.9.0 + Spark 3.1.3)
test/validate.sh → single script, run unchanged against both environments
```
完整的端到端流程 — 按顺序执行步骤 1 至 4:
```
Step 1: Build vulnerable image → start container → verify Livy is up
Step 2: Run validate.sh → confirm VULNERABLE (attack HTTP 201) → stop container
Step 3: Build fixed image → start container → verify Livy is up
Step 4: Run validate.sh → confirm FIXED (attack HTTP 400) → stop container
```
### 步骤 1 — 构建并启动易受攻击的环境 (Livy 0.8.0 + Spark 3.1.3)
**文件:**
- `docker/vulnerable/Dockerfile` — eclipse-temurin:11-jdk-focal, Spark 3.1.3, Livy 0.8.0-incubating
- `docker/vulnerable/livy.conf` — 绑定 `0.0.0.0:8998`, 本地模式, 白名单 = `/opt/safe-data`
**1a. 构建镜像:**
```
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
标签:Apache Livy, CVE-2025-66249, CWE-22, JS文件枚举, Maven, POC, Scala, SDLC, Spark, 中间件漏洞, 任意文件访问, 大数据安全, 应用安全, 文件读取, 漏洞复现, 漏洞验证, 白名单绕过, 网络安全审计, 请求拦截, 路径遍历