
Apache Livy Unauthorized File Access Vunerability에 대한 POC
교육 및 보안 연구 목적으로만 사용하십시오. 귀하가 소유하지 않거나 테스트에 대한 명시적 서면 허가를 받지 않은 시스템에 사용하지 마십시오. → 전체 면책 조항
| 필드 | 세부 정보 |
|---|---|
| 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 세션 또는 배치 작업을 제출할 수 있습니다. 두 가지 취약점이 결합되어 공격자가 허용된 경로 외부의 로컬 파일 시스템 파일을 참조할 수 있게 합니다.
spark.archives에 대한 검증 누락 — Spark 3.1은 모든 클러스터 관리자에서 아카이브 파일을 배포하는 통합 방법으로 spark.archives를 도입했습니다. Livy 0.8.0의 경로 검증 대상 구성 키 하드코딩 목록(HARDCODED_SPARK_FILE_LISTS)에는 spark.archives가 포함되어 있지 않습니다. 따라서 이 키를 통해 전달된 경로는 로컬 파일 시스템 허용 목록(livy.file.local-dir-whitelist)에 대해 검사되지 않으므로 공격자가 모든 로컬 파일을 참조할 수 있습니다.
허용 목록 검사의 경로 탐색 우회 — 검증되는 구성 키의 경우에도 Livy 0.8.0의 허용 목록 비교는 원시 경로에 대한 일반 Java String startsWith 호출을 사용합니다. 공격자는 경로 탐색을 사용하여 이를 우회할 수 있습니다. /whitelisted/dir/../../etc/passwd는 문자열 검사를 통과하지만 허용된 디렉토리 외부로 해석됩니다.
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
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```bash
git clone --depth=1 --branch v0.8.0-incubating
https://github.com/apache/incubator-livy
livy-0.8.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/` |
## 정확한 코드 차이점
차이점은 두 태그를 로컬에서 클론하고(위 참조) 다음을 실행하여 생성되었습니다:```bash
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
LivyConf.scala: spark.archives 하드코딩된 파일 목록에 추가됨```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**v0.8.0에서 누락된 항목의 영향:**
사용자가 `conf: {"spark.archives": "file:///etc/passwd"}`로 세션을 제출하면 Livy
0.8.0은 해당 값에 대해 `resolveURIs()`를 호출하지 않으며 `livy.file.local-dir-whitelist`에 대해 검사하지 않습니다.
이 경로는 검증되지 않은 상태로 Spark에 전달됩니다.
---
### Fix 2 — `Session.scala`: 허용 목록 검사 전 경로 정규화```diff
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
| 구성 요소 | 세부 사항 |
|---|---|
| 호스트 OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| 커널 | 6.17.0-14-generic x86_64 |
| 아키텍처 | x86_64 |
| 총 메모리 | 15.49 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 (Scala 2.12 build) |
| Livy 버전 — 수정 이미지 | 0.9.0-incubating (Scala 2.12 build) |
. ├── LICENSE ├── README.md ├── docker/ │ ├── fixed/ │ │ ├── Dockerfile │ │ └── livy.conf │ └── vulnerable/ │ ├── Dockerfile │ └── livy.conf ├── livy-0.8.0/ ← Apache Livy 0.8.0-incubating source ├── livy-0.9.0/ ← Apache Livy 0.9.0-incubating source └── test/ └── validate.sh
---
## 개념 증명
### 개요```
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
> **참고:** `docker run` 후 Livy가 준비되기까지 약 15~20초가 소요됩니다.
> 아래 모든 단계에는 API 호출 전에 명시적인 `sleep 20`이 포함되어 있습니다.
---
### 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. 이미지 빌드:**```bash
docker build -t cve-2025-60012-vulnerable docker/vulnerable/
검증 — 이미지가 생성되었습니다:```bash docker images cve-2025-60012-vulnerable
예상 출력:```
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-60012-vulnerable latest <id> <time> <size>