Apache Spark 4.2.0 — высокий уровень — удалённо — запись файла — History appStorePath несанитизированный App ID
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-store-path
Разбор: https://abraxaslabs.tech/research/spark-history-store-path
Класс: запись файлов (зависит от конфигурации) Достижимость: удалённая
Apache Spark 4.2.0 (32f72996) — Apache Software Foundation
HistoryServerDiskManager.appStorePath выполняет new File(appStoreDir, appId + extension) без какой-либо очистки. Utils.sanitizeDirName применяется только к именам файлов журнала событий. JSON-поле App ID обрабатывается через asText(). При листинге сохраняется event.appId.orNull. Lease.commit использует эту строку. App ID ../ записывает .rdb за пределами spark.history.store.path. По умолчанию незаданный store.path хранится в памяти (без записи). Содержимое представляет собой файл хранилища, а не RCE.
| ID | CVE пока нет |
| Класс | Запись файлов (дисковое хранилище History, зависит от конфигурации) |
| Достижимость | Удалённая (запись журнала событий плюс History с заданным spark.history.store.path; ACL интерфейса по умолчанию отключены) |
| CWE | CWE-22 |
| CVSS | Высокий: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N при заданном spark.history.store.path |
| Продукт | Apache Spark History Server |
| Затронуто | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Аутентификация | запись в spark.history.fs.logDirectory (по умолчанию file:/tmp/spark-events); в History должен быть задан spark.history.store.path |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1:18990. Гадже́т упомянутого объединения. Свидетель .rdb вне store.path. |
Та же модель жертвы, что и в CVE-2025-54920: History Server указывает на каталог журналов, в который атакующий может писать. Jackson больше не создаёт гаджеты класса HiveConnection. Путь дискового хранилища по-прежнему доверяет App ID из JSON событий.
| Метод | Сток | Привилегия | Лаборатория vs вывод |
|---|---|---|---|
Подложить журнал событий, чей JSON App ID равен ../<name> (или ../../oracle/...). Дождаться сканирования. GET, который загружает UI приложения / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath, затем Lease.commit renameTo | Целостность: создать путь .rdb (по умолчанию RocksDB) или .ldb (LevelDB) вне {store.path}/apps под UID History. Доказано в лаборатории на гаджете: escaped=yes, файл /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb содержит свидетельство. Содержимое — это файл хранилища, а не выбранный атакующим байткод хоста, не shell. | Доказано в лаборатории (запись байтов-свидетелей гаджетом по экранированному пути). Полный renameTo History для каталога RocksDB — тот же join. |
| Тот же App ID, когда экранированный пункт назначения уже является каталогом | Lease.commit if (dst.isDirectory()) deleteStore(dst), затем Utils.deleteRecursively | Доступность: удалить существующий каталог, имя которого уже заканчивается на суффикс бэкенда, затем заменить его арендованным хранилищем. | Выведено (закреплено в коммите HistoryServerDiskManager.scala). Гаджет записал файл, но не вызывал deleteRecursively. |
По умолчанию неустановленный spark.history.store.path | LOCAL_STORE_DIR createOptional; diskManager = storePath.map | Закрыто. Листинг в памяти. Нет HistoryServerDiskManager. Доказано в лаборатории gated=yes written=no. | Доказано в лаборатории |
Применить Utils.sanitizeDirName к App ID заранее | помощник имени файла журнала событий | Остаётся внутри apps/. / и : становятся -. Доказано в лаборатории как отрицательный результат. | Доказано в лаборатории |
Управляющий App ID local-control при установленном store.path | тот же join | Записывает {store.path}/apps/local-control.rdb. Остаётся внутри apps/. | Доказано в лаборатории |
Смежное закрыто: неустановленное хранилище по умолчанию (в памяти). Jackson sparkEventFromJson по-прежнему требует SparkListenerEvent (CVE-2025-54920). Резервный путь URL getAppUI eventlog_v2_ + nameForAppAndAttempt санитизируется. Ключ KV листинга не является путём файловой системы. У маппера HybridStore / kvstore нет Id.CLASS. History HTTP не принимает путь загрузки. Компакция по умолчанию выключена. Очистка по умолчанию выключена. Отправка заданий в открытом кластере (SECURITY.md FAQ). Загрузка ML-модели (docs/ml-security.md). Этот остаток не является RCE хоста.
Я закрепил Spark 4.2.0, не master и не 4.3 RC. Двадцать восемь поисков. Публичный History Jackson RCE закрыт на этой итерации. Писатели журналов событий уже вызывают Utils.sanitizeDirName, поэтому файл журнала не может содержать /. Имя файла дискового кэша использует необработанный App ID.
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. В Unix new File(parent, "../../oracle/name.rdb") разрешается за пределы родительского каталога. JsonProtocol.applicationStartFromJson вызывает json.get("App ID").asText(). AppListingListener.onApplicationStart сохраняет event.appId.orNull. lease.commit(appId, attempt.info.attemptId) передаёт эту строку в appStorePath.
Уже зафиксированные ошибочные пути: трактовка неустановленного по умолчанию store.path как записи (он находится в памяти); ожидание, что Unix new File(dir, "/abs") проигнорирует родительский каталог (это не так; остаётся ..); утверждение о RCE из-за суффикса .rdb; запуск полного History Server, когда соединение и есть oracle; трактовка sanitizeDirName как покрывающего этот путь.
python3 spark-history-store-path-Abraxas-Labs.py
или
cd lab
./run.sh
Гаджет HistoryServerDiskManager.appStorePath на 127.0.0.1:18990. Закрепите фрагменты в lab/pin/. Полный History Server не загружается. Привяжите его к 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
Запустите Utils.sanitizeDirName (или отклоните .. / / / NUL) для appId и attemptId внутри appStorePath перед new File. После объединения потребуйте, чтобы dst.getCanonicalFile() оставался внутри appStoreDir.getCanonicalFile(). Неустановленный spark.history.store.path остаётся безопасным значением по умолчанию.