Initial commit — SetecSuite Camera MITM Framework

Original tooling from the Camhak research project (camera teardown of a
rebranded UBIA / Javiscam IP camera). PyQt6 GUI on top of a curses TUI on
top of a service controller; per-service start/stop, intruder detection,
protocol fingerprinting, OAM HMAC signing, CVE verifiers, OTA bucket
probe, firmware fetcher, fuzzer, packet injection.

Tabs: Dashboard, Live Log, Intruders, Cloud API, Fuzzer, Inject, CVEs,
Config, Help. Real-time per-packet protocol detection, conntrack-based
original-destination lookup, log rotation at 1 GiB.

See SECURITY_PAPER.md for the full writeup, site/index.html for the
public report, README.md for usage. Run with:
    sudo /usr/bin/python3 gui.py

Co-authored by Setec Labs.
This commit is contained in:
sssnake
2026-04-09 08:14:18 -07:00
commit 800052acc2
38 changed files with 7148 additions and 0 deletions

813
gui.py Executable file
View File

@@ -0,0 +1,813 @@
#!/usr/bin/python3
"""
SetecSuite — Camera MITM Tool (PyQt6 GUI)
Usage: sudo /usr/bin/python3 gui.py
"""
import os
import sys
import json
import threading
import signal
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject
from PyQt6.QtGui import QFont, QTextCursor, QColor, QPalette, QAction
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QPlainTextEdit, QLabel, QLineEdit, QTableWidget,
QTableWidgetItem, QHeaderView, QFormLayout, QGroupBox, QComboBox,
QStatusBar, QMessageBox, QSplitter, QTextEdit, QAbstractItemView,
QCheckBox,
)
from config import Config
from utils.log import (
log, log_lines, init_logfile, close_logfile, lock,
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 utils import proto as proto_id
from api import ubox_client, server as rest_server, fuzzer, firmware_fetch, cve_checks, ota_bucket_probe
from api.fuzzer import KNOWN_ENDPOINTS
from inject import packet
from mitm import Controller
# ─── Color map (Qt) ──────────────────────────────────────────────
QT_COLORS = {
C_NONE: QColor("#cccccc"),
C_ERROR: QColor("#ff5555"),
C_SUCCESS: QColor("#50fa7b"),
C_INFO: QColor("#8be9fd"),
C_TRAFFIC: QColor("#f1fa8c"),
C_IMPORTANT: QColor("#ff79c6"),
}
# ─── Bridge: log_lines deque -> Qt signal ────────────────────────
class LogBridge(QObject):
new_lines = pyqtSignal(list) # list of (line, color)
class CloudBridge(QObject):
response = pyqtSignal(str, object) # label, payload
class CveBridge(QObject):
result = pyqtSignal(str, object) # cve_id, result dict
# ─── Main Window ─────────────────────────────────────────────────
class MainWindow(QMainWindow):
def __init__(self, ctrl):
super().__init__()
self.ctrl = ctrl
self.setWindowTitle("SetecSuite — Camera MITM")
self.resize(1400, 900)
self._apply_dark_theme()
self.bridge = LogBridge()
self.bridge.new_lines.connect(self._append_log)
self._last_log_idx = 0
self.cloud_bridge = CloudBridge()
self.cloud_bridge.response.connect(self._cloud_set_response)
self.cloud_response_signal = self.cloud_bridge.response
self.cve_bridge = CveBridge()
self.cve_bridge.result.connect(self._cve_set_status)
self.cve_signal = self.cve_bridge.result
self.tabs = QTabWidget()
self.tabs.addTab(self._build_dashboard(), "Dashboard")
self.tabs.addTab(self._build_log_tab(), "Live Log")
self.tabs.addTab(self._build_intruder_tab(), "Intruders")
self.tabs.addTab(self._build_cloud_tab(), "Cloud API")
self.tabs.addTab(self._build_fuzzer_tab(), "Fuzzer")
self.tabs.addTab(self._build_inject_tab(), "Inject")
self.tabs.addTab(self._build_cve_tab(), "CVEs")
self.tabs.addTab(self._build_config_tab(), "Config")
self.tabs.addTab(self._build_help_tab(), "Help")
self.setCentralWidget(self.tabs)
self.status = QStatusBar()
self.setStatusBar(self.status)
# Periodic UI refresh
self.refresh_timer = QTimer(self)
self.refresh_timer.timeout.connect(self._tick)
self.refresh_timer.start(300)
# Start REST API
threading.Thread(
target=rest_server.start_server,
args=(self.ctrl, self.ctrl.cfg["rest_port"]),
daemon=True,
).start()
log("SetecSuite GUI ready", C_SUCCESS)
log(f"Target: {ctrl.cfg['camera_ip']} Us: {ctrl.cfg['our_ip']} Router: {ctrl.cfg['router_ip']}", C_INFO)
# ── Theme ────────────────────────────────────────────────────
def _apply_dark_theme(self):
self.setStyleSheet("""
QWidget { background: #1e1f29; color: #f8f8f2; font-family: 'JetBrains Mono','Fira Code',monospace; font-size: 11pt; }
QTabWidget::pane { border: 1px solid #44475a; }
QTabBar::tab { background: #282a36; padding: 8px 16px; border: 1px solid #44475a; }
QTabBar::tab:selected { background: #44475a; color: #50fa7b; }
QPushButton { background: #44475a; border: 1px solid #6272a4; padding: 6px 14px; border-radius: 3px; }
QPushButton:hover { background: #6272a4; }
QPushButton:pressed { background: #50fa7b; color: #282a36; }
QPlainTextEdit, QTextEdit, QLineEdit { background: #282a36; border: 1px solid #44475a; selection-background-color: #44475a; }
QHeaderView::section { background: #44475a; color: #f8f8f2; padding: 4px; border: none; }
QTableWidget { background: #282a36; gridline-color: #44475a; }
QTableWidget::item:selected { background: #44475a; }
QGroupBox { border: 1px solid #44475a; margin-top: 10px; padding-top: 10px; }
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #8be9fd; }
QStatusBar { background: #282a36; color: #50fa7b; }
QLabel#bigState { font-size: 18pt; font-weight: bold; }
QLabel.flag_on { color: #50fa7b; font-weight: bold; }
QLabel.flag_off { color: #ff5555; }
""")
# ── Dashboard tab ────────────────────────────────────────────
def _build_dashboard(self):
w = QWidget()
layout = QVBoxLayout(w)
ctrl_box = QGroupBox("MITM Control")
cl = QHBoxLayout(ctrl_box)
self.btn_start = QPushButton("▶ START MITM")
self.btn_stop = QPushButton("⏹ STOP")
self.btn_clear = QPushButton("Clear Log")
self.btn_start.clicked.connect(lambda: threading.Thread(target=self.ctrl.start_services, daemon=True).start())
self.btn_stop.clicked.connect(lambda: threading.Thread(target=self.ctrl.stop_services, daemon=True).start())
self.btn_clear.clicked.connect(self._clear_log)
cl.addWidget(self.btn_start)
cl.addWidget(self.btn_stop)
cl.addWidget(self.btn_clear)
cl.addStretch()
layout.addWidget(ctrl_box)
self.state_label = QLabel("MITM: STOPPED")
self.state_label.setObjectName("bigState")
self.state_label.setStyleSheet("color: #ff5555; font-size: 18pt; font-weight: bold; padding: 10px;")
layout.addWidget(self.state_label)
flags_box = QGroupBox("Services (click to toggle)")
self.flags_layout = QVBoxLayout(flags_box)
self.svc_buttons = {}
for name in ("arp", "dns", "http", "https", "udp10240", "udp20001", "sniffer", "intruder"):
btn = QPushButton(f"{name}: off")
btn.setStyleSheet("text-align:left; color:#ff5555; padding:6px; background:#282a36;")
btn.clicked.connect(lambda _, n=name: threading.Thread(
target=self.ctrl.toggle_service, args=(n,), daemon=True).start())
self.svc_buttons[name] = btn
self.flags_layout.addWidget(btn)
layout.addWidget(flags_box)
proto_box = QGroupBox("Protocols Seen")
self.proto_layout = QVBoxLayout(proto_box)
self.proto_label = QLabel("(none yet)")
self.proto_label.setStyleSheet("color: #f1fa8c; font-family: monospace;")
self.proto_layout.addWidget(self.proto_label)
layout.addWidget(proto_box)
info_box = QGroupBox("Target")
il = QFormLayout(info_box)
self.lbl_cam = QLabel(self.ctrl.cfg["camera_ip"])
self.lbl_us = QLabel(self.ctrl.cfg["our_ip"])
self.lbl_rtr = QLabel(self.ctrl.cfg["router_ip"])
self.lbl_mac = QLabel(self.ctrl.cfg["camera_mac"])
for lbl in (self.lbl_cam, self.lbl_us, self.lbl_rtr, self.lbl_mac):
lbl.setStyleSheet("color: #f1fa8c; font-weight: bold;")
il.addRow("Camera IP:", self.lbl_cam)
il.addRow("Our IP:", self.lbl_us)
il.addRow("Router IP:", self.lbl_rtr)
il.addRow("Camera MAC:", self.lbl_mac)
layout.addWidget(info_box)
layout.addStretch()
return w
# ── Live log tab ─────────────────────────────────────────────
def _build_log_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
bar = QHBoxLayout()
bar.addWidget(QLabel("Filter:"))
self.log_filter = QLineEdit()
self.log_filter.setPlaceholderText("substring filter (live)…")
bar.addWidget(self.log_filter)
self.autoscroll_cb = QCheckBox("Autoscroll")
self.autoscroll_cb.setChecked(True)
bar.addWidget(self.autoscroll_cb)
btn_clear = QPushButton("Clear")
btn_clear.clicked.connect(self._clear_log)
bar.addWidget(btn_clear)
layout.addLayout(bar)
self.log_view = QTextEdit()
self.log_view.setReadOnly(True)
self.log_view.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
f = QFont("JetBrains Mono", 10)
f.setStyleHint(QFont.StyleHint.Monospace)
self.log_view.setFont(f)
layout.addWidget(self.log_view)
return w
# ── Intruder tab ─────────────────────────────────────────────
def _build_intruder_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
head = QHBoxLayout()
self.intruder_count = QLabel("0 events")
self.intruder_count.setStyleSheet("color: #ff79c6; font-size: 14pt; font-weight: bold;")
head.addWidget(self.intruder_count)
head.addStretch()
btn_clear = QPushButton("Clear")
btn_clear.clicked.connect(lambda: (intruder_watch.clear_intruders(), self._refresh_intruders()))
head.addWidget(btn_clear)
layout.addLayout(head)
self.intruder_table = QTableWidget(0, 5)
self.intruder_table.setHorizontalHeaderLabels(["Time", "Kind", "Source", "Destination", "Detail"])
self.intruder_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.intruder_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.intruder_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
layout.addWidget(self.intruder_table)
return w
# ── Cloud API tab ────────────────────────────────────────────
def _build_cloud_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
cred_box = QGroupBox("UBox Credentials")
cl = QFormLayout(cred_box)
self.api_email = QLineEdit(self.ctrl.cfg.get("api_email", ""))
self.api_pw = QLineEdit(self.ctrl.cfg.get("api_password", ""))
self.api_pw.setEchoMode(QLineEdit.EchoMode.Password)
cl.addRow("Email:", self.api_email)
cl.addRow("Password:", self.api_pw)
layout.addWidget(cred_box)
self.cloud_status = QLabel("not logged in")
self.cloud_status.setStyleSheet("color: #ff5555; font-weight: bold; padding: 4px;")
layout.addWidget(self.cloud_status)
btn_box = QHBoxLayout()
actions = [
("Login", self._cloud_login),
("Devices", lambda: self._cloud_call("devices", ubox_client.devices)),
("Firmware", lambda: self._cloud_call("firmware", ubox_client.check_firmware)),
("Services", lambda: self._cloud_call("services", ubox_client.device_services)),
("Families", lambda: self._cloud_call("families", ubox_client.families)),
]
for label, fn in actions:
b = QPushButton(label)
b.clicked.connect(fn)
btn_box.addWidget(b)
layout.addLayout(btn_box)
fw_box = QGroupBox("Firmware Download")
fwl = QHBoxLayout(fw_box)
fwl.addWidget(QLabel("host_version:"))
self.fw_version = QLineEdit()
self.fw_version.setPlaceholderText("blank = auto-try common versions")
fwl.addWidget(self.fw_version)
fw_btn = QPushButton("Download FW")
fw_btn.clicked.connect(lambda: self._cloud_call(
"download_fw",
lambda cfg: firmware_fetch.check_and_download(
cfg, host_version=(self.fw_version.text() or None)
),
))
fwl.addWidget(fw_btn)
probe_btn = QPushButton("Probe OTA Bucket")
probe_btn.clicked.connect(lambda: self._cloud_call(
"ota_probe", ota_bucket_probe.probe
))
fwl.addWidget(probe_btn)
layout.addWidget(fw_box)
raw_box = QGroupBox("Raw POST")
rl = QHBoxLayout(raw_box)
self.raw_endpoint = QComboBox()
self.raw_endpoint.setEditable(True)
self.raw_endpoint.addItem("") # blank first
for ep in sorted(set(KNOWN_ENDPOINTS)):
self.raw_endpoint.addItem(ep)
self.raw_endpoint.lineEdit().setPlaceholderText(
f"pick one of {len(KNOWN_ENDPOINTS)} known endpoints, or type your own"
)
self.raw_endpoint.setMinimumWidth(420)
rl.addWidget(self.raw_endpoint, stretch=1)
rb = QPushButton("Send")
rb.clicked.connect(lambda: self._cloud_call(
f"raw {self.raw_endpoint.currentText()}",
lambda cfg: ubox_client.raw_request(cfg, self.raw_endpoint.currentText())
))
rl.addWidget(rb)
oam_btn = QPushButton("OAM Send")
oam_btn.setToolTip("Sign with OAM HMAC secret and post to oam.ubianet.com")
oam_btn.clicked.connect(lambda: self._cloud_call(
f"oam {self.raw_endpoint.currentText()}",
lambda cfg: ubox_client.oam_post(self.raw_endpoint.currentText(), {})
))
rl.addWidget(oam_btn)
oam_fuzz_btn = QPushButton("OAM Fuzz")
oam_fuzz_btn.setToolTip("Probe ~50 candidate OAM admin endpoints")
oam_fuzz_btn.clicked.connect(lambda: self._cloud_call(
"oam_fuzz",
lambda cfg: ubox_client.oam_fuzz(ubox_client.OAM_ENDPOINT_GUESSES)
))
rl.addWidget(oam_fuzz_btn)
layout.addWidget(raw_box)
resp_box = QGroupBox("Response")
rvl = QVBoxLayout(resp_box)
self.cloud_response = QPlainTextEdit()
self.cloud_response.setReadOnly(True)
f = QFont("JetBrains Mono", 10)
f.setStyleHint(QFont.StyleHint.Monospace)
self.cloud_response.setFont(f)
self.cloud_response.setPlaceholderText("API responses will appear here")
rvl.addWidget(self.cloud_response)
layout.addWidget(resp_box, stretch=1)
return w
def _cloud_set_response(self, label, obj):
try:
text = json.dumps(obj, indent=2, ensure_ascii=False)
except Exception:
text = repr(obj)
self.cloud_response.setPlainText(f"=== {label} ===\n{text}")
def _cloud_call(self, label, fn):
def run():
try:
result = fn(self.ctrl.cfg)
if label == "devices" and isinstance(result, list):
self.ctrl._devices = result
self.cloud_response_signal.emit(label, result if result is not None else {"result": None})
except Exception as e:
self.cloud_response_signal.emit(label, {"exception": str(e)})
threading.Thread(target=run, daemon=True).start()
def _cloud_login(self):
self.ctrl.cfg["api_email"] = self.api_email.text()
self.ctrl.cfg["api_password"] = self.api_pw.text()
self.ctrl.cfg.save()
def run():
ok = ubox_client.login(self.ctrl.cfg)
payload = {
"logged_in": bool(ok),
"token": self.ctrl.cfg.get("api_token", ""),
"email": self.ctrl.cfg.get("api_email", ""),
}
self.cloud_response_signal.emit("login", payload)
threading.Thread(target=run, daemon=True).start()
# ── Fuzzer tab ───────────────────────────────────────────────
def _build_fuzzer_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
bb = QHBoxLayout()
b1 = QPushButton("Fuzz Endpoints")
b1.clicked.connect(lambda: threading.Thread(target=self.ctrl.run_fuzz_endpoints, daemon=True).start())
bb.addWidget(b1)
b2 = QPushButton("Fuzz Auth")
b2.clicked.connect(lambda: threading.Thread(target=self.ctrl.run_fuzz_auth, daemon=True).start())
bb.addWidget(b2)
bb.addStretch()
layout.addLayout(bb)
param_box = QGroupBox("Param Fuzz")
pl = QHBoxLayout(param_box)
self.fuzz_ep = QLineEdit()
self.fuzz_ep.setPlaceholderText("endpoint name")
pl.addWidget(self.fuzz_ep)
b3 = QPushButton("Run")
b3.clicked.connect(lambda: threading.Thread(
target=self.ctrl.run_fuzz_params, args=(self.fuzz_ep.text(),), daemon=True
).start())
pl.addWidget(b3)
layout.addWidget(param_box)
self.fuzz_status = QLabel("Idle")
layout.addWidget(self.fuzz_status)
b4 = QPushButton("Stop fuzzer")
b4.clicked.connect(lambda: (self.ctrl.fuzzer and self.ctrl.fuzzer.stop()))
layout.addWidget(b4)
layout.addStretch()
return w
# ── Inject tab ───────────────────────────────────────────────
def _build_inject_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
udp_box = QGroupBox("UDP Inject")
ul = QFormLayout(udp_box)
self.udp_ip = QLineEdit(self.ctrl.cfg["camera_ip"])
self.udp_port = QLineEdit("10240")
self.udp_payload = QLineEdit()
self.udp_payload.setPlaceholderText("hex payload, e.g. deadbeef")
ul.addRow("Dst IP:", self.udp_ip)
ul.addRow("Dst port:", self.udp_port)
ul.addRow("Payload:", self.udp_payload)
b = QPushButton("Send UDP")
b.clicked.connect(self._send_udp)
ul.addRow("", b)
layout.addWidget(udp_box)
arp_box = QGroupBox("ARP Reply")
al = QFormLayout(arp_box)
self.arp_src = QLineEdit(self.ctrl.cfg["router_ip"])
self.arp_dst = QLineEdit(self.ctrl.cfg["camera_ip"])
al.addRow("Src IP (spoof):", self.arp_src)
al.addRow("Dst IP:", self.arp_dst)
b2 = QPushButton("Send ARP")
b2.clicked.connect(lambda: packet.inject(self.ctrl.cfg, {
"type": "arp_reply", "src_ip": self.arp_src.text(), "dst_ip": self.arp_dst.text()
}))
al.addRow("", b2)
layout.addWidget(arp_box)
layout.addStretch()
return w
def _send_udp(self):
try:
packet.inject(self.ctrl.cfg, {
"type": "udp",
"dst_ip": self.udp_ip.text(),
"dst_port": int(self.udp_port.text()),
"payload": self.udp_payload.text(),
"payload_hex": True,
})
except Exception as e:
QMessageBox.warning(self, "Inject error", str(e))
# ── Config tab ───────────────────────────────────────────────
def _build_config_tab(self):
w = QWidget()
layout = QFormLayout(w)
self.cfg_inputs = {}
for k, v in self.ctrl.cfg.items():
if "password" in k or "token" in k:
continue
le = QLineEdit(str(v))
self.cfg_inputs[k] = le
layout.addRow(k, le)
b = QPushButton("Save Config")
b.clicked.connect(self._save_config)
layout.addRow("", b)
return w
# ── CVE tab ──────────────────────────────────────────────────
def _build_cve_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
head = QHBoxLayout()
title = QLabel("CVE verification — original probes, non-destructive")
title.setStyleSheet("color:#ff79c6; font-size:13pt; font-weight:bold;")
head.addWidget(title)
head.addStretch()
b_run = QPushButton("▶ Run All Verifications")
b_run.clicked.connect(self._cve_run_all)
head.addWidget(b_run)
b_rep = QPushButton("📝 Generate Report")
b_rep.clicked.connect(self._cve_generate_report)
head.addWidget(b_rep)
layout.addLayout(head)
# Card per CVE
self.cve_cards = {}
cve_specs = [
("CVE-2025-12636",
"Ubia Ubox cloud API leaks IOTC device credentials in plaintext",
"The cloud `user/device_list` endpoint returns `cam_user` / `cam_pwd` "
"in plaintext to any authenticated owner. These are the IOTC P2P "
"device-auth credentials and grant full local control. Verifier calls "
"the endpoint and inspects the response.",
cve_checks.verify_cve_2025_12636),
("CVE-2021-28372",
"ThroughTek Kalay P2P UID-based session hijack (UBIC rebrand)",
"Kalay master server identifies devices by UID alone. An attacker "
"knowing the UID can register the same UID against the master and "
"intercept the next legitimate client login. We verify preconditions "
"(UID format + camera P2P stack alive) without performing the spoof "
"registration.",
cve_checks.verify_cve_2021_28372),
("CVE-2023-6322 / 6323 / 6324",
"ThroughTek Kalay LAN-side memory corruption + auth bypass chain",
"Three flaws in the Kalay LAN protocol parser: an auth bypass, a "
"heap overflow, and a stack overflow. Verifier sends only safe small "
"probes — no overflow payloads — and reports based on stack "
"fingerprint and pre/post liveness.",
cve_checks.verify_cve_2023_6322_chain),
]
for cve_id, title_txt, desc, verifier in cve_specs:
card = QGroupBox(cve_id)
cl = QVBoxLayout(card)
t = QLabel(f"<b>{title_txt}</b>")
t.setWordWrap(True)
t.setStyleSheet("color:#8be9fd;")
cl.addWidget(t)
d = QLabel(desc)
d.setWordWrap(True)
d.setStyleSheet("color:#cccccc; padding:4px;")
cl.addWidget(d)
row = QHBoxLayout()
status = QLabel("● not run")
status.setStyleSheet("color:#888888; font-weight:bold;")
row.addWidget(status)
row.addStretch()
btn = QPushButton("Verify")
btn.clicked.connect(lambda _, v=verifier, cid=cve_id: self._cve_run_one(cid, v))
row.addWidget(btn)
cl.addLayout(row)
evidence = QPlainTextEdit()
evidence.setReadOnly(True)
evidence.setMaximumHeight(120)
evidence.setPlaceholderText("evidence + raw artifacts will appear here")
cl.addWidget(evidence)
self.cve_cards[cve_id] = {
"status": status,
"evidence": evidence,
"result": None,
}
layout.addWidget(card)
layout.addStretch()
return w
def _cve_set_status(self, cve_id, result):
card = self.cve_cards.get(cve_id)
if not card:
return
card["result"] = result
s = result.get("status", "?")
color = {"VULN": "#ff5555", "NOT_VULN": "#50fa7b",
"UNKNOWN": "#f1fa8c", "ERROR": "#ff79c6"}.get(s, "#cccccc")
card["status"].setText(f"{s}")
card["status"].setStyleSheet(f"color:{color}; font-weight:bold;")
text = result.get("evidence", "") + "\n\n" + json.dumps(result.get("details", {}), indent=2, ensure_ascii=False)
card["evidence"].setPlainText(text)
def _cve_run_one(self, cve_id, verifier):
def run():
try:
r = verifier(self.ctrl.cfg)
except Exception as e:
r = {"cve": cve_id, "status": "ERROR",
"title": "exception", "evidence": str(e), "details": {}}
self.cve_signal.emit(cve_id, r)
threading.Thread(target=run, daemon=True).start()
def _cve_run_all(self):
def run():
for cve_id, fn in [
("CVE-2025-12636", cve_checks.verify_cve_2025_12636),
("CVE-2021-28372", cve_checks.verify_cve_2021_28372),
("CVE-2023-6322 / 6323 / 6324", cve_checks.verify_cve_2023_6322_chain),
]:
try:
r = fn(self.ctrl.cfg)
except Exception as e:
r = {"cve": cve_id, "status": "ERROR",
"title": "exception", "evidence": str(e), "details": {}}
self.cve_signal.emit(cve_id, r)
threading.Thread(target=run, daemon=True).start()
def _cve_generate_report(self):
results = []
for cve_id, card in self.cve_cards.items():
if card["result"]:
results.append(card["result"])
if not results:
QMessageBox.information(self, "Report", "Run at least one verification first.")
return
try:
path = cve_checks.build_report(self.ctrl.cfg, results)
QMessageBox.information(self, "Report written", f"Saved to:\n{path}")
except Exception as e:
QMessageBox.warning(self, "Report failed", str(e))
# ── Help tab ─────────────────────────────────────────────────
def _build_help_tab(self):
w = QWidget()
layout = QVBoxLayout(w)
view = QTextEdit()
view.setReadOnly(True)
view.setHtml("""
<h2 style="color:#50fa7b">SetecSuite — Camera MITM</h2>
<p><b>Target:</b> Javiscam/UBox cam (TUTK Kalay) at the IP shown in the Dashboard.</p>
<h3 style="color:#8be9fd">Tabs</h3>
<ul>
<li><b>Dashboard</b> — START/STOP MITM, click any service row to toggle individually, watch protocol counts and target info.</li>
<li><b>Live Log</b> — every log line, color-coded. Filter by substring. Toggle Autoscroll if you want to read history while traffic flows.</li>
<li><b>Intruders</b> — table of detected suspicious activity (ARP spoofs, unknown LAN peers, unexpected outbound dests).</li>
<li><b>Cloud API</b> — UBox portal: Login, Devices, Firmware, Services, Families, Raw POST. Firmware Download tries multiple host_versions to trick the cloud into returning an OTA URL.</li>
<li><b>Fuzzer</b> — endpoint discovery (~146 known + ~600 wordlist), parameter mutation, auth bypass.</li>
<li><b>Inject</b> — craft and send raw UDP, ARP, or DNS packets.</li>
<li><b>Config</b> — edit any config key, save to disk.</li>
</ul>
<h3 style="color:#8be9fd">Services (Dashboard buttons)</h3>
<ul>
<li><b>arp</b> — ARP spoof: tell camera we are the gateway, tell gateway we are the camera.</li>
<li><b>dns</b> — DNS spoof: redirect cloud lookups to us.</li>
<li><b>http / https</b> — Intercept ports 80/443. HTTPS uses our regen'd cert with SAN list for ubianet, aliyuncs, myqcloud.</li>
<li><b>udp10240</b> — IOTC P2P relay port (Tencent/Alibaba clouds use this).</li>
<li><b>udp20001</b> — Push notification service.</li>
<li><b>sniffer</b> — Raw packet sniffer; logs cam:src → us:dst with conntrack-extracted original destination + protocol fingerprint.</li>
<li><b>intruder</b> — Detects: ARP spoofs against the camera, unknown LAN hosts contacting it, unexpected outbound destinations not in known cloud whitelist.</li>
</ul>
<h3 style="color:#8be9fd">Workflow: get firmware</h3>
<ol>
<li>Cloud API → fill creds → Login</li>
<li>Devices (populates device_uid)</li>
<li>Download FW (auto-tries 6 versions, or type one in the field)</li>
<li>If empty: start MITM, power-cycle camera, watch Live Log for the camera's own check_version request</li>
</ol>
<h3 style="color:#8be9fd">REST API</h3>
<p>Always running on <code>http://127.0.0.1:9090</code>. Endpoints: <code>/status, /logs, /devices, /config, /fuzz/results, /start, /stop, /command, /api, /fuzz/endpoints, /fuzz/params, /fuzz/auth, /inject</code></p>
<h3 style="color:#8be9fd">Files</h3>
<ul>
<li>Config: <code>~/setec_suite/cam-mitm/config.json</code></li>
<li>Logs: <code>/root/dumps/mitm_logs/mitm.log</code> (rotates at 1 GiB)</li>
<li>Captures: <code>/root/dumps/mitm_logs/raw_*.bin</code></li>
<li>SSL cert: regen with <code>sudo /home/snake/setec_suite/cam-mitm/regen_cert.sh</code></li>
</ul>
<h3 style="color:#8be9fd">Known credentials (from APK)</h3>
<ul>
<li><b>Camera local:</b> admin / yyc1G::HPEv7om3O</li>
<li><b>Camera local alt:</b> admin / iotCam31</li>
<li><b>Box-mode:</b> admin / admin</li>
<li><b>OAM HMAC secret:</b> 2894df25f8f740dff5266bc155c662ca</li>
</ul>
<h3 style="color:#8be9fd">Run</h3>
<p><code>sudo /usr/bin/python3 /home/snake/setec_suite/cam-mitm/gui.py</code></p>
<p>(Custom Python 3.14 build at /usr/local/bin lacks _curses and PyQt6 — must use the system /usr/bin/python3)</p>
""")
layout.addWidget(view)
return w
def _save_config(self):
for k, le in self.cfg_inputs.items():
old = self.ctrl.cfg[k]
v = le.text()
try:
if isinstance(old, int):
v = int(v)
elif isinstance(old, float):
v = float(v)
except ValueError:
pass
self.ctrl.cfg[k] = v
self.ctrl.cfg.save()
log("Config saved from GUI", C_SUCCESS)
# ── Periodic refresh ─────────────────────────────────────────
def _tick(self):
# New log lines
with lock:
total = len(log_lines)
if total < self._last_log_idx:
self._last_log_idx = 0 # deque rolled
new = list(log_lines)[self._last_log_idx:]
self._last_log_idx = total
if new:
self.bridge.new_lines.emit(new)
# State
if self.ctrl.services_running:
self.state_label.setText("MITM: RUNNING")
self.state_label.setStyleSheet("color: #50fa7b; font-size: 18pt; font-weight: bold; padding: 10px;")
else:
self.state_label.setText("MITM: STOPPED")
self.state_label.setStyleSheet("color: #ff5555; font-size: 18pt; font-weight: bold; padding: 10px;")
for name, btn in self.svc_buttons.items():
on = self.ctrl.flags.get(name, False)
btn.setText(f"{name}: {'ON' if on else 'off'}")
color = "#50fa7b" if on else "#ff5555"
btn.setStyleSheet(f"text-align:left; color:{color}; padding:6px; background:#282a36;")
# Protocols seen
counts = proto_id.seen_counts()
if counts:
txt = " ".join(f"{k}={v}" for k, v in sorted(counts.items(), key=lambda x: -x[1]))
self.proto_label.setText(txt)
# Intruders
self._refresh_intruders()
# Cloud status
if self.ctrl.cfg.get("api_token"):
tok = self.ctrl.cfg["api_token"][:16]
self.cloud_status.setText(f"logged in as {self.ctrl.cfg.get('api_email','?')} token={tok}")
self.cloud_status.setStyleSheet("color: #50fa7b; font-weight: bold; padding: 4px;")
else:
self.cloud_status.setText("not logged in")
self.cloud_status.setStyleSheet("color: #ff5555; font-weight: bold; padding: 4px;")
# Status bar
n_devs = len(self.ctrl._devices) if self.ctrl._devices else 0
self.status.showMessage(
f"REST :{self.ctrl.cfg['rest_port']} | Devices: {n_devs} | "
f"Token: {'yes' if self.ctrl.cfg['api_token'] else 'no'} | "
f"Intruders: {len(intruder_watch.get_intruders())}"
)
def _refresh_intruders(self):
items = intruder_watch.get_intruders()
self.intruder_count.setText(f"{len(items)} events")
if self.intruder_table.rowCount() != len(items):
self.intruder_table.setRowCount(len(items))
for i, e in enumerate(items):
self.intruder_table.setItem(i, 0, QTableWidgetItem(e["ts"]))
kind_item = QTableWidgetItem(e["kind"])
kind_item.setForeground(QColor("#ff79c6"))
self.intruder_table.setItem(i, 1, kind_item)
self.intruder_table.setItem(i, 2, QTableWidgetItem(e["src"]))
self.intruder_table.setItem(i, 3, QTableWidgetItem(e["dst"]))
self.intruder_table.setItem(i, 4, QTableWidgetItem(e["detail"]))
# ── Log append ───────────────────────────────────────────────
def _append_log(self, lines):
flt = self.log_filter.text().lower()
autoscroll = self.autoscroll_cb.isChecked()
# Preserve current scroll position when autoscroll off
sb = self.log_view.verticalScrollBar()
old_pos = sb.value()
old_user_cursor = self.log_view.textCursor()
write_cursor = QTextCursor(self.log_view.document())
write_cursor.movePosition(QTextCursor.MoveOperation.End)
for line, color in lines:
if flt and flt not in line.lower():
continue
fmt = write_cursor.charFormat()
fmt.setForeground(QT_COLORS.get(color, QT_COLORS[C_NONE]))
write_cursor.setCharFormat(fmt)
write_cursor.insertText(line + "\n")
if autoscroll:
self.log_view.moveCursor(QTextCursor.MoveOperation.End)
self.log_view.ensureCursorVisible()
else:
self.log_view.setTextCursor(old_user_cursor)
sb.setValue(old_pos)
def _clear_log(self):
self.log_view.clear()
with lock:
log_lines.clear()
self._last_log_idx = 0
def closeEvent(self, ev):
if self.ctrl.services_running:
self.ctrl.stop_services()
self.ctrl.running = False
close_logfile()
ev.accept()
def main():
if os.geteuid() != 0:
print("Run with: sudo /usr/bin/python3 gui.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, signal.SIG_DFL)
app = QApplication(sys.argv)
win = MainWindow(ctrl)
win.show()
rc = app.exec()
if ctrl.services_running:
ctrl.stop_services()
close_logfile()
sys.exit(rc)
if __name__ == "__main__":
main()