
Apache Spark 4.2.0 - Élevé - À distance - Lecture de fichier - Le zip d'historique suit un lien symbolique planté
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-zip-symlink
Classe : Lecture de fichier Portée : Distante
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
CVE-2025-54920 a verrouillé Jackson dans le History Server afin que sparkEventFromJson ne construise que des sous-classes de SparkListenerEvent. Le rolling zipEventLogFiles vide toujours chaque enfant de listStatus via FileSystem.open. LocalFileSystem suit un lien symbolique POSIX. Un enfant planté dans eventlog_v2_* est empaqueté dans le GET /api/v1/applications/{id}/logs non authentifié. Le corps du membre du zip est la cible du SHS-UID. Le nom de l'entrée est le basename du lien symbolique.
Ce n'est pas un nouveau signalement du RCE Jackson. Jackson reste fermé. Ce n'est pas un RCE hôte. L'intégrité et la disponibilité ne sont pas affectées.
| ID | pas encore de CVE |
| Classe | Lecture de fichier (résidu du zip rolling du modèle de victime CVE-2025-54920) |
| Portée | Distante (écriture dans le répertoire de logs du History plus GET non authentifié du zip) |
| CWE | CWE-59, CWE-22 |
| CVSS | Élevé : 7.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Produit | Apache Spark |
| Affecté | 4.2.0 tag v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Auth | PR:L = écriture dans spark.history.fs.logDirectory (même victime que 54920). Les ACL de l'interface History sont désactivées par défaut. |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18980 uniquement |
Quiconque peut écrire dans spark.history.fs.logDirectory (par défaut file:/tmp/spark-events) peut planter un répertoire rolling que le History Server va zipper. Les writers rolling sont activés par défaut. Les ACL de l'interface History sont désactivées par défaut. L'UID du processus SHS est le député confus.
| Privilège | Méthode / sink | Prouvé en lab ou déduit |
|---|---|---|
| Confidentialité : lire un fichier hôte que l'UID SHS peut ouvrir | Planter eventlog_v2_<id>/ avec un appstatus_* valide plus un JSON events_* (SparkListenerLogStart / SparkListenerApplicationStart) plus un enfant lien symbolique dont la cible se trouve hors du répertoire de logs. Attendre le scan. GET /api/v1/applications/{id}/logs non authentifié (OneApplicationResource.getEventLogs -> FsHistoryProvider.writeEventLogs -> RollingEventLogFilesFileReader.zipEventLogFiles -> FileSystem.open) | Prouvé en lab. Le corps du membre du zip eventlog_v2_app-2026-0001/planted-secret est SPARK-HISTORY-ZIP-SYMLINK-WITNESS. Le membre events n'a pas de témoin. Le nom de l'entrée reste le basename du lien symbolique, pas le chemin cible. |
Confidentialité : log d'événements d'un locataire voisin que l'attaquant ne peut pas open (660) | Même zip, la cible du lien symbolique est le log d'une autre application | Déduit. Même FileSystem.open. Non exercé. |
Confidentialité : keytab, spark-env.sh, /etc/passwd si le SHS peut les lire | Même zip | Déduit. L'oracle est un /secret/host-secret dédié, pas ces fichiers. |
| Intégrité des fichiers du SHS ou de l'hôte | le zip est lu dans le corps HTTP | Fermé. Prouvé en lab : aucun. |
| Disponibilité des fichiers du SHS ou de l'hôte | le zip ne supprime ni ne tronque la cible | Fermé. Prouvé en lab : aucun. |
| RCE hôte / construction de gadget Jackson | JsonProtocol.sparkEventFromJson | Fermé. SparkListenerEvent requis. Ce lab ne plante que du JSON Event bénin. |
Zip slip de ../ dans le nom de l'entrée | Path.getName() | Fermé. Un nom de fichier POSIX ne peut pas contenir /. L'entrée est eventlog_v2_<id>/<basename>. |
Évasion par chaîne de chemin de resolveLogPath | new Path(logDir, attempt.logPath) | Fermé. L'objet Path reste sous le répertoire de logs configuré. L'évasion est l'inode. |
| Réécriture de compaction / suppression par le cleaner de la cible | spark.history.fs.eventLog.rolling.maxFilesToRetain par défaut Integer.MAX_VALUE ; cleaner désactivé par défaut | Fermé. Un fs.delete local d'un lien symbolique supprime le lien, pas la cible. |
| Soumission de job non authentifiée / chargement de modèle ML | FAQ SECURITY.md / docs/ml-security.md | Fermé. Par conception, pas ce résidu. |
J'ai épinglé apache/spark v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) plutôt que master ou une RC 4.3. Vingt-huit chasses. Le RCE Jackson du History est fermé sur ce peel (JsonProtocol.scala:965-972). Le History Server pointait toujours vers un répertoire de logs accessible en écriture par l'attaquant, ce qui est le même modèle de victime que CVE-2025-54920.
zipEventLogFiles zippe tous les files, pas seulement les fichiers d'événements :
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 est fileSystem.open puis transferTo. LocalFileSystem suit les liens symboliques POSIX. getLogDirInfo commente « handles symbolic links properly » puis parcourt les parents Path non résolus pour un nom d'affichage.
Fausses pistes déjà consignées : traiter SPARK-52381 comme toute l'histoire du History ; planter le témoin dans events_* (le lab échoue à cela) ; s'attendre à ce que l'entrée du zip porte le chemin cible ; traiter la jointure de préfixe de resolveLogPath comme l'évasion ; essayer une classe Event HiveConnection sur ce pin.
python3 spark-history-zip-symlink-Abraxas-Labs.py
ou
cd lab
./run.sh
Gadget du dump zip rolling de EventLogFileReaders.scala, pas un History Server complet et pas l'image officielle spark:4.2.0. Il ne parse pas le JSON Event via Jackson. Loopback 127.0.0.1:18980. Projet Compose spark-history-zip-symlink. Le répertoire rolling contient un appstatus_* + events_* valide (sans témoin) plus planted-secret -> /secret/host-secret.
SUCCESS SPARK-HISTORY-ZIP-SYMLINK SPARK-HISTORY-ZIP-SYMLINK-WITNESS
N'empaqueter que les enfants events_* / appstatus_*, pas chaque nom de listStatus. Ouvrir sans suivi (O_NOFOLLOW / options Hadoop no-symlink) et rejeter un enfant dont le realpath est hors de spark.history.fs.logDirectory. Un helper de nom d'affichage n'est pas une vérification de realpath.