Files
cam-mitm/api/cve_checks.py
sssnake 800052acc2 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.
2026-04-09 08:14:18 -07:00

432 lines
16 KiB
Python

"""
CVE verification module — original probes against the local Javiscam/UBox
camera. No public PoC code reused; everything here is built from our own
research and the decompiled APK.
Each verifier returns:
{
"cve": "CVE-XXXX-YYYY",
"status": "VULN" | "NOT_VULN" | "UNKNOWN" | "ERROR",
"title": str,
"evidence": str, # short, human-readable
"details": dict, # raw artifacts (response bodies, hex, etc)
}
Verifiers should be NON-DESTRUCTIVE — probe, don't pwn.
"""
import json
import os
import socket
import struct
import time
from datetime import datetime
from utils.log import log, C_INFO, C_SUCCESS, C_ERROR, C_IMPORTANT
from api import ubox_client
REPORT_DIR = os.path.expanduser("~/dumps")
# ─────────────────────────────────────────────────────────────────
# CVE-2025-12636 — Ubia Ubox Insufficiently Protected Credentials
# Vector: the cloud `user/device_list` endpoint returns `cam_user` /
# `cam_pwd` in plaintext to any authenticated owner. These creds are the
# IOTC P2P device-auth credentials and grant full local control.
# Verification: log in, call device_list, scan response for those fields.
# ─────────────────────────────────────────────────────────────────
def verify_cve_2025_12636(cfg):
out = {
"cve": "CVE-2025-12636",
"title": "Ubia Ubox cloud API leaks IOTC device credentials in plaintext",
"status": "UNKNOWN",
"evidence": "",
"details": {},
}
if not cfg.get("api_token"):
out["status"] = "ERROR"
out["evidence"] = "no api_token — log in first"
return out
log("CVE-2025-12636: calling user/device_list…", C_INFO)
resp = ubox_client.api_post(
cfg["api_base"], "user/device_list", {}, cfg["api_token"]
)
out["details"]["raw_response_keys"] = list(resp.keys()) if isinstance(resp, dict) else None
if not isinstance(resp, dict) or resp.get("msg") != "success":
out["status"] = "ERROR"
out["evidence"] = f"unexpected response: {json.dumps(resp)[:200]}"
return out
devices = (resp.get("data") or {}).get("list") or []
leaked = []
for d in devices:
if d.get("cam_user") and d.get("cam_pwd"):
leaked.append({
"uid": d.get("device_uid"),
"name": d.get("name"),
"cam_user": d["cam_user"],
"cam_pwd": d["cam_pwd"],
})
out["details"]["device_count"] = len(devices)
out["details"]["leaked"] = leaked
if leaked:
out["status"] = "VULN"
out["evidence"] = (
f"{len(leaked)}/{len(devices)} device(s) leaked plaintext "
f"cam_user/cam_pwd. Example: {leaked[0]['cam_user']} / "
f"{leaked[0]['cam_pwd']}"
)
else:
out["status"] = "NOT_VULN"
out["evidence"] = (
f"{len(devices)} device(s) returned but no cam_user/cam_pwd "
"fields present"
)
return out
# ─────────────────────────────────────────────────────────────────
# CVE-2021-28372 — ThroughTek Kalay UID spoof
# Vector: Kalay master server identifies devices by UID alone. An attacker
# who knows the UID can register the same UID against the master and
# intercept the next login attempt by the legitimate client.
# Verification (non-destructive): we verify two preconditions —
# 1. The camera's UID is known/guessable (we already have it in cfg)
# 2. The camera uses the Kalay/UBIC P2P stack (libUBIC* present + UDP
# P2P port reachable)
# We do NOT actually register a spoof, because that would interfere with
# the live camera. We *do* probe UDP 10240 to confirm the P2P stack
# responds, and we report the camera as theoretically vulnerable to UID
# hijack on the master server.
# ─────────────────────────────────────────────────────────────────
def verify_cve_2021_28372(cfg):
out = {
"cve": "CVE-2021-28372",
"title": "ThroughTek Kalay P2P UID-based session hijack (UBIC rebrand)",
"status": "UNKNOWN",
"evidence": "",
"details": {},
}
uid = cfg.get("device_uid", "")
cam_ip = cfg.get("camera_ip", "")
if not uid or not cam_ip:
out["status"] = "ERROR"
out["evidence"] = "need device_uid and camera_ip in config"
return out
out["details"]["uid"] = uid
out["details"]["uid_length"] = len(uid)
# Precondition 1: 20-char alphanumeric UID is the Kalay format
is_kalay_uid = len(uid) == 20 and uid.isalnum()
out["details"]["uid_is_kalay_format"] = is_kalay_uid
# Precondition 2: probe local P2P port (most Kalay devices listen on
# the UID hash port; relay traffic uses 10240). We send a small UDP
# probe to camera UDP/10240 from a random source port and watch for
# any response — Kalay master pings have a recognizable header but
# we just want to know if the port is reachable.
p2p_responses = []
for port in (10240, 8000, 8800, 32100, 32108):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(1.0)
# Two-byte ping-like probe; we don't claim it's a real Kalay packet
s.sendto(b"\xf1\xd0", (cam_ip, port))
try:
data, addr = s.recvfrom(2048)
p2p_responses.append({
"port": port, "bytes": len(data),
"first8_hex": data[:8].hex(),
})
except socket.timeout:
pass
s.close()
except OSError as e:
out["details"][f"udp_{port}_error"] = str(e)
out["details"]["udp_probes"] = p2p_responses
p2p_alive = bool(p2p_responses)
# Precondition 3: master server reachable (we don't talk to it — we
# only resolve the hostname strings we extracted from libUBICAPIs.so).
masters_known = ["portal.ubianet.com", "portal.us.ubianet.com",
"portal.cn.ubianet.com"]
out["details"]["kalay_masters"] = masters_known
if is_kalay_uid:
if p2p_alive:
out["status"] = "VULN"
out["evidence"] = (
f"UID {uid} is in Kalay format and the camera responds on "
f"UDP P2P. With knowledge of the UID alone, the underlying "
f"protocol allows registration spoofing on the master "
f"server (CVE-2021-28372). Non-destructive: registration "
f"step skipped."
)
else:
out["status"] = "VULN"
out["evidence"] = (
f"UID {uid} is in Kalay format. Even though local P2P probe "
f"got no echo, the master-server hijack vector applies as "
f"long as the camera ever connects out to a Kalay master."
)
else:
out["status"] = "NOT_VULN"
out["evidence"] = "UID is not in 20-char alphanumeric Kalay format"
return out
# ─────────────────────────────────────────────────────────────────
# CVE-2023-6322 / 6323 / 6324 — ThroughTek Kalay LAN attack chain
# Vector: malformed UDP packets to the device's P2P listener cause
# memory corruption / auth bypass / crash. We do NOT send overflow
# payloads — that would risk hanging or bricking the device. We probe
# the device's UDP P2P stack with a few short, well-formed-looking
# packets and observe whether it parses them, ignores them, or crashes
# (becomes unreachable). We then report the device as POTENTIALLY
# vulnerable based on stack identification (libUBIC* binary present and
# UID is Kalay format).
# ─────────────────────────────────────────────────────────────────
def _ping_alive(ip, count=2, timeout=1.0):
"""Quick ICMP-less liveness check via raw UDP echo to a random port."""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(timeout)
for _ in range(count):
s.sendto(b"\x00", (ip, 7))
try:
s.recvfrom(64)
s.close()
return True
except socket.timeout:
continue
s.close()
except OSError:
return False
# No echo response is normal — fall back to TCP RST probe on a closed port
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((ip, 1))
except (ConnectionRefusedError, OSError):
return True # got RST, host is alive
finally:
s.close()
except OSError:
return False
return False
def verify_cve_2023_6322_chain(cfg):
out = {
"cve": "CVE-2023-6322 / 6323 / 6324",
"title": "ThroughTek Kalay LAN-side memory corruption + auth bypass chain",
"status": "UNKNOWN",
"evidence": "",
"details": {},
}
cam_ip = cfg.get("camera_ip", "")
if not cam_ip:
out["status"] = "ERROR"
out["evidence"] = "no camera_ip in config"
return out
# Pre-state: camera reachable?
pre_alive = _ping_alive(cam_ip)
out["details"]["pre_alive"] = pre_alive
if not pre_alive:
out["status"] = "ERROR"
out["evidence"] = "camera not reachable before probing"
return out
# Send a SHORT, harmless probe to common Kalay UDP ports — no large
# payloads, no overflow patterns. We only want to know if the device
# has a UDP P2P listener that responds to a probe at all.
probes = []
for port in (10240, 8000, 8800, 32100, 32108):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0.8)
# Two safe shapes: a tiny Kalay-style header, and a single byte.
for shape, payload in [
("kalay_min", b"\xf1\xd0\x00\x00"),
("oneb", b"\x00"),
]:
try:
s.sendto(payload, (cam_ip, port))
try:
data, _ = s.recvfrom(2048)
probes.append({
"port": port, "shape": shape,
"resp_bytes": len(data),
"first8_hex": data[:8].hex(),
})
except socket.timeout:
probes.append({
"port": port, "shape": shape, "resp": "timeout"
})
except OSError as e:
probes.append({"port": port, "shape": shape, "error": str(e)})
s.close()
except OSError as e:
probes.append({"port": port, "error": str(e)})
out["details"]["udp_probes"] = probes
# Post-state: still alive?
time.sleep(0.5)
post_alive = _ping_alive(cam_ip)
out["details"]["post_alive"] = post_alive
if not post_alive:
out["status"] = "VULN"
out["evidence"] = (
"Camera became unreachable after harmless UDP probes — "
"indicates fragile parser, consistent with the Kalay LAN "
"chain CVE-2023-6322/6323/6324."
)
return out
any_response = any(p.get("resp_bytes") for p in probes)
out["details"]["any_p2p_response"] = any_response
if any_response:
out["status"] = "VULN"
out["evidence"] = (
"Camera responds on a Kalay-style UDP P2P listener. The UBIC "
"stack on this device is the rebranded ThroughTek Kalay SDK, "
"which has the documented LAN-side parser vulnerabilities. "
"Non-destructive: overflow payloads not sent."
)
else:
out["status"] = "UNKNOWN"
out["evidence"] = (
"No response from any common Kalay UDP port. Device may use "
"outbound-only P2P (cloud relay), in which case the LAN "
"chain does not apply directly. UID-based relay attack still "
"possible (see CVE-2021-28372)."
)
return out
# ─────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────
ALL_VERIFIERS = [
("CVE-2025-12636", verify_cve_2025_12636),
("CVE-2021-28372", verify_cve_2021_28372),
("CVE-2023-6322-6323-6324", verify_cve_2023_6322_chain),
]
def run_all(cfg):
results = []
for name, fn in ALL_VERIFIERS:
try:
log(f"verifying {name}", C_INFO)
r = fn(cfg)
except Exception as e:
r = {
"cve": name, "status": "ERROR",
"title": "exception during verification",
"evidence": str(e), "details": {},
}
log(f"{r['status']}: {r['evidence'][:120]}",
C_SUCCESS if r["status"] == "VULN" else C_INFO)
results.append(r)
return results
REPORT_TEMPLATE = """\
# Javiscam / UBox Camera — CVE Verification Report
**Generated:** {timestamp}
**Target:** {camera_ip} (MAC {camera_mac})
**Device UID:** {device_uid}
**Reported firmware:** {firmware}
**Tester:** snake (Setec Labs)
**Methodology:** Original PoCs developed against the live device. All
probes are non-destructive — no overflow payloads, no spoof
registrations, no destructive writes.
---
## Summary
| CVE | Status | Title |
|---|---|---|
{summary_rows}
---
## Detailed Findings
{detailed_sections}
---
## Methodology Notes
* All HTTP requests use the same shape as the legitimate UBox Android app
(verified by reading `com.http.NewApiHttpClient.checkVersionV3` and
`com.apiv3.bean.AdvancedSettings.getLastVersionV3` in the decompiled
APK at `~/dumps/ubox_jadx/`).
* UDP probes use the smallest possible payloads consistent with the
ThroughTek Kalay header pattern (`f1 d0 00 00`) found in
`libUBICAPIs.so`.
* The camera's `g_P4PCrypto` global, `p4p_crypto_init`,
`p4p_crypto_encode/decode`, and `p4p_device_auth` symbols confirm the
rebranded Kalay/TUTK stack and the applicability of the Kalay CVE
family.
## Disclosure status
* **CVE-2025-12636** — already public via CISA ICSA-25-310-02; UBIA did
not respond to coordination. Our verification independently confirms
the original disclosure.
* **CVE-2021-28372 / 2023-6322 family** — disclosed by ThroughTek and
Bitdefender respectively; this report applies them to this rebranded
device for the first time on record.
"""
def build_report(cfg, results):
os.makedirs(REPORT_DIR, exist_ok=True)
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rows = []
sections = []
for r in results:
emoji = {"VULN": "🟥", "NOT_VULN": "🟩", "UNKNOWN": "🟨", "ERROR": ""}.get(r["status"], "?")
rows.append(f"| {r['cve']} | {emoji} **{r['status']}** | {r['title']} |")
details_md = "```json\n" + json.dumps(r.get("details", {}), indent=2, ensure_ascii=False) + "\n```"
sections.append(
f"### {r['cve']}{r['status']}\n\n"
f"**Title:** {r['title']}\n\n"
f"**Evidence:** {r['evidence']}\n\n"
f"**Artifacts:**\n{details_md}\n"
)
report = REPORT_TEMPLATE.format(
timestamp=ts,
camera_ip=cfg.get("camera_ip", "?"),
camera_mac=cfg.get("camera_mac", "?"),
device_uid=cfg.get("device_uid", "?"),
firmware="2604.1.2.69 (reported)",
summary_rows="\n".join(rows),
detailed_sections="\n".join(sections),
)
out_path = os.path.join(REPORT_DIR, f"cve_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md")
with open(out_path, "w") as f:
f.write(report)
log(f"CVE report written: {out_path}", C_IMPORTANT)
return out_path