Apache Spark 4.2.0 - Alta - Remoto - Gravação de arquivo - History appStorePath App ID não sanitizado
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-store-path
Write-up: https://abraxaslabs.tech/research/spark-history-store-path
Classe: Escrita de arquivo (condicionada por configuração) Alcance: Remoto
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
HistoryServerDiskManager.appStorePath executa new File(appStoreDir, appId + extension) sem sanitização. Utils.sanitizeDirName serve apenas para nomes de arquivo de event-log. O App ID do JSON é asText(). A listagem armazena event.appId.orNull. Lease.commit usa essa string. O App ID ../ grava um .rdb fora de spark.history.store.path. O padrão não definido de store.path é em memória (sem escrita). O conteúdo é um arquivo de store, não RCE.
| ID | ainda sem CVE |
| Classe | Escrita de arquivo (store em disco do History condicionado por configuração) |
| Alcance | Remoto (escritor de event-log mais History com spark.history.store.path definido; ACLs da UI desativadas por padrão) |
| 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 está definido |
| Produto | Apache Spark History Server |
| Afetado | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Autenticação | escritor de spark.history.fs.logDirectory (padrão file:/tmp/spark-events); o History deve ter spark.history.store.path definido |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1:18990. Gadget da junção citada. Testemunha .rdb fora de store.path. |
Mesmo modelo de vítima do CVE-2025-54920: History Server apontado para um diretório de logs no qual o atacante pode escrever. O Jackson não constrói mais gadgets da classe HiveConnection. O caminho do disk-store ainda confia no App ID do JSON de eventos.
| Método | Sink | Privilégio | Lab vs inferido |
|---|---|---|---|
Plantar um event log cujo JSON App ID é ../<name> (ou ../../oracle/...). Aguardar o scan. GET que carrega a UI do app / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath e depois Lease.commit renameTo | Integridade: criar um caminho .rdb (RocksDB por padrão) ou .ldb (LevelDB) fora de {store.path}/apps como o History UID. Comprovado em lab no gadget: escaped=yes, o arquivo /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb contém a testemunha. O conteúdo é um arquivo de store, não bytecode de host escolhido pelo atacante, não um shell. | Comprovado em lab (gravação de bytes de testemunha pelo gadget no caminho escapado). O renameTo completo do History de um diretório RocksDB é o mesmo join. |
| Mesmo App ID quando o destino escapado já é um diretório | Lease.commit if (dst.isDirectory()) deleteStore(dst) e depois Utils.deleteRecursively | Disponibilidade: excluir um diretório existente cujo nome já termina no sufixo do backend e, em seguida, substituí-lo pelo store arrendado. | Inferido (fixar HistoryServerDiskManager.scala commit). O gadget gravou um arquivo, não chamou deleteRecursively. |
spark.history.store.path padrão não definido | LOCAL_STORE_DIR createOptional; diskManager = storePath.map | Fechado. Listagem em memória. Sem HistoryServerDiskManager. Comprovado em lab gated=yes written=no. | Comprovado em lab |
Aplicar Utils.sanitizeDirName ao App ID primeiro | helper do nome do arquivo de event-log | Permanece dentro de apps/. / e : tornam-se -. Comprovado em lab negativo. | Comprovado em lab |
Controlar App ID local-control com store.path definido | mesmo join | Grava {store.path}/apps/local-control.rdb. Permanece dentro de apps/. | Comprovado em lab |
Fechados adjacentes: store padrão não definido (em memória). Jackson sparkEventFromJson ainda exige SparkListenerEvent (CVE-2025-54920). O fallback de URL getAppUI eventlog_v2_ + nameForAppAndAttempt é sanitizado. A chave KV de listagem não é um caminho de filesystem. O mapper HybridStore / kvstore não tem Id.CLASS. O History HTTP não aceita um caminho de upload. Compactação desativada por padrão. Cleaner desativado por padrão. Submissão de job em um cluster aberto (SECURITY.md FAQ). Carregamento de modelo ML (docs/ml-security.md). Este resíduo não é RCE de host.
Eu fixei o Spark 4.2.0, não o master, não um RC 4.3. Vinte e oito caçadas. O RCE público do History Jackson está fechado nesta camada. Os escritores de event-log já chamam Utils.sanitizeDirName, então o arquivo de log não pode conter /. O nome do arquivo de disk-cache usa o App ID bruto.
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. No Unix, new File(parent, "../../oracle/name.rdb") é resolvido fora do parent. JsonProtocol.applicationStartFromJson usa json.get("App ID").asText(). AppListingListener.onApplicationStart armazena event.appId.orNull. lease.commit(appId, attempt.info.attemptId) alimenta essa string para appStorePath.
Caminhos errados já registrados: tratar o store.path padrão não definido como uma escrita (ele está em memória); esperar que no Unix new File(dir, "/abs") ignore o parent (não ignora; o que sobra é ..); alegar RCE porque o sufixo é .rdb; inicializar um History Server completo quando o join é o oracle; tratar sanitizeDirName como se cobrisse esse caminho.
python3 spark-history-store-path-Abraxas-Labs.py
ou
cd lab
./run.sh
Gadget de HistoryServerDiskManager.appStorePath em 127.0.0.1:18990. Fixe os trechos em lab/pin/. O History Server completo não é inicializado. Vincule-o ao 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
Execute Utils.sanitizeDirName (ou rejeite .. / / / NUL) em appId e attemptId dentro de appStorePath antes de new File. Após o join, exija que dst.getCanonicalFile() permaneça sob appStoreDir.getCanonicalFile(). spark.history.store.path não definido continua sendo o padrão seguro.