
Apache Spark 4.2.0 – Hoch – Remote – Dateilesen – History-Zip folgt gepflanztem Symlink
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-zip-symlink
Klasse: Dateilesen Reichweite: Remote
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
CVE-2025-54920 hat den History Server Jackson gesperrt, sodass sparkEventFromJson nur noch SparkListenerEvent-Unterklassen konstruiert. Das rollierende zipEventLogFiles packt weiterhin jedes listStatus-Kind über FileSystem.open ein. LocalFileSystem folgt einem POSIX-Symlink. Ein eingeschleustes Kind in eventlog_v2_* wird in das unauthentifizierte GET /api/v1/applications/{id}/logs gepackt. Der Zip-Member-Body ist das SHS-UID-Ziel. Der Eintragsname ist der Symlink-Basename.
Dies ist keine erneute Meldung der Jackson-RCE. Jackson bleibt geschlossen. Dies ist keine Host-RCE. Integrität und Verfügbarkeit sind nicht betroffen.
| ID | noch keine CVE |
| Klasse | Dateilesen (rollierender Zip-Überrest des Opfermodells von CVE-2025-54920) |
| Reichweite | Remote (Schreiber des History-Log-Verzeichnisses plus unauthentifiziertes GET des Zips) |
| CWE | CWE-59, CWE-22 |
| CVSS | Hoch: 7.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Produkt | Apache Spark |
| Betroffen | 4.2.0 Tag v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Auth | PR:L = Schreiben in spark.history.fs.logDirectory (gleiches Opfer wie 54920). History-UI-ACLs standardmäßig aus. |
| Lizenz | GNU Affero GPL v3.0 |
| Lab | nur 127.0.0.1:18980 |
Wer in spark.history.fs.logDirectory schreiben kann (Standard file:/tmp/spark-events), kann ein rollierendes Verzeichnis einschleusen, das der History Server zippen wird. Rollierende Writer sind standardmäßig aktiv. History-UI-ACLs sind standardmäßig aus. Die SHS-Prozess-UID ist der verwirrte Stellvertreter.
| Privileg | Methode / Sink | Im Lab bewiesen oder abgeleitet |
|---|---|---|
| Vertraulichkeit: eine Host-Datei lesen, die die SHS-UID öffnen kann | eventlog_v2_<id>/ mit gültigem appstatus_* plus events_* JSON (SparkListenerLogStart / SparkListenerApplicationStart) plus einem Symlink-Kind einschleusen, dessen Ziel außerhalb des Log-Verzeichnisses liegt. Auf Scan warten. Unauthentifiziertes GET /api/v1/applications/{id}/logs (OneApplicationResource.getEventLogs -> FsHistoryProvider.writeEventLogs -> RollingEventLogFilesFileReader.zipEventLogFiles -> FileSystem.open) | Im Lab bewiesen. Zip-Member eventlog_v2_app-2026-0001/planted-secret Body ist SPARK-HISTORY-ZIP-SYMLINK-WITNESS. Events-Member hat keinen Witness. Eintragsname bleibt der Symlink-Basename, nicht der Zielpfad. |
Vertraulichkeit: Event-Log eines benachbarten Mandanten, das der Angreifer nicht open kann (660) | Gleicher Zip, Symlink-Ziel ist das Log einer anderen App | Abgeleitet. Gleiches FileSystem.open. Nicht ausgeführt. |
Vertraulichkeit: Keytab, spark-env.sh, /etc/passwd, falls SHS sie lesen kann | Gleicher Zip | Abgeleitet. Orakel ist ein dediziertes /secret/host-secret, nicht diese Dateien. |
| Integrität von SHS- oder Host-Dateien | Zip wird in den HTTP-Body gelesen | Geschlossen. Im Lab als nicht vorhanden bewiesen. |
| Verfügbarkeit von SHS- oder Host-Dateien | Zip löscht oder kürzt das Ziel nicht | Geschlossen. Im Lab als nicht vorhanden bewiesen. |
| Host-RCE / Jackson-Gadget-Konstruktion | JsonProtocol.sparkEventFromJson | Geschlossen. SparkListenerEvent erforderlich. Dieses Lab schleust nur harmloses Event-JSON ein. |
Zip-Slip von ../ im Eintragsnamen | Path.getName() | Geschlossen. POSIX-Dateiname kann kein / enthalten. Eintrag ist eventlog_v2_<id>/<basename>. |
Pfad-String-Escape von resolveLogPath | new Path(logDir, attempt.logPath) | Geschlossen. Das Path-Objekt bleibt unter dem konfigurierten Log-Verzeichnis. Der Escape ist der Inode. |
| Compaction-Rewrite / Cleaner-Delete des Ziels | spark.history.fs.eventLog.rolling.maxFilesToRetain Standard Integer.MAX_VALUE; Cleaner standardmäßig aus | Geschlossen. Lokales fs.delete eines Symlinks entfernt den Link, nicht das Ziel. |
| Unauthentifizierter Job-Submit / ML-Modell-Load | SECURITY.md FAQ / docs/ml-security.md | Geschlossen. By Design, nicht dieser Überrest. |
Ich habe apache/spark v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) gepinnt statt master oder einen 4.3 RC. Achtundzwanzig Jagden. Die Jackson-History-RCE ist auf diesem Peel geschlossen (JsonProtocol.scala:965-972). Der History Server zeigte weiterhin auf ein angreiferschreibbares Log-Verzeichnis, was dasselbe Opfermodell wie CVE-2025-54920 ist.
zipEventLogFiles zippt alle files, nicht nur Event-Dateien:
override def zipEventLogFiles(zipStream: ZipOutputStream): Unit = {
val dirEntryName = rootPath.getName + "/"
zipStream.putNextEntry(new ZipEntry(dirEntryName))
files.foreach { file =>
addFileAsZipEntry(zipStream, file.getPath, dirEntryName + file.getPath.getName)
}
}
addFileAsZipEntry ist fileSystem.open dann transferTo. LocalFileSystem folgt POSIX-Symlinks. getLogDirInfo kommentiert "handles symbolic links properly" und durchläuft dann unaufgelöste Path-Eltern für einen Anzeigenamen.
Bereits dokumentierte Irrwege: SPARK-52381 als die ganze History-Story zu behandeln; den Witness in events_* einzuschleusen (das Lab scheitert daran); zu erwarten, dass der Zip-Eintrag den Zielpfad trägt; den Prefix-Join von resolveLogPath als den Escape zu behandeln; den Versuch einer HiveConnection-Event-Klasse auf diesem Pin.
python3 spark-history-zip-symlink-Abraxas-Labs.py
oder
cd lab
./run.sh
Gadget des rollierenden Zip-Dumps von EventLogFileReaders.scala, kein vollständiger History Server und nicht das offizielle spark:4.2.0-Image. Es parst Event-JSON nicht durch Jackson. Loopback 127.0.0.1:18980. Compose-Projekt spark-history-zip-symlink. Rollierendes Verzeichnis enthält gültiges appstatus_* + events_* (kein Witness) plus planted-secret -> /secret/host-secret.
SUCCESS SPARK-HISTORY-ZIP-SYMLINK SPARK-HISTORY-ZIP-SYMLINK-WITNESS
Nur events_* / appstatus_*-Kinder packen, nicht jeden listStatus-Namen. Mit No-Follow öffnen (O_NOFOLLOW / Hadoop-No-Symlink-Optionen) und ein Kind ablehnen, dessen realpath außerhalb von spark.history.fs.logDirectory liegt. Ein Display-Name-Helper ist keine realpath-Prüfung.