sid6224/CVE-2025-60012-POC
GitHub: sid6224/CVE-2025-60012-POC
针对 Apache Livy 未授权文件访问漏洞(CVE-2025-60012)的概念验证项目,含完整 Docker 环境和攻击演示脚本。
Stars: 0 | Forks: 0
# CVE-2025-60012 — Apache Livy 未授权文件访问
## 概述
| 字段 | 详情 |
|--------------|--------|
| CVE ID | CVE-2025-60012 |
| 严重程度 | 中危 (CVSS 6.3) |
| 受影响版本 | Apache Livy 0.7.0-incubating, 0.8.0-incubating — 当连接到 Apache Spark 3.1 或更高版本时 |
| 修复版本 | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: 输入验证不恰当 |
| 披露日期 | 2026-03-13 |
| 报告者 | Furue Hideyuki |
## 漏洞描述
拥有 Livy REST 或 JDBC 接口访问权限的已认证用户可以提交带有精心构造配置值的 Spark 会话
或批处理作业。两个缺陷结合在一起,允许攻击者
引用其允许路径之外的本地文件系统文件:
1. **缺少对 `spark.archives` 的验证** — Spark 3.1 引入了 `spark.archives` 作为
一种跨所有集群管理器分发归档文件的统一方式。Livy 0.8.0 中经过路径验证的配置键硬编码
列表(`HARDCODED_SPARK_FILE_LISTS`)不包含
`spark.archives`。因此,通过此键传递的路径永远不会根据
本地文件系统白名单(`livy.file.local-dir-whitelist`)进行检查,从而允许攻击者
引用任何本地文件。
2. **白名单检查中的路径遍历绕过** — 即使对于经过验证的配置键,
Livy 0.8.0 中的白名单比较也使用普通 Java String `startsWith`
调用处理原始路径。攻击者可以使用路径遍历绕过此检查:
`/whitelisted/dir/../../etc/passwd` 通过了字符串检查,但解析到了
允许目录之外。
## 受影响的源文件
### 文件 1 — `LivyConf.scala`
**易受攻击 (v0.8.0):**
https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
**已修复 (v0.9.0):**
https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
### 文件 2 — `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/` |
## 确切代码差异
差异是通过在本地克隆两个标签(见上文)并运行以下命令生成的:
```
diff -u livy-0.8.0/server/src/main/scala/org/apache/livy/LivyConf.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/LivyConf.scala
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
```
### 修复 1 — `LivyConf.scala`: `spark.archives` 已添加到硬编码文件列表
```
private val HARDCODED_SPARK_FILE_LISTS = Seq(
SPARK_JARS,
SPARK_FILES,
SPARK_ARCHIVES,
SPARK_PY_FILES,
+ "spark.archives", // <-- ADDED in v0.9.0 (Spark 3.1+ config key)
"spark.yarn.archive",
"spark.yarn.dist.files",
"spark.yarn.dist.jars",
"spark.yarn.jar",
"spark.yarn.jars"
)
```
**v0.8.0 中缺少条目的影响:**
当用户提交包含 `conf: {"spark.archives": "file:///etc/passwd"}` 的会话时,Livy
0.8.0 永远不会对该值调用 `resolveURIs()`,也永远不会根据
`livy.file.local-dir-whitelist` 对其进行检查。该路径在未经验证的情况下转发给 Spark。
### 修复 2 — `Session.scala`: 白名单检查前的路径规范化
```
def resolveURI(uri: URI, livyConf: LivyConf): URI = {
...
if (resolved.getScheme() == "file") {
- 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/../../../etc/passwd
```
- v0.8.0: `"/opt/safe-data/../../../etc/passwd".startsWith("/opt/safe-data")` → **true** (被绕过)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (被阻止)
## 攻击向量摘要
```
Attacker (authenticated REST/JDBC user)
│
▼
POST /sessions (or /batches)
{
"conf": {
"spark.archives": "file:///etc/shadow" ← Attack 1: unvalidated Spark 3.1 key
"spark.jars": "file:///safe/../etc/shadow" ← Attack 2: path traversal bypass
}
}
│
▼
Livy 0.8.0 — validation skipped / bypassed
│
▼
Spark reads the file and distributes it to executors
│
▼
Attacker retrieves file contents via job output / logs
```
## 测试环境
此 PoC 中的所有步骤均在以下系统上执行并验证:
| 组件 | 详情 |
|-----------|--------|
| 主机 OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| 内核 | 6.17.0-14-generic x86\_64 |
| 架构 | x86\_64 |
| 总内存 | 15.49 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 (Scala 2.12 build) |
| Livy 版本 — 已修复镜像 | 0.9.0-incubating (Scala 2.12 build) |
## 概念验证
### 概述
```
docker/vulnerable/ → image: cve-2025-60012-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → image: cve-2025-60012-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 (both attacks HTTP 201) → stop container
Step 3: Build fixed image → start container → verify Livy is up
Step 4: Run validate.sh → confirm FIXED (both attacks 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-60012-vulnerable docker/vulnerable/
```
**验证 — 镜像已创建:**
```
docker images cve-2025-60012-vulnerable
```
预期输出:
```
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-60012-vulnerable latest
标签:Apache Livy, Apache Spark, CVE-2025-60012, CVSS 6.3, CWE-20, GHAS, Java安全, JS文件枚举, POC验证, SDLC, 中间件漏洞, 任意文件读取, 大数据安全, 应用安全, 文件读取漏洞, 未授权访问, 消息认证码, 白名单绕过, 网络安全审计, 请求拦截, 路径遍历, 输入验证绕过