
Apache Spark 4.2.0 — высокий уровень — удалённо — чтение файла — History zip следует по подложенной символической ссылке
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-zip-symlink
Класс: чтение файлов Достижимость: удалённая
Apache Spark 4.2.0 (32f72996) — Apache Software Foundation
CVE-2025-54920 закрыл Jackson в History Server, поэтому sparkEventFromJson теперь создаёт только подклассы SparkListenerEvent. При этом zipEventLogFiles по-прежнему выгружает каждого потомка из listStatus через FileSystem.open. LocalFileSystem следует по POSIX-симлинку. Подложенный потомок в eventlog_v2_* упаковывается в неаутентифицированный GET /api/v1/applications/{id}/logs. Тело элемента zip — это цель SHS-UID. Имя записи — это basename симлинка.
Это не повторная отправка Jackson RCE. Jackson остаётся закрытым. Это не RCE на хосте. Целостность и доступность не затронуты.
| ID | CVE пока нет |
| Класс | чтение файлов (остаток от rolling zip в модели жертвы CVE-2025-54920) |
| Достижимость | удалённая (запись в каталог логов History плюс неаутентифицированный GET zip) |
| CWE | CWE-59, CWE-22 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Продукт | Apache Spark |
| Затронуто | тег 4.2.0 v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Аутентификация | PR:L = запись в spark.history.fs.logDirectory (та же жертва, что и в 54920). ACL History UI по умолчанию отключены. |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1:18980 |
Тот, кто может писать в spark.history.fs.logDirectory (по умолчанию file:/tmp/spark-events), может подложить rolling-каталог, который History Server заархивирует. Rolling-писатели по умолчанию включены. ACL History UI по умолчанию отключены. UID процесса SHS — это confused deputy.
| Привилегия | Метод / sink | Доказано в лаборатории или выведено |
|---|---|---|
| Конфиденциальность: чтение файла хоста, который может открыть UID SHS | Подложить eventlog_v2_<id>/ с валидными appstatus_* и events_* JSON (SparkListenerLogStart / SparkListenerApplicationStart) плюс потомок-симлинк, цель которого находится вне каталога логов. Дождаться сканирования. Неаутентифицированный GET /api/v1/applications/{id}/logs (OneApplicationResource.getEventLogs -> FsHistoryProvider.writeEventLogs -> RollingEventLogFilesFileReader.zipEventLogFiles -> FileSystem.open) | Доказано в лаборатории. Тело элемента zip eventlog_v2_app-2026-0001/planted-secret — это SPARK-HISTORY-ZIP-SYMLINK-WITNESS. В элементе events свидетеля нет. Имя записи остаётся basename симлинка, а не путём цели. |
Конфиденциальность: лог событий соседнего тенанта, который атакующий не может open (660) | Тот же zip, цель симлинка — лог другого приложения | Выведено. Тот же FileSystem.open. Не проверялось. |
Конфиденциальность: keytab, spark-env.sh, /etc/passwd, если SHS может их прочитать | Тот же zip | Выведено. Оракулом служит выделенный /secret/host-secret, а не эти файлы. |
| Целостность файлов SHS или хоста | zip читается в тело HTTP-ответа | Закрыто. Доказано в лаборатории отсутствие. |
| Доступность файлов SHS или хоста | zip не удаляет и не усекает цель | Закрыто. Доказано в лаборатории отсутствие. |
| RCE на хосте / конструирование Jackson-гаджета | JsonProtocol.sparkEventFromJson | Закрыто. Требуется SparkListenerEvent. Эта лаборатория подкладывает только безобидный Event JSON. |
Zip slip с ../ в имени записи | Path.getName() | Закрыто. POSIX-имя файла не может содержать /. Запись — eventlog_v2_<id>/<basename>. |
Выход по строке пути из resolveLogPath | new Path(logDir, attempt.logPath) | Закрыто. Объект Path остаётся внутри настроенного каталога логов. Выход происходит через inode. |
| Перезапись при компакции / удаление цели cleaner'ом | spark.history.fs.eventLog.rolling.maxFilesToRetain по умолчанию Integer.MAX_VALUE; cleaner по умолчанию отключён | Закрыто. Локальный fs.delete симлинка удаляет ссылку, а не цель. |
| Неаутентифицированная отправка заданий / загрузка ML-модели | SECURITY.md FAQ / docs/ml-security.md | Закрыто. По замыслу, не этот остаток. |
Я зафиксировал apache/spark v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8), а не master или 4.3 RC. Двадцать восемь поисков. Jackson History RCE закрыт на этой ревизии (JsonProtocol.scala:965-972). History Server по-прежнему указывал на доступный для записи атакующим каталог логов, что представляет ту же модель жертвы, что и CVE-2025-54920.
zipEventLogFiles архивирует все files, а не только файлы событий:
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, затем transferTo. LocalFileSystem следует по POSIX-симлинкам. getLogDirInfo комментирует «handles symbolic links properly», а затем обходит неразрешённые родительские Path для отображаемого имени.
Уже зафиксированные неверные пути: трактовка SPARK-52381 как всей истории History; размещение свидетеля в events_* (лаборатория на этом падает); ожидание, что запись zip будет содержать путь цели; трактовка префиксного соединения в resolveLogPath как выхода; попытка использовать класс события HiveConnection на этой ревизии.
python3 spark-history-zip-symlink-Abraxas-Labs.py
или
cd lab
./run.sh
Гаджет выгрузки rolling zip из EventLogFileReaders.scala, а не полный History Server и не официальный образ spark:4.2.0. Он не разбирает Event JSON через Jackson. Loopback 127.0.0.1:18980. Проект Compose spark-history-zip-symlink. Rolling-каталог содержит валидные appstatus_* + events_* (без свидетеля) плюс planted-secret -> /secret/host-secret.
SUCCESS SPARK-HISTORY-ZIP-SYMLINK SPARK-HISTORY-ZIP-SYMLINK-WITNESS
Упаковывать только потомков events_* / appstatus_*, а не каждое имя из listStatus. Открывать без следования (O_NOFOLLOW / опции Hadoop no-symlink) и отклонять потомка, realpath которого находится вне spark.history.fs.logDirectory. Помощник для отображаемого имени — это не проверка realpath.