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
Classe: Escrita de arquivo (controlada por configuração) Alcance: Remoto
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
HistoryServerDiskManager.appStorePath faz 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. 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 | sem CVE ainda |
| Classe | Escrita de arquivo (store em disco do History controlado 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 do join citado. Witness .rdb fora de store.path. |
Mesmo modelo de vítima que CVE-2025-54920: History Server apontado para um diretório de log 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 event-JSON.
| Método | Sink | Privilégio | Lab vs inferido |
|---|---|---|---|
Plantar um event log cujo App ID no JSON seja ../<name> (ou ../../oracle/...). Aguardar o scan. GET que carrega a UI do app / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath e então Lease.commit renameTo | Integridade: criar um caminho .rdb (RocksDB por padrão) ou .ldb (LevelDB) fora de {store.path}/apps como o UID do History. Comprovado em lab no gadget: escaped=yes, o arquivo /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb contém o witness. O conteúdo é um arquivo de store, não bytecode de host escolhido pelo atacante, não um shell. | Comprovado em lab (gravação do gadget dos bytes do witness 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 então Utils.deleteRecursively | Disponibilidade: excluir um diretório existente cujo nome já termina com o sufixo do backend, e então substituí-lo pelo store arrendado. | Inferido (fixar HistoryServerDiskManager.scala commit). O gadget gravou um arquivo, não chamou deleteRecursively. |
Padrão não definido de spark.history.store.path | 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 de nome de 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 |
Adjacentes fechados: 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 da listagem não é um caminho de filesystem. O mapper HybridStore / kvstore não tem Id.CLASS. O HTTP do History 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.
Fiz pin do Spark 4.2.0, não do master, não de um RC 4.3. Vinte e oito caçadas. O RCE público do Jackson no History está fechado nesta peel. Escritores de event-log já chamam Utils.sanitizeDirName, então o arquivo de log não pode conter /. O nome do arquivo do 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") resolve fora do parent. JsonProtocol.applicationStartFromJson pega 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 padrão não definido de store.path como uma escrita (é em memória); esperar que new File(dir, "/abs") no Unix ignore o parent (não ignora; o resíduo é ..); afirmar RCE porque o sufixo é .rdb; inicializar um History Server completo quando o join é o oráculo; tratar sanitizeDirName como cobrindo este caminho.
python3 spark-history-store-path-Abraxas-Labs.py
ou
cd lab
./run.sh
Gadget de HistoryServerDiskManager.appStorePath em 127.0.0.1:18990. Trechos do pin 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(). O spark.history.store.path não definido permanece o padrão seguro.