
Apache Spark 4.2.0 - Élevé - À distance - Écriture de fichier - History appStorePath App ID non assaini
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-store-path
Classe : Écriture de fichier (conditionnée par la configuration) Portée : Distante
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
HistoryServerDiskManager.appStorePath effectue new File(appStoreDir, appId + extension) sans assainissement. Utils.sanitizeDirName ne concerne que les noms de fichiers du journal d'événements. Le champ JSON App ID est traité via asText(). Le listing stocke event.appId.orNull. Lease.commit utilise cette chaîne. Un App ID ../ écrit un .rdb en dehors de spark.history.store.path. Par défaut, store.path non défini est en mémoire (aucune écriture). Le contenu est un fichier de stockage, pas une RCE.
| ID | pas encore de CVE |
| Classe | Écriture de fichier (stockage disque History conditionné par la configuration) |
| Portée | Distante (écrivain du journal d'événements plus History avec spark.history.store.path défini ; ACLs de l'UI désactivées par défaut) |
| CWE | CWE-22 |
| CVSS | Élevé : 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N lorsque spark.history.store.path est défini |
| Produit | Apache Spark History Server |
| Affecté | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Auth | écrivain de spark.history.fs.logDirectory (par défaut file:/tmp/spark-events) ; History doit avoir spark.history.store.path défini |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18990 uniquement. Gadget de la jointure citée. Témoin .rdb en dehors de store.path. |
Même modèle de victime que CVE-2025-54920 : History Server pointé vers un répertoire de journaux dans lequel l'attaquant peut écrire. Jackson ne construit plus de gadgets de classe HiveConnection. Le chemin du stockage disque fait toujours confiance à l'App ID du JSON d'événement.
| Méthode | Point de chute | Privilège | Lab vs déduit |
|---|---|---|---|
Placer un journal d'événements dont le JSON App ID est ../<name> (ou ../../oracle/...). Attendre le scan. GET qui charge l'UI de l'app / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath puis Lease.commit renameTo | Intégrité : créer un chemin .rdb (RocksDB par défaut) ou .ldb (LevelDB) en dehors de {store.path}/apps en tant qu'UID History. Prouvé en lab sur le gadget : escaped=yes, le fichier /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb contient le témoin. Le contenu est un fichier de stockage, pas du bytecode hôte choisi par l'attaquant, pas un shell. | Prouvé en lab (écriture gadget des octets témoins au chemin échappé). Le renameTo complet de History d'un répertoire RocksDB est la même jointure. |
| Même App ID lorsque la destination échappée est déjà un répertoire | Lease.commit if (dst.isDirectory()) deleteStore(dst) puis Utils.deleteRecursively | Disponibilité : supprimer un répertoire existant dont le nom se termine déjà par le suffixe du backend, puis le remplacer par le stockage loué. | Déduit (épingler le commit de HistoryServerDiskManager.scala). Le gadget a écrit un fichier, n'a pas appelé deleteRecursively. |
spark.history.store.path non défini par défaut | LOCAL_STORE_DIR createOptional ; diskManager = storePath.map | Fermé. Listing en mémoire. Pas de HistoryServerDiskManager. Prouvé en lab gated=yes written=no. | Prouvé en lab |
Appliquer Utils.sanitizeDirName à l'App ID d'abord | helper de nom de fichier du journal d'événements | Reste dans apps/. / et : deviennent -. Négatif prouvé en lab. | Prouvé en lab |
Contrôler l'App ID local-control avec store.path défini | même jointure | Écrit {store.path}/apps/local-control.rdb. Reste dans apps/. | Prouvé en lab |
Fermé adjacent : store non défini par défaut (en mémoire). Jackson sparkEventFromJson requiert toujours SparkListenerEvent (CVE-2025-54920). Le fallback URL getAppUI eventlog_v2_ + nameForAppAndAttempt est assaini. La clé KV du listing n'est pas un chemin de système de fichiers. Le mapper HybridStore / kvstore n'a pas de Id.CLASS. History HTTP n'accepte pas de chemin d'upload. Compaction désactivée par défaut. Cleaner désactivé par défaut. Soumission de job sur un cluster ouvert (FAQ SECURITY.md). Chargement de modèle ML (docs/ml-security.md). Ce reliquat n'est pas une RCE hôte.
J'ai épinglé Spark 4.2.0, pas master, pas une RC 4.3. Vingt-huit chasses. La RCE Jackson publique de History est fermée sur cette version. Les écrivains de journaux d'événements appellent déjà Utils.sanitizeDirName donc le fichier journal ne peut pas contenir /. Le nom du fichier de cache disque utilise l'App ID brut.
private[history] def appStorePath(appId: String, attemptId: Option[String]): File = {
val fileName = appId + attemptId.map("_" + _).getOrElse("") + extension
new File(appStoreDir, fileName)
}
appStoreDir est {spark.history.store.path}/apps. Sous Unix, new File(parent, "../../oracle/name.rdb") se résout en dehors du parent. JsonProtocol.applicationStartFromJson prend json.get("App ID").asText(). AppListingListener.onApplicationStart stocke event.appId.orNull. lease.commit(appId, attempt.info.attemptId) alimente cette chaîne vers appStorePath.
Fausses pistes déjà consignées : traiter store.path non défini par défaut comme une écriture (il est en mémoire) ; s'attendre à ce que new File(dir, "/abs") sous Unix ignore le parent (ce n'est pas le cas ; le reliquat est ..) ; prétendre à une RCE parce que le suffixe est .rdb ; démarrer un History Server complet alors que la jointure est l'oracle ; considérer que sanitizeDirName couvre ce chemin.
python3 spark-history-store-path-Abraxas-Labs.py
ou
cd lab
./run.sh
Gadget de HistoryServerDiskManager.appStorePath sur 127.0.0.1:18990. Extraits épinglés sous lab/pin/. Le History Server complet n'est pas démarré. Le lier à loopback.
SUCCESS SPARK-HISTORY-STORE-PATH how=gadget escaped=yes gated=yes outside=/work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb SPARK-HISTORY-STORE-PATH-WITNESS
Exécuter Utils.sanitizeDirName (ou rejeter .. / / / NUL) sur appId et attemptId à l'intérieur de appStorePath avant new File. Après la jointure, exiger que dst.getCanonicalFile() reste sous appStoreDir.getCanonicalFile(). spark.history.store.path non défini reste la valeur par défaut sûre.