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/ 是 map 查找(404)。receive() / receiveStream() 保持 401。这不是 9480 RCE。
| ID | 暂无 CVE |
| 类别 | 权限残留 / 文件读取(CVE-2020-9480 未门控的 RPC 数据平面残留) |
| 可达性 | 远程(未认证 TCP 连接到启用了 spark.authenticate=true 的 driver RpcEnv / ESS / BlockManager) |
| CWE | CWE-862、CWE-22 |
| CVSS | 高危: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 |
TCP 连接到启用了 spark.authenticate=true 且已注册目录的 Spark RPC 端口。无需共享密钥。无需 spark.network.crypto.enabled。发送传输层 StreamRequest(消息类型 6),streamId 为 classes/../../<path>。TransportRequestHandler.processStreamRequest 从不调用 RpcHandler.receive。它调用 getStreamManager().openStream。9480 的包装器返回真实的管理器。new File(dir, "../../oracle/secret") 规范化后位于 REPL/artifact 根目录之外,并作为 FileSegmentManagedBuffer 返回。
这就是以 Spark UID 身份进行的主机文件读取。它不是作业提交。它不是 RequestSubmitDriver。它不是 shell。
| 方法 | 汇聚点 | 权限 |
|---|---|---|
未认证 StreamRequest classes/../../<path> | NettyStreamManager.openStream 的 new File(dir, fname) 然后 FileSegmentManagedBuffer | 读取 driver 能打开的 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,无见证字符串。确认即使对于受限名称,文件服务器也能在没有 SASL 的情况下工作。 |
| 方法 | 汇聚点 | 权限 |
|---|---|---|
未认证 ChunkFetchRequest / StreamRequest 针对活跃 ESS streamId | OneForOneStreamManager.getChunk / openStream。当 client.getClientId() == null 时 checkAuthorization 跳过 | 读取其他应用正在进行的 shuffle / disk-RDD 字节。可用性:排空会推进 curChunk。需要活跃的 streamId(随机起始,然后 +1)。Gadget 未与活跃 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 map 查找 | 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,因此此主机文件遍历是 driver / REPL / Connect,而非 master:7077。spark.authenticate.secret。我固定了 apache/spark v4.2.0(32f7299601108917fb01920a54e084595b7b3bf8),不是 master,也不是 4.3 RC。二十八次源码搜寻。开放集群上的作业提交是设计使然。ML 模型加载是可执行代码。公开的 Jackson History RCE、UI id -Gn shell、PySpark 本地模拟以及 9480 接收路径在此剥离版本上均已关闭。
RPC 认证搜寻本应是对 9480 的重新归档检查。AuthServerBootstrap 仍然包装了处理器。未认证 receive 仍然抛出异常。然后我读了 getStreamManager()。三行。它返回委托。TransportRequestHandler.handle 将 StreamRequest 路由绕过 receive。默认的 StreamManager.checkAuthorization 是空操作。SPARK-57889 在 StreamRequest 上添加了字符串 ID 检查,并保留了 ESS 用于"认证关闭"的 null-clientId 跳过,这同时也是"此连接从未认证"。
当设置了 spark.repl.class.outputDir 时,SparkContext 注册 /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。高危路径是 classes/../../oracle/secret。
作秀:声称这是 9480 RCE。控制平面仍然被门控。预言机只是 Spark UID 已经能读取的文件中的一个见证字符串。
固定源码的 Gadget(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。见证字符串 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;跳过就是残留。
在此之前,不要在 TCP 端口可被任何你尚未信任其访问 Spark UID 文件的人访问的 RpcEnv 上注册 /classes 或 artifacts。spark.authenticate=true 并不能关闭此数据平面。