
Apache Spark 4.2.0 - 高 - リモート - ファイル読み取り - 認証不要のRPC StreamRequestファイル結合
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-rpc-stream-unauth
クラス: 権限の取り残し / ファイル読み取り 到達範囲: リモート
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
CVE-2020-9480 は、RPC コントロールプレーン上の未認証の RequestSubmitDriver / RegisterApplication を塞いだ。AbstractAuthRpcHandler は receive() と receiveStream() をゲートする。しかし getStreamManager() はゲートしない。TransportRequestHandler は依然として SASL の前に StreamRequest をそのマネージャへディスパッチする。NettyStreamManager.openStream はルート配下に留まるチェックなしで new File(dir, fname) を実行する。
未認証の StreamRequest classes/../../path が Spark-UID のファイルを読み取る。 Unix の絶対パスの子は登録ディレクトリ配下に留まる(ラボでは 404)。files/ はマップ取得(404)。receive() / receiveStream() は 401 のまま。これは 9480 の RCE ではない。
| ID | CVE 未割当 |
| クラス | 権限の取り残し / ファイル読み取り(CVE-2020-9480 のゲートされていない RPC データプレーンの取り残し) |
| 到達範囲 | リモート(spark.authenticate=true のドライバ RpcEnv / ESS / BlockManager への未認証 TCP) |
| CWE | CWE-862, CWE-22 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| 製品 | Apache Spark |
| 影響を受けるバージョン | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8)。オペレータが spark.authenticate=true を有効化していること。ホストファイル読み取りには登録ディレクトリ(spark.repl.class.outputDir の /classes および/または Connect の artifacts)が必要。 |
| 認証 | StreamRequest / getStreamManager() には不要 |
| ライセンス | GNU Affero GPL v3.0 |
| ラボ | 127.0.0.1 のみ |
spark.authenticate=true と登録ディレクトリを持つ Spark RPC ポートへの TCP。共有シークレットは不要。spark.network.crypto.enabled も不要。トランスポートの StreamRequest(メッセージタイプ 6)を streamId classes/../../<path> で送信する。TransportRequestHandler.processStreamRequest は RpcHandler.receive を決して呼び出さない。代わりに getStreamManager().openStream を呼び出す。9480 のラッパーは本物のマネージャを返す。new File(dir, "../../oracle/secret") は REPL/アーティファクトルートの外へ正規化され、FileSegmentManagedBuffer として返ってくる。
これは Spark UID としてのホストファイル読み取りである。ジョブ投入ではない。RequestSubmitDriver ではない。シェルでもない。
| 手法 | シンク | 権限 |
|---|---|---|
未認証 StreamRequest classes/../../<path> | NettyStreamManager.openStream の new File(dir, fname) の後 FileSegmentManagedBuffer | ドライバが開ける Spark-UID ファイルを読み取る。ラボで実証済み: opened /opt/repl-classes/../../oracle/secret、正規化後 /oracle/secret、本文 SPARK-RPC-STREAM-UNAUTH-WITNESS、x-auth-gated=false、x-authenticated=false。 |
未認証 StreamRequest classes/hello.txt(ディレクトリ内コントロール) | 同じ結合、登録ディレクトリ配下に留まる | 実際に /classes 配下にあるファイルを読み取る。ラボで実証済み 200 IN-DIR-OK、witness なし。含まれる名前であっても SASL なしでファイルサーバが機能することを確認。 |
| 手法 | シンク | 権限 |
|---|---|---|
稼働中の ESS streamId に対する未認証 ChunkFetchRequest / StreamRequest | OneForOneStreamManager.getChunk / openStream。client.getClientId() == null のとき checkAuthorization がスキップされる | 別アプリの進行中の shuffle / ディスク RDD バイトを読み取る。可用性: ドレインが curChunk を進める。稼働中の streamId が必要(ランダム開始、その後 +1)。ガジェットは稼働中の ESS と通信しなかった。 |
未認証 MergedBlockMetaRequest | AbstractAuthRpcHandler.getMergedBlockMetaReqHandler() もゲートされていない | ESS がマージド shuffle を持つとき、選択した appId のマージド shuffle チャンクビットマップを読み取る。同じ null clientId スキップ。検証はしていない。 |
| 手法 | シンク | 塞がれたまま |
|---|---|---|
未認証 StreamRequest classes//oracle/secret(Unix 絶対パスの子) | new File(dir, "/oracle/secret") | 404。開かれるパスは /opt/repl-classes/oracle/secret。Unix の File は連結する。親を無視しない。絶対パスの子がルートを捨てるというハントの主張は Unix では誤り。 |
未認証 StreamRequest files//oracle/secret | files マップ取得 | 404。files/ と jars/ はベース名のルックアップであり、結合ではない。 |
未認証 receive() / OneWayMessage | AbstractAuthRpcHandler.receive | 401 Unauthenticated call to receive(). x-auth-gated=true。 |
未認証 receiveStream() / UploadStream | AbstractAuthRpcHandler.receiveStream | 401 Unauthenticated call to receiveStream(). |
ラボのシークレットによる認証済み receiveStream() | doAuthChallenge 後の同じゲート | 200 receiveStream-ok。シークレットが依然として 9480 パスを開くことのコントロール。 |
ラボで実証済み vs 推測: 未認証 .. ファイル読み取り、Unix 絶対パス 404、files/ 404、ゲートされた receive / receiveStream はラボ。ESS shuffle ドレインは推測。
隣接する塞がれたもの(誇張しないこと):
RequestSubmitDriver / RegisterApplication。依然として共有シークレットが必要。spark.authenticate=false での未認証 shuffle とジョブ投入(SECURITY.md FAQ、信頼ネットワークのデフォルトとして文書化)。この取り残しではない。AppIsolationSuite / SPARK-57889 の非 null clientId)。/classes や artifacts を登録していないため、このホストファイル探索はドライバ / REPL / Connect であり、master:7077 ではない。spark.authenticate.secret をダンプしない。私は apache/spark v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) をピン留めした。master でも 4.3 RC でもない。28 回のソースハント。オープンクラスタでのジョブ投入は設計通り。ML モデルロードは実行可能コード。公開されている Jackson History RCE、UI id -Gn シェル、PySpark ローカルなりすまし、9480 の receive パスはこの剥離では塞がれている。
RPC 認証のハントは 9480 の再ファイルチェックのはずだった。AuthServerBootstrap は依然としてハンドラをラップしている。未認証 receive は依然として例外を投げる。そこで私は getStreamManager() を読んだ。3 行。デリゲートを返す。TransportRequestHandler.handle は StreamRequest を receive を迂回してルーティングする。デフォルトの StreamManager.checkAuthorization は no-op。SPARK-57889 は StreamRequest に文字列 ID チェックを追加し、ESS が「認証オフ」に使う null clientId スキップを残した。それは「この接続は一度も認証していない」でもある。
SparkContext は spark.repl.class.outputDir が設定されているとき /classes を登録する。Connect の ArtifactManager は artifacts を登録する。NettyStreamManager.openStream は ftype/fname を分割する。files/jars 以外のタイプは正規化プレフィックスチェックなしで new File(dir, fname) を実行する。
記録に残す誤った道。ハントの自信を一日無駄にしたからだ。JavaDoc は絶対パスの子が親を無視すると述べている。Unix では UnixFileSystem.resolve は連結する。new File("/opt/repl-classes", "/oracle/secret") は /opt/repl-classes/oracle/secret になる。ラボでは 404。High の道は classes/../../oracle/secret である。
茶番: これを 9480 RCE だと主張すること。コントロールプレーンは依然としてゲートされている。オラクルは Spark UID がすでに読めるファイル内の witness 文字列にすぎない。
ピン留めソースのガジェット(AbstractAuthRpcHandler.java:52-87、NettyStreamManager.scala:54-70)。HTTP は TransportRequestHandler の代用: /stream?id= は StreamRequest、POST /rpc/receive は OneWayMessage、POST /rpc/receive-stream は UploadStream。公式の spark:4.2.0 は不要。ループバックのみ。
python3 spark-rpc-stream-unauth-Abraxas-Labs.py
または:
cd lab
./run.sh
ループバック 127.0.0.1:18910。Compose プロジェクト spark-rpc-stream-unauth。witness SPARK-RPC-STREAM-UNAUTH-WITNESS は /oracle/secret 内。登録ディレクトリ /opt/repl-classes に hello.txt = IN-DIR-OK。最終行:
SUCCESS SPARK-RPC-STREAM-UNAUTH SPARK-RPC-STREAM-UNAUTH-WITNESS
isAuthenticated まで getStreamManager()(および getMergedBlockMetaReqHandler())をゲートするか、拒否するマネージャを返す。NettyStreamManager.openStream では、getCanonicalFile 後に登録ディレクトリを離れる fname を拒否する。ESS では、spark.authenticate がオンのとき null clientId を未認証として扱う。SPARK-57889 はすでに StreamRequest で checkAuthorization を呼び出している。スキップが取り残しである。
それまでは、Spark UID のファイルをすでに信頼していない誰からでも TCP ポートに到達できる RpcEnv に /classes や artifacts を登録しないこと。spark.authenticate=true はこのデータプレーンを塞がない。