Apache Spark 4.2.0 - Alto - Remoto - Scrittura di file - History appStorePath App ID non sanificato
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-store-path
Write-up: https://abraxaslabs.tech/research/spark-history-store-path
Classe: Scrittura di file (condizionata dalla configurazione) Portata: Remota
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
HistoryServerDiskManager.appStorePath esegue new File(appStoreDir, appId + extension) senza alcuna sanitizzazione. Utils.sanitizeDirName è usato solo per i nomi dei file di event-log. L'App ID nel JSON viene letto con asText(). L'elenco memorizza event.appId.orNull. Lease.commit usa quella stringa. Un App ID ../ scrive un .rdb al di fuori di spark.history.store.path. Il valore predefinito non impostato di store.path è in memoria (nessuna scrittura). Il contenuto è un file di store, non RCE.
| ID | nessuna CVE al momento |
| Classe | Scrittura di file (disk store della History condizionato dalla configurazione) |
| Portata | Remota (scrittore dell'event-log più History con spark.history.store.path impostato; ACL della UI disattivate per impostazione predefinita) |
| CWE | CWE-22 |
| CVSS | Alto: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N quando spark.history.store.path è impostato |
| Prodotto | Apache Spark History Server |
| Affetto | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Auth | scrittore di spark.history.fs.logDirectory (predefinito file:/tmp/spark-events); la History deve avere spark.history.store.path impostato |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1:18990. Gadget del join citato. Witness .rdb al di fuori di store.path. |
Stesso modello di vittima di CVE-2025-54920: History Server puntato a una directory di log in cui l'attaccante può scrivere. Jackson non costruisce più gadget della classe HiveConnection. Il percorso del disk store continua comunque a fidarsi dell'App ID nel JSON dell'evento.
| Metodo | Sink | Privilegio | Lab vs dedotto |
|---|---|---|---|
Impiantare un event log il cui App ID JSON è ../<name> (o ../../oracle/...). Attendere la scansione. GET che carica la UI dell'app / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath poi Lease.commit renameTo | Integrità: creare un percorso .rdb (RocksDB predefinito) o .ldb (LevelDB) al di fuori di {store.path}/apps come History UID. Dimostrato in laboratorio sul gadget: escaped=yes, il file /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb contiene il witness. Il contenuto è un file di store, non bytecode host scelto dall'attaccante, non una shell. | Dimostrato in laboratorio (scrittura del witness da parte del gadget al percorso evaso). Il renameTo completo di History di una directory RocksDB è lo stesso join. |
| Stesso App ID quando la destinazione evasa è già una directory | Lease.commit if (dst.isDirectory()) deleteStore(dst) poi Utils.deleteRecursively | Disponibilità: eliminare una directory esistente il cui nome termina già con il suffisso del backend, poi sostituirla con lo store in lease. | Dedotto (pin di HistoryServerDiskManager.scala commit). Il gadget ha scritto un file, non ha chiamato deleteRecursively. |
spark.history.store.path predefinito non impostato | LOCAL_STORE_DIR createOptional; diskManager = storePath.map | Chiuso. Elenco in memoria. Nessun HistoryServerDiskManager. Dimostrato in laboratorio gated=yes written=no. | Dimostrato in laboratorio |
Applicare Utils.sanitizeDirName all'App ID prima | helper del nome file dell'event log | Rimane dentro apps/. / e : diventano -. Negativo dimostrato in laboratorio. | Dimostrato in laboratorio |
Controllare l'App ID local-control con store.path impostato | stesso join | Scrive {store.path}/apps/local-control.rdb. Rimane dentro apps/. | Dimostrato in laboratorio |
Adiacenti chiusi: store predefinito non impostato (in memoria). Jackson sparkEventFromJson richiede comunque SparkListenerEvent (CVE-2025-54920). Il fallback URL getAppUI eventlog_v2_ + nameForAppAndAttempt è sanitizzato. La chiave KV dell'elenco non è un percorso filesystem. HybridStore / kvstore mapper non ha Id.CLASS. History HTTP non accetta un percorso di upload. Compaction disattivata per impostazione predefinita. Cleaner disattivato per impostazione predefinita. Invio di job su un cluster aperto (SECURITY.md FAQ). Caricamento di modelli ML (docs/ml-security.md). Questo residuo non è RCE sull'host.
Ho fissato Spark 4.2.0, non master, non una 4.3 RC. Ventotto ricerche. La RCE pubblica di History Jackson è chiusa su questo livello. I writer degli event log chiamano già Utils.sanitizeDirName quindi il file di log non può contenere /. Il nome del file della cache su disco usa l'App ID grezzo.
private[history] def appStorePath(appId: String, attemptId: Option[String]): File = {
val fileName = appId + attemptId.map("_" + _).getOrElse("") + extension
new File(appStoreDir, fileName)
}
appStoreDir è {spark.history.store.path}/apps. Su Unix new File(parent, "../../oracle/name.rdb") viene risolto al di fuori della directory padre. JsonProtocol.applicationStartFromJson esegue json.get("App ID").asText(). AppListingListener.onApplicationStart memorizza event.appId.orNull. lease.commit(appId, attempt.info.attemptId) passa quella stringa a appStorePath.
Vicoli ciechi già registrati: considerare la scrittura quando store.path non è impostato e assume il valore predefinito (è in memoria); aspettarsi che su Unix new File(dir, "/abs") ignori la directory padre (non lo fa; ciò che resta è ..); sostenere l'RCE perché il suffisso è .rdb; avviare un History Server completo quando il join è l'oracle; considerare sanitizeDirName come copertura di questo percorso.
python3 spark-history-store-path-Abraxas-Labs.py
o
cd lab
./run.sh
Gadget di HistoryServerDiskManager.appStorePath su 127.0.0.1:18990. Estratti Pin sotto lab/pin/. Il server History completo non è avviato. Associalo al 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
Esegui Utils.sanitizeDirName (oppure rifiuta .. / / / NUL) su appId e attemptId all'interno di appStorePath prima di new File. Dopo il join, richiedi che dst.getCanonicalFile() rimanga sotto appStoreDir.getCanonicalFile(). Lasciare spark.history.store.path non impostato resta il default sicuro.