Apache Spark 4.2.0 - 높음 - 원격 - 파일 쓰기 - Connect 아카이브 해시 프래그먼트 대상
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-connect-archive-fragment
분류: 파일 쓰기 도달 범위: 원격
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
Spark Connect AddArtifacts 이름 중 archives/<file>#<fragment> 형태는 해시 / 샵 문자를 기준으로 분리됩니다. zip 바이트는 concatenatePaths를 거칩니다. fragment는 그렇지 않습니다. 그런 다음 SparkContext.addFile(..., isArchive=true)가 드라이버에서 new File(root, uri.getFragment), deleteRecursively(dest), unpack을 수행합니다. 익스큐터도 동일한 dest를 구성합니다. SPARK-33530은 #name을 SparkFiles 아래의 디렉터리 이름으로 문서화했습니다. ../<outside> 형태의 fragment가 dest가 됩니다.
Connect archives/<file>#../<path>는 세션 SparkFiles 디렉터리 외부에서 Spark UID 권한으로 압축을 풀고 deleteRecursively를 수행합니다.
| ID | 아직 CVE 없음 |
| 분류 | 파일 쓰기 |
| 도달 범위 | 원격 (Spark Connect 클라이언트; archive=True인 AddArtifacts) |
| CWE | CWE-22 |
| CVSS | 높음: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H |
| 제품 | Apache Spark |
| 영향받는 버전 | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) Connect archives/ 해시-fragment dest |
| 인증 | Connect 클라이언트. 기본 Connect에는 PSK가 없음(spark.connect.authenticate.token 미설정). PSK / 네트워크 ACL이 있으면 PR:L; 기본 개방형 Connect에서는 PR:N. |
| 라이선스 | GNU Affero GPL v3.0 |
| 랩 | 127.0.0.1 전용 |
태그 v4.2.0의 peel 32f72996(2026-07-11, v4.2.0-rc6)을 고정했습니다. 랩은 고정된 StagedArtifact 해시 분리, ArtifactUtils.concatenatePaths, new File(root, fragment), deleteRecursively, 그리고 dest 내부에 한정된 zip 압축 해제를 구현한 Java 가젯입니다. 127.0.0.1:18930에서 루프백 HTTP를 사용합니다. 공식 PySpark는 이미 URI fragment를 아카이브 이름에 복사합니다.
이것은 RCE가 아닙니다. dest를 벗어나는 zip 멤버는 거부됩니다(Hadoop FileUtil.unZip / HADOOP-13649 계열). 남는 것은 dest 자체입니다.
| 방법 | 싱크 | 권한 |
|---|---|---|
Connect AddArtifacts 이름 archives/<file>#../<outside> | SparkContext.addFile dest new File(root, uri.getFragment) 후 Utils.unpack (SparkContext.scala:1891-1900) | 무결성 (랩 입증): 세션 SparkFiles 외부에 Spark UID 권한으로 아카이브 멤버를 씁니다. 랩 dest 정규 경로 /lab/oracle/SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS, uid 1000, dest-outside=yes. |
| 동일 dest, 압축 해제 전 | Utils.deleteRecursively(dest) (SparkContext.scala:1899) | 가용성 (랩 입증): dest 카나리아가 사라짐. 랩 deleteRecursively=yes; dest 아래의 canary.txt는 살아남지 못함. 형제 파일 /lab/oracle/canary-keep.txt는 유지됨. |
| 아카이브를 가져오는 각 익스큐터에서 동일한 프리미티브 | Executor dest new File(root, sourceURI.getFragment) 후 deleteRecursively + unpack (Executor.scala:1468-1477) | 무결성 / 가용성 (추론): 익스큐터에서도 동일한 dest 구성. 랩은 드라이버 측 가젯임. |
아티팩트 이름 files/../../... 또는 archives/../../... | ArtifactUtils.concatenatePaths (ArtifactUtils.scala:26-39) | 차단됨 (랩 입증): INVALID_ARTIFACT_PATH. |
Unix 절대 경로 fragment /abs | new File(root, "/abs") | Unix에서 차단됨 (랩 입증): 부모가 무시되지 않음; dest는 root 아래에 결합됨. 랩 dest /lab/root/lab/oracle/abs-..., outside=false, abs 카나리아 유지됨. |
자체 세션 jars/ / classes/ 추가 | 세션 URLClassLoader | 잔여 사항으로 차단됨: 자체 세션 JAR 로드는 설계상 허용됨. 이번 발견 사항이 아님. |
| 교차 세션 jar 로드 | Connect 격리 spark.sql.artifact.isolation.enabled=true | 차단됨: 격리된 세션은 addedJars(sessionUUID)를 읽음. |
랩 입증 vs 추론: SparkFiles 외부의 해시-fragment dest, dest 카나리아 삭제, 형제 카나리아 유지, files/ 및 archives/ 이름 결합 거부, Unix 절대 경로 fragment가 root 아래에 결합, uid 1000, dest 내부에 한정된 zip 압축 해제: 랩. 동일한 dest의 익스큐터 가져오기: 추론(동일한 생성자). 드라이버 클래스패스에서 나중에 로드되는 클래스 덮어쓰기: 추론된 연쇄로, 입증되지 않았으며 RCE로 과장하지 않음.
요구 사항: archive=True로 AddArtifacts를 수행할 수 있는 Connect 클라이언트. 공식 PySpark spark.addArtifact(..., archive=True)는 이미 비어 있지 않은 URI fragment를 이름에 넣습니다(python/pyspark/sql/connect/client/artifact.py:223-236). 기본 Connect에는 PSK가 없습니다.
| 공격 표면 | 랩 |
|---|---|
아티팩트 이름 .. / concatenatePaths를 통한 절대 경로 | files-concat=rejected INVALID_ARTIFACT_PATH |
archives/ 이름 탐색 archives/../../... | 동일한 INVALID_ARTIFACT_PATH |
Unix new File(root, "/abs") | dest가 root 아래에 위치; abs 오라클 카나리아 유지됨 |
| 포함된 dest 아래의 zip 멤버 zip-slip | 가젯이 dest를 벗어나는 멤버를 거부함(HADOOP-13649 계열). 이번 잔여 사항이 아님. |
files/ 경로 결합 (해시 분리 없음) | fragment가 사용되지 않음; 이름은 여전히 concatenatePaths |
| 교차 세션 jar 격리 | Connect에서 기본 활성화 |
| 개방형 Connect에서의 무인증 SQL / Python | 설계상 작업 제출(SECURITY.md FAQ). 이번 잔여 사항이 아님. |
| 작업 제출 / ML 모델 로드 | 설계상 허용 |
StagedArtifact는 archives와 서버 File.separator로 시작하는 이름만 분리하며, #에서만 분리합니다:
val splits = name.split("#")
if (splits.length == 2) {
(splits(0), Some(splits(1)))
}
splits(0)은 concatenatePaths를 통해 스테이징됩니다. splits(1)은 fragment로 유지됩니다. ArtifactManager는 해당 fragment를 세션 파일 서버 URI에 붙이고 SparkContext.addArchive를 호출합니다. Dest는 new File(root, uri.getFragment)입니다. SPARK-33530은 sc.addArchive(s"${jarFile.getAbsolutePath}#foo")를 SparkFiles 아래의 디렉터리 이름으로 테스트합니다.
헌트 첫 번째 패스에서는 Unix 절대 경로 fragment가 root를 무시한다고 주장했습니다(절대 경로 자식에 대한 java.io.File 부모 무시). Unix에서 new File(root, "/abs")는 선행 슬래시를 제거하고 root 아래에 결합합니다. 부모는 Windows에서만 무시됩니다. 이 고정 버전에서의 High 경로는 #../입니다.
cd lab
./run.sh
루프백 127.0.0.1:18930. Compose 프로젝트 spark-connect-archive-fragment. 이미지 eclipse-temurin:21-jdk-jammy, 런타임 uid 1000. Witness SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS. 마지막 줄:
SUCCESS SPARK-CONNECT-ARCHIVE-FRAGMENT how=gadget dest-outside=yes deleteRecursively=yes files-concat=rejected uid=1000 SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS
CONTROL: archives/payload.zip은 /lab/root 아래에 압축이 풀립니다. ATTACK: archives/payload-frag.zip#../oracle/SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS dest 정규 경로 /lab/oracle/..., 카나리아 삭제됨, pwned.txt가 uid 1000으로 작성됨. Unix 절대 경로 fragment는 root 아래에 결합됩니다. 스택은 EXIT 시 정리됩니다.
아카이브 fragment를 아티팩트 이름과 동일한 root 하위 유지 검사(concatenatePaths, 또는 .. / 절대 경로 거부)를 통과시키십시오. SPARK-33530의 #name은 SparkFiles 아래의 단일 디렉터리 구성 요소로 유지하십시오. 해당 검사를 통과하지 못한 dest에 대해 deleteRecursively를 수행하지 마십시오. 익스큐터도 동일한 생성자를 사용하므로 둘 다 수정하십시오.
그때까지는 Spark UID 파일 시스템을 이미 신뢰하는 대상이 아닌 한 Spark Connect를 노출하지 마십시오. spark.connect.authenticate.token에 PSK를 설정하면 토큰 보유자로 기준이 높아질 뿐입니다. fragment를 정화하지는 않습니다.