""" 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