
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 に ../ を指定すると、spark.history.store.path の外側に .rdb が書き込まれます。デフォルトでは未設定の store.path はインメモリ (書き込みなし) です。内容はストアファイルであり、RCE ではありません。
| ID | CVE 未割り当て |
| 分類 | ファイル書き込み (設定により条件付きの History ディスクストア) |
| 到達範囲 | リモート (イベントログの書き込み者、および spark.history.store.path が設定された History。UI ACL はデフォルトで無効) |
| CWE | CWE-22 |
| CVSS | High: 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 のみ。引用された結合のガジェット。store.path の外側に .rdb を証拠として生成。 |
被害者のモデルは CVE-2025-54920 と同じです。History Server が、攻撃者が書き込み可能なログディレクトリを指している状態です。Jackson はもはや HiveConnection クラスのガジェットを構築しません。しかしディスクストアのパスは依然としてイベント JSON の App ID を信頼しています。
| 手法 | シンク | 権限 | ラボ検証 vs 推測 |
|---|---|---|---|
JSON の App ID が ../<name>(または ../../oracle/...)であるイベントログを仕込み、スキャンを待つ。アプリ UI を読み込む GET / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath、続いて Lease.commit の renameTo | 完全性: History UID として {store.path}/apps の外に .rdb(デフォルトの RocksDB)または .ldb(LevelDB)のパスを作成する。ガジェット上でラボ実証済み: escaped=yes、ファイル /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb にウィットネスが含まれる。内容はストアファイルであり、攻撃者が選択したホストのバイトコードでもシェルでもない。 | ラボ実証済み(エスケープされたパスへのウィットネスバイトのガジェット書き込み)。RocksDB ディレクトリの完全な History renameTo も同じ結合である。 |
| エスケープ先がすでにディレクトリである場合の同じ App ID | Lease.commit の if (dst.isDirectory()) deleteStore(dst)、続いて Utils.deleteRecursively | 可用性: 名前がすでにバックエンドのサフィックスで終わる既存のディレクトリを削除し、リースされたストアで置き換える。 | 推測(HistoryServerDiskManager.scala の commit をピン留め)。ガジェットはファイルを書き込み、deleteRecursively は呼び出さなかった。 |
デフォルトの未設定 spark.history.store.path | LOCAL_STORE_DIR の createOptional、diskManager = storePath.map | クローズ。インメモリの一覧表示。HistoryServerDiskManager なし。ラボ実証済み gated=yes written=no。 | ラボ実証済み |
App ID に先に Utils.sanitizeDirName を適用 | イベントログのファイル名ヘルパー | apps/ 内に留まる。/ と : は - になる。ラボ実証済みのネガティブ。 | ラボ実証済み |
store.path を設定した状態で App ID local-control を制御 | 同じ結合 | {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 でもない。28 回の調査。公開されている 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") が親を無視すると期待すること(実際は無視しない。残るのは ..); サフィックスが .rdb であるという理由で RCE を主張すること; 結合部分がオラクルであるのに完全な History Server を起動すること; sanitizeDirName がこのパスをカバーしていると扱うこと。
python3 spark-history-store-path-Abraxas-Labs.py
または
cd lab
./run.sh
127.0.0.1:18990 上の HistoryServerDiskManager.appStorePath のガジェット。抜粋は lab/pin/ 配下に固定する。完全な History Server は起動しない。ループバックにバインドする。
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
new File の前に、appStorePath 内で appId と attemptId に対して Utils.sanitizeDirName を実行します(または .. / / / NUL を拒否します)。結合後、dst.getCanonicalFile() が appStoreDir.getCanonicalFile() の配下に留まることを必須とします。spark.history.store.path を未設定のままにすることが安全なデフォルトです。
32f7299601108917fb01920a54e084595b7b3bf8)HistoryServerDiskManager.scala appStorePath / Lease.commitFsHistoryProvider.scala diskManager / loadDiskStore / onApplicationStartJsonProtocol.scala applicationStartFromJsonUtils.scala sanitizeDirNameHistory.scala LOCAL_STORE_DIR(任意)GNU Affero GPL v3.0。LICENSE を参照してください。ループバックラボ専用です。保証はありません。