
Apache Spark 4.2.0 - Alta - Remoto - Leitura de arquivo - History zip segue symlink plantado
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-zip-symlink
Classe: Leitura de arquivo Alcance: Remoto
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
O CVE-2025-54920 travou o Jackson do History Server para que sparkEventFromJson apenas construísse subclasses de SparkListenerEvent. O rolling zipEventLogFiles ainda despeja todo filho de listStatus via FileSystem.open. O LocalFileSystem segue um symlink POSIX. Um filho plantado em eventlog_v2_* é empacotado no GET /api/v1/applications/{id}/logs sem autenticação. O corpo do membro do zip é o alvo do SHS-UID. O nome da entrada é o basename do symlink.
Isto não é um re-registro do RCE do Jackson. O Jackson permanece fechado. Isto não é RCE no host. Integridade e disponibilidade são nulas.
| ID | ainda sem CVE |
| Classe | Leitura de arquivo (resíduo do zip rolling do modelo de vítima do CVE-2025-54920) |
| Alcance | Remoto (escritor do diretório de log do History mais GET não autenticado do zip) |
| CWE | CWE-59, CWE-22 |
| CVSS | Alto: 7.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Produto | Apache Spark |
| Afetado | 4.2.0 tag v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Autenticação | PR:L = escrita em spark.history.fs.logDirectory (mesma vítima do 54920). ACLs da UI do History desativadas por padrão. |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1:18980 |
Alguém que consiga escrever em spark.history.fs.logDirectory (padrão file:/tmp/spark-events) pode plantar um diretório rolling que o History Server irá zipar. Os escritores rolling estão ativados por padrão. As ACLs da UI do History estão desativadas por padrão. O UID do processo SHS é o deputy confundido.
| Privilégio | Método / sink | Comprovado em lab ou inferido |
|---|---|---|
| Confidencialidade: ler um arquivo do host que o UID do SHS consegue abrir | Plantar eventlog_v2_<id>/ com appstatus_* válido mais JSON events_* (SparkListenerLogStart / SparkListenerApplicationStart) mais um filho symlink cujo alvo fica fora do diretório de log. Aguardar o scan. GET /api/v1/applications/{id}/logs sem autenticação (OneApplicationResource.getEventLogs -> FsHistoryProvider.writeEventLogs -> RollingEventLogFilesFileReader.zipEventLogFiles -> FileSystem.open) | Comprovado em lab. O corpo do membro do zip eventlog_v2_app-2026-0001/planted-secret é SPARK-HISTORY-ZIP-SYMLINK-WITNESS. O membro de events não tem witness. O nome da entrada permanece o basename do symlink, não o caminho do alvo. |
Confidencialidade: log de eventos de tenant irmão que o atacante não consegue open (660) | Mesmo zip, alvo do symlink é o log de outro app | Inferido. Mesmo FileSystem.open. Não exercitado. |
Confidencialidade: keytab, spark-env.sh, /etc/passwd se o SHS conseguir lê-los | Mesmo zip | Inferido. O oráculo é um /secret/host-secret dedicado, não esses arquivos. |
| Integridade de arquivos do SHS ou do host | o zip é lido para o corpo HTTP | Fechado. Comprovado em lab como nulo. |
| Disponibilidade de arquivos do SHS ou do host | o zip não apaga nem trunca o alvo | Fechado. Comprovado em lab como nulo. |
| RCE no host / construção de gadget Jackson | JsonProtocol.sparkEventFromJson | Fechado. SparkListenerEvent obrigatório. Este lab planta apenas JSON de Event benigno. |
Zip slip de ../ no nome da entrada | Path.getName() | Fechado. Nome de arquivo POSIX não pode conter /. A entrada é eventlog_v2_<id>/<basename>. |
Escape de string de caminho de resolveLogPath | new Path(logDir, attempt.logPath) | Fechado. O objeto Path permanece sob o diretório de log configurado. O escape é o inode. |
| Reescrita de compactação / exclusão do alvo pelo cleaner | spark.history.fs.eventLog.rolling.maxFilesToRetain padrão Integer.MAX_VALUE; cleaner desativado por padrão | Fechado. O fs.delete local de um symlink desvincula o link, não o alvo. |
| Submissão de job sem autenticação / carregamento de modelo ML | FAQ do SECURITY.md / docs/ml-security.md | Fechado. Por design, não este resíduo. |
Fixe em apache/spark v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) em vez de master ou um RC 4.3. Vinte e oito caçadas. O RCE do Jackson no History está fechado nesta revisão (JsonProtocol.scala:965-972). O History Server ainda apontava para um diretório de log gravável pelo atacante, que é o mesmo modelo de vítima do CVE-2025-54920.
zipEventLogFiles zipa todos os files, não apenas arquivos de evento:
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 é fileSystem.open e depois transferTo. O LocalFileSystem segue symlinks POSIX. getLogDirInfo comenta "handles symbolic links properly" e então percorre pais de Path não resolvidos para um nome de exibição.
Caminhos errados já registrados: tratar SPARK-52381 como toda a história do History; plantar o witness em events_* (o lab falha nisso); esperar que a entrada do zip carregue o caminho do alvo; tratar o prefix-join de resolveLogPath como o escape; tentar uma classe Event de HiveConnection nesta revisão.
python3 spark-history-zip-symlink-Abraxas-Labs.py
ou
cd lab
./run.sh
Gadget do dump do zip rolling de EventLogFileReaders.scala, não um History Server completo e não a imagem oficial spark:4.2.0. Ele não faz parsing de Event JSON através do Jackson. Loopback 127.0.0.1:18980. Projeto Compose spark-history-zip-symlink. O diretório rolling contém appstatus_* + events_* válidos (sem witness) mais planted-secret -> /secret/host-secret.
SUCCESS SPARK-HISTORY-ZIP-SYMLINK SPARK-HISTORY-ZIP-SYMLINK-WITNESS
Empacotar apenas filhos events_* / appstatus_*, não todo nome de listStatus. Abrir com no-follow (O_NOFOLLOW / opções no-symlink do Hadoop) e rejeitar um filho cujo realpath esteja fora de spark.history.fs.logDirectory. Um helper de nome de exibição não é uma verificação de realpath.