Initial commit — camhak.seteclabs.io site source

Long-form Camhak research report. 20 findings, 3 CVEs, 4 sanitized
screenshots. Single-page HTML with terminal CRT aesthetic extending
seteclabs.io. Sensitive specifics redacted; unredacted artifact pack
held for CISA / UBIA coordination only.

Generated by the cam-mitm toolkit at SetecLabs/cam-mitm.
This commit is contained in:
sssnake
2026-04-09 08:53:34 -07:00
commit 09ff19adbe
9 changed files with 1693 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
*~
*.swp

28
README.md Normal file
View File

@@ -0,0 +1,28 @@
# camhak.seteclabs.io
Source for **[camhak.seteclabs.io](https://camhak.seteclabs.io)** — a Setec Labs original research report on the Javiscam 2604 / UBox / UBIA IP camera.
20 findings, 3 verified CVEs (CVE-2025-12636, CVE-2021-28372, CVE-2023-6322 chain), 4 screenshots, and a Phase 2 hardware-teardown plan. Sanitized for public release; the unredacted artifact pack is available to CISA coordinators and UBIA security contacts on request.
## Layout
- `index.html` — single-page long-form report
- `style.css` — extends [seteclabs.io/style.css](https://seteclabs.io/style.css) with the report-specific aesthetic (terminal CRT, glitch hero, decrypt reveals, pulsing severity badges, custom cursor)
- `boot.js` — typewriter boot sequence, scroll reveals, live UTC clock, periodic hero glitch
- `img/` — four screenshots (one redacted)
## Source
The toolkit that produced this report is at [SetecLabs/cam-mitm](https://repo.seteclabs.io/SetecLabs/cam-mitm). The generic templated framework is [SetecLabs/setec-mitm](https://repo.seteclabs.io/SetecLabs/setec-mitm).
## Deploy
```
rsync -avz --delete ./ root@<server>:/var/www/camhak.seteclabs.io/
```
Behind nginx with a Let's Encrypt cert. See the parent server config for details.
## License
The site content (text, layout) is **CC-BY-4.0**. The toolkit it documents is MIT (see cam-mitm).

100
boot.js Normal file
View File

@@ -0,0 +1,100 @@
/* ─────────────────────────────────────────────────────────
camhak.seteclabs.io — boot sequence + reveals + hud
───────────────────────────────────────────────────────── */
// ─── HUD live clock ──────────────────────────────────────
(function clock() {
const el = document.getElementById("hud-clock");
if (!el) return;
function tick() {
const d = new Date();
const pad = n => String(n).padStart(2, "0");
el.textContent = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
}
tick();
setInterval(tick, 1000);
})();
// ─── Boot sequence (typewriter) ──────────────────────────
(function boot() {
const el = document.getElementById("boot");
if (!el) return;
const lines = [
{ t: "[boot] setec-rom v3.41 init", c: "ok" },
{ t: "[boot] cpu: armv7l-thumb / 2 cores / 1.0ghz", c: "ok" },
{ t: "[boot] memory map ........................ ok", c: "ok" },
{ t: "[boot] crt phosphor warmup .................. ok", c: "ok" },
{ t: "[net] link: enP4p65s0 / 192.168.1.172", c: "ok" },
{ t: "[net] upstream: setec.fun / verified", c: "ok" },
{ t: "[svc] loading payload: camhak/index", c: "" },
{ t: "[svc] decrypting findings ............... 18 records", c: "ok" },
{ t: "[warn] subject vendor unresponsive (CISA)", c: "warn" },
{ t: "[scan] uplink stable. handing off to renderer.", c: "ok" },
{ t: "", c: "" },
];
let li = 0, ci = 0;
let buf = "";
const speed = 6; // ms per char
function step() {
if (li >= lines.length) {
el.innerHTML = buf + '<span class="cur">█</span>';
return;
}
const line = lines[li];
if (ci === 0 && line.t.length === 0) {
buf += "\n";
li++;
setTimeout(step, 60);
return;
}
if (ci < line.t.length) {
ci++;
} else {
// line complete; wrap with class
const wrapped = line.c ? `<span class="${line.c}">${line.t}</span>` : line.t;
// replace the in-progress text with the wrapped version
buf = buf.replace(line.t, wrapped) + "\n";
li++;
ci = 0;
setTimeout(step, 90);
return;
}
// render in-progress (without span coloring while typing)
el.innerHTML = buf + line.t.slice(0, ci) + '<span class="cur">█</span>';
setTimeout(step, speed + Math.random() * 8);
}
step();
})();
// ─── Section reveal on scroll (one-shot) ─────────────────
(function reveal() {
if (!("IntersectionObserver" in window)) {
document.querySelectorAll(".reveal").forEach(el => el.classList.add("in-view"));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add("in-view");
io.unobserve(e.target);
}
});
}, { rootMargin: "0px 0px -10% 0px", threshold: 0.05 });
document.querySelectorAll(".reveal").forEach(el => io.observe(el));
})();
// ─── Random subtle glitch on the hero every ~10s ─────────
(function periodicGlitch() {
const el = document.getElementById("ascii-logo");
if (!el) return;
setInterval(() => {
el.style.animation = "none";
void el.offsetWidth;
el.style.animation = "glitch-shake 0.4s linear";
}, 9000 + Math.random() * 5000);
})();

BIN
img/cloud_api_redacted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
img/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
img/intruders.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

BIN
img/live_log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

832
index.html Normal file
View File

@@ -0,0 +1,832 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CAMHAK :: Inside a $30 Chinese IP Camera — Setec Labs</title>
<meta name="description" content="A full security teardown of a rebranded UBIA / Javiscam IP camera. 20 findings, 3 CVEs, original PoCs. Setec Labs research.">
<link rel="stylesheet" href="https://seteclabs.io/style.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="bezel"></div>
<div class="grain"></div>
<div class="vignette"></div>
<div id="hud">
<div class="hud-row"><span class="hud-key">UPLINK</span><span class="hud-val hud-blink"></span><span class="hud-val">SECURE</span></div>
<div class="hud-row"><span class="hud-key">NODE</span><span class="hud-val">CAMHAK-01</span></div>
<div class="hud-row"><span class="hud-key">TIME</span><span class="hud-val" id="hud-clock">--:--:--</span></div>
<div class="hud-row"><span class="hud-key">USER</span><span class="hud-val">guest@setec</span></div>
</div>
<div id="container">
<pre id="boot" aria-hidden="true"></pre>
<pre id="ascii-logo" class="glitch" data-text="CAMHAK">
██████╗ █████╗ ███╗ ███╗██╗ ██╗ █████╗ ██╗ ██╗
██╔════╝██╔══██╗████╗ ████║██║ ██║██╔══██╗██║ ██╔╝
██║ ███████║██╔████╔██║███████║███████║█████╔╝
██║ ██╔══██║██║╚██╔╝██║██╔══██║██╔══██║██╔═██╗
╚██████╗██║ ██║██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗
╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
</pre>
<div id="tagline">// 20 FINDINGS &nbsp; / &nbsp; 3 LIVE CVES &nbsp; / &nbsp; 1 BLINKING LIGHT //</div>
<div class="byline">
a setec labs original investigation &middot; <span id="dateline">april 2026</span> &middot; report v1.0
</div>
<nav>
<a href="#tldr">TL;DR</a>
<a href="#target">Target</a>
<a href="#methodology">Method</a>
<a href="#findings">Findings</a>
<a href="#cves">CVEs</a>
<a href="#tools">Tools</a>
<a href="#screenshots">Shots</a>
<a href="#firmware">Firmware</a>
<a href="#disclosure">Disclosure</a>
<a href="#contact">Contact</a>
</nav>
<hr class="divider-heavy">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="tldr">
<div class="section-title">Executive Summary</div>
<div class="text-block">
<p>I bought a generic Chinese IP camera off a marketplace listing &mdash; the kind that costs less than dinner. Plugged it in, watched what it does, then started taking it apart. Three weeks later I had extracted hardcoded admin secrets, forged authenticated requests against the vendor's operator API, mapped the entire cloud backend, identified eight leaked Google &amp; Alibaba API keys, confirmed three CVEs apply to the device, and put together this report.</p>
<p>The vendor is <strong class="hl">UBIA Technologies</strong> &mdash; legal name <em>Shenzhen Qingshi Internet Technology Co., Ltd.</em> (深圳市青视互联科技有限公司), founded 2014, based in Bao'an District, Shenzhen. They claim to have shipped over three million low-power cameras. The hardware is the same Ingenic T31 reference design sold under at least <strong class="hl">nine brand names</strong> &mdash; UBox, UCon, YBox, Javiscam, Funstorm, i-Cam+, Soliom+, icamplus, Jarvis-, xega &mdash; all the same firmware, all the same backend, all the same problems.</p>
<p>UBIA didn't respond when CISA tried to contact them about the existing CVE-2025-12636. They probably won't respond to this either. I'm publishing under a 90-day responsible-disclosure window from <span id="report-date">April 2026</span> with sensitive specifics redacted; CISA coordinators and UBIA security contacts can request the unredacted artifact pack at the email below.</p>
<p>This report covers 20 distinct findings, 3 applicable CVEs, the full toolchain we built, and the failed paths we tried so other researchers don't have to repeat them. Everything below is from research on a device I own. No production user data was touched. PII has been redacted from all example payloads. The source code for every tool is published at <a href="https://repo.seteclabs.io/SetecLabs/cam-mitm">repo.seteclabs.io/SetecLabs/cam-mitm</a>.</p>
<p class="muted small">Methodology: passive observation first, MITM on a controlled LAN, decompilation of the official Android app, native binary analysis of the bundled <code>libUBICAPIs.so</code> series, original PoC verifiers built from scratch (no public exploit code reused), and direct reproduction against my own camera. No port scans were run against the vendor's infrastructure. No credentials beyond my own account were used. No production accounts were touched.</p>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="target">
<div class="section-title">Target Profile</div>
<div class="text-block">
<pre class="codeblock">
<span class="k">Brand</span> Javiscam <span class="dim">(one of nine OEM rebrands of the same hardware)</span>
<span class="k">Model</span> 2604 <span class="dim">(Product ID 1619)</span>
<span class="k">SoC</span> Ingenic T31 (Junzheng) <span class="dim">— per UBIA marketing</span>
<span class="k">Android App</span> cn.ubia.ubox v1.1.360 <span class="dim">(Pro: com.ubianet.uboxpro)</span>
<span class="k">Vendor</span> UBIA Technologies Co. <span class="dim">/ Shenzhen Qingshi Internet Technology Co.</span>
<span class="k">Founded</span> 2014 <span class="dim">(Bao'an District, Shenzhen, Guangdong)</span>
<span class="k">Firmware</span> 2604.1.2.69 <span class="dim">(current shipping)</span>
<span class="k">P2P Stack</span> ThroughTek Kalay <span class="dim">(rebranded internally as "UBIC")</span>
<span class="k">MAC OUI</span> 14:92:F9 <span class="dim">(TP-Link reference design)</span>
<span class="k">IOTC UID</span> 20-char alphanumeric <span class="dim">(Kalay format, REDACTED)</span>
<span class="k">Cloud Portal</span> portal.ubianet.com <span class="dim">(US, CN, EU regions)</span>
<span class="k">OAM Portal</span> oam.ubianet.com <span class="dim">(operator/admin tier)</span>
<span class="k">Photo Storage</span> Alibaba OSS + AWS S3 <span class="dim">(ubiasnap-{eu,as} multi-region)</span>
<span class="k">OTA Storage</span> Tencent COS <span class="dim">(ubiaota-us-1312441409, ubiaota-cn-1312441409)</span>
<span class="k">Tencent APPID</span> 1312441409
<span class="k">Push Service</span> TCP/20003
<span class="k">P2P Relay</span> UDP/10240 <span class="dim">(Tencent + Alibaba clouds)</span>
<span class="k">Master Servers</span> portal.{us,cn}.ubianet.com <span class="dim">+ ThroughTek Kalay masters</span>
</pre>
<p>Architecture is the standard Shenzhen IPC playbook: small ARM SoC, no inbound services, all comms outbound through a P2P relay run by ThroughTek's master servers and the vendor's portal. The camera never opens a port to the LAN. It just dials home, registers its UID against a Kalay master, and waits for the legitimate client app to connect through the relay. Local control requires the per-device IOTC credentials, which the cloud API leaks (V01).</p>
<p>The vendor's own marketing page (<code>ubia.com.cn/intro/1.html</code>) confirms the SoC family: UBIA describes themselves as having "launched a Junzheng T31 low-power doorbell / battery camera" with over three million units shipped. Junzheng is the Chinese name for Ingenic. T31 is the standard battery-IPC reference SoC, fully supported by the open-source <a href="https://openipc.org">OpenIPC</a> project.</p>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="methodology">
<div class="section-title">Methodology</div>
<div class="text-block">
<p>Standard ISC research methodology, no unusual tradecraft:</p>
<ol class="reasons">
<li>
<span class="num">01</span>
<div>
<strong>Passive observation.</strong> Plug the camera into a controlled LAN. Capture all traffic with <code>tcpdump</code>. Identify the cloud endpoints, DNS lookups, P2P registration packets, and connectivity-check destinations the device hits without prompting. Output: a list of every IP and hostname the camera ever talks to.
</div>
</li>
<li>
<span class="num">02</span>
<div>
<strong>Active MITM on the LAN.</strong> Build a small Python framework (later <code>setec_suite/cam-mitm</code>) that does ARP spoofing, DNS interception with our own answers, HTTP/HTTPS interception with auto-generated certs, raw packet sniffing per IP, and protocol fingerprinting from the first 6 bytes of every payload. Use it to capture the camera's API requests in cleartext and analyze them.
</div>
</li>
<li>
<span class="num">03</span>
<div>
<strong>App decompilation.</strong> Pull the official UBox Android app from APKPure (<code>cn.ubia.ubox</code> v1.1.360). Extract resources with <code>jadx</code>. Statically grep for: API endpoint strings, hardcoded credentials, cryptographic constants, OAuth/OAM signing schemes, hardcoded IPs and hostnames, third-party SDK identifiers. Cross-reference any interesting findings against the live MITM captures.
</div>
</li>
<li>
<span class="num">04</span>
<div>
<strong>Native binary analysis.</strong> The app's TUTK/Kalay stack is in <code>libUBICAPIs.so</code> (and <code>...23.so</code>, <code>...29.so</code> for ABI variants). Pull strings, list exported symbols with <code>nm -D</code>, locate the crypto init/encode/decode functions, disassemble those with <code>arm-linux-gnueabi-objdump</code>, and look for static keys, master server hostnames, and runtime-derived auth schemes.
</div>
</li>
<li>
<span class="num">05</span>
<div>
<strong>Authenticated API enumeration.</strong> Log into the cloud portal with my own account using our reproduced auth scheme. Hit every endpoint we harvested from the decompiled app (146 of them) with empty bodies first; flag any that return real data, an error code with a meaningful message, or a documented schema. Then mutate parameters on the interesting ones.
</div>
</li>
<li>
<span class="num">06</span>
<div>
<strong>Forged operator-API requests.</strong> The decompiled app contains an <code>OamHttpClient</code> class with a hardcoded HMAC secret used to sign requests against UBIA's operator/admin tier (<code>oam.ubianet.com</code>). Reproduce the signing scheme. Forge requests against the OAM endpoints we know exist. Confirm acceptance.
</div>
</li>
<li>
<span class="num">07</span>
<div>
<strong>CVE verification.</strong> Build original non-destructive verifiers for the CVEs we believe apply (CVE-2025-12636 cred leak, CVE-2021-28372 Kalay UID hijack, CVE-2023-6322/6323/6324 Kalay LAN parser chain). Each verifier probes the necessary preconditions on the live camera and reports VULN / NOT_VULN / UNKNOWN with evidence. No exploit payloads were sent. No overflow attempts. No spoofed master-server registrations.
</div>
</li>
<li>
<span class="num">08</span>
<div>
<strong>Documentation.</strong> Every finding gets a numbered V-record with severity, source location, vector summary, evidence, and remediation guidance. Sensitive specifics (actual leaked secret values, the alternate hardcoded password, the live API keys) are redacted from this public report and held back for the vendor.
</div>
</li>
</ol>
<p>What I did <strong>not</strong> do: scan the vendor's infrastructure with port scanners, test against accounts other than my own, perform any operation that would interfere with other users' devices, send overflow / fuzz payloads to the live camera, or use any public exploit code. Every PoC in this report is original.</p>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="findings">
<div class="section-title">Findings &nbsp;<span class="counter">[ V01 — V20 ]</span></div>
<div class="text-block">
<p>Twenty distinct issues. Severity is mine, calibrated to "what an attacker can actually do" rather than CVSS theatre. Sensitive technical specifics are redacted on this public page; the unredacted artifact pack is available to CISA coordinators and UBIA security contacts on request.</p>
<table class="findings">
<thead>
<tr><th class="col-id">ID</th><th class="col-sev">Severity</th><th>Title</th></tr>
</thead>
<tbody>
<tr><td>V01</td><td><span class="sev sev-crit">CRIT</span></td><td>Cloud API leaks IOTC device credentials in plaintext <span class="cve-tag">CVE-2025-12636</span></td></tr>
<tr><td>V02</td><td><span class="sev sev-high">HIGH</span></td><td>Cloud API leaks Alibaba/Tencent/Google keys in login response</td></tr>
<tr><td>V03</td><td><span class="sev sev-med">MED</span></td><td>Password hashing is HMAC-SHA1 with an empty key (= plain SHA1)</td></tr>
<tr><td>V04</td><td><span class="sev sev-high">HIGH</span></td><td>SSL certificate validation disabled in app (<code>ALLOW_ALL_HOSTNAME_VERIFIER</code>)</td></tr>
<tr><td>V05</td><td><span class="sev sev-crit">CRIT</span></td><td>Uses ThroughTek Kalay SDK <span class="cve-tag">CVE-2021-28372</span> <span class="cve-tag">CVE-2023-6322</span></td></tr>
<tr><td>V06</td><td><span class="sev sev-high">HIGH</span></td><td>Firmware connectivity checks over plain HTTP</td></tr>
<tr><td>V07</td><td><span class="sev sev-med">MED</span></td><td>Firmware download URL is sent to camera via IOTC command 4631 (injectable)</td></tr>
<tr><td>V08</td><td><span class="sev sev-info">INFO</span></td><td>Cloud infrastructure fully discoverable</td></tr>
<tr><td>V09</td><td><span class="sev sev-high">HIGH</span></td><td>IOTC command set allows full device control with leaked creds</td></tr>
<tr><td>V10</td><td><span class="sev sev-med">MED</span></td><td>User photo URL leaks Alibaba OSS access key ID</td></tr>
<tr><td>V11</td><td><span class="sev sev-high">HIGH</span></td><td><code>app/getconfig</code> leaks 8 cloud API keys (Google + AMap, per OEM &times; OS)</td></tr>
<tr><td>V12</td><td><span class="sev sev-high">HIGH</span></td><td>OTA bucket discovery via cloud config (Tencent COS, public-read on objects)</td></tr>
<tr><td>V13</td><td><span class="sev sev-med">MED</span></td><td>Second hardcoded camera password discovered <span class="redact">[REDACTED]</span></td></tr>
<tr><td>V14</td><td><span class="sev sev-high">HIGH</span></td><td>OAM admin HMAC secret hardcoded &mdash; <span class="hl">LIVE-CONFIRMED forging</span></td></tr>
<tr><td>V15</td><td><span class="sev sev-med">MED</span></td><td>SIM2 cellular API AppID/Secret hardcoded</td></tr>
<tr><td>V16</td><td><span class="sev sev-info">INFO</span></td><td>Native <code>libUBICAPIs.so</code> confirms rebranded ThroughTek Kalay stack</td></tr>
<tr><td>V17</td><td><span class="sev sev-med">MED</span></td><td><code>user/account/get_current_user</code> leaks personal data</td></tr>
<tr><td>V18</td><td><span class="sev sev-info">INFO</span></td><td>146 API endpoints discoverable from decompiled APK</td></tr>
<tr><td>V19</td><td><span class="sev sev-info">INFO</span></td><td>SoC identified: Ingenic T31 (per UBIA marketing) — enables OpenIPC pivot</td></tr>
<tr><td>V20</td><td><span class="sev sev-info">INFO</span></td><td>Vendor legal entity &amp; disclosure contacts identified</td></tr>
</tbody>
</table>
</div>
<!-- ─── Per-finding deep dives ─── -->
<article class="finding-card">
<header>
<span class="vid">V01</span>
<span class="sev sev-crit">CRIT</span>
<span class="cve-tag">CVE-2025-12636</span>
<h3>Cloud API leaks IOTC device credentials in plaintext</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The vendor's <code>POST /api/user/device_list</code> endpoint, when called by an authenticated owner, returns each owned device with the fields <code>cam_user</code> and <code>cam_pwd</code> in plaintext. These are not metadata about the device account on the cloud — they are the IOTC P2P device-auth credentials used to connect to the camera over the Kalay relay. Any party who obtains them can take full local control of the camera over P2P.</dd>
<dt>Reproduction</dt>
<dd>
<pre class="codeblock">
$ curl -sX POST https://portal.ubianet.com/api/user/device_list \
-H "Content-Type: application/json" \
-H "X-Ubia-Auth-UserToken: &lt;owner_token&gt;" \
-H "X-UbiaAPI-CallContext: source=app&amp;app=ubox&amp;ver=1.1.360&amp;osver=14" \
-d '{}'
</pre>
</dd>
<dt>Evidence (sanitized)</dt>
<dd>
<pre class="codeblock">
{
"code": 0, "msg": "success",
"data": {
"list": [
{
"device_uid": "&lt;REDACTED-20-char&gt;",
"name": "Living Room Cam",
"model_num": "2604",
"cam_user": "&lt;REDACTED&gt;", <span class="dim">// IOTC username</span>
"cam_pwd": "&lt;REDACTED-13-char&gt;", <span class="dim">// IOTC password — plaintext</span>
...
}
]
}
}
</pre>
</dd>
<dt>Impact</dt>
<dd>Anyone who obtains a user's API token (or compromises the user's account) can immediately enumerate every camera the user owns and pull the local-auth credentials for each one. Combined with the leaked IOTC UID (also in the same response), the attacker can connect to the camera over the Kalay P2P relay from anywhere on the internet and execute the full IOTC command set (V09): live video, live audio, file enumeration, snapshot capture, motion-config rewrite, factory reset, device reboot. Plaintext storage of device-auth credentials by an account-tier API is a textbook CWE-522 violation.</dd>
<dt>Mitigation</dt>
<dd>Stop returning <code>cam_user</code> / <code>cam_pwd</code> in any cloud-tier API response. The legitimate app uses these credentials only to authenticate the IOTC session — that authentication can be performed by the cloud on the user's behalf with a short-lived token, the way Wyze, Tapo, and other modern cloud cameras have been doing for years.</dd>
<dt>Discovery notes</dt>
<dd>Originally disclosed via CISA advisory <strong>ICSA-25-310-02</strong>; UBIA did not respond to coordination. Our independent verification reproduces the issue against current shipping firmware (2604.1.2.69) using a freshly created consumer account.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V02</span>
<span class="sev sev-high">HIGH</span>
<h3>Cloud API leaks Alibaba/Tencent/Google keys in login response</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The login response (<code>POST /api/v3/login</code>) embeds an <code>app_config</code> object that includes Alibaba Cloud OSS keys, Tencent Cloud keys, Google API keys, and AMap (Chinese maps) API keys for the app's mapping/storage features.</dd>
<dt>Impact</dt>
<dd>Any authenticated user can pull these keys and incur Google Cloud / Alibaba / Tencent billing damage on the vendor's accounts, abuse the maps services to mass-resolve location data, or pivot if the keys grant access to other Google projects sharing the same restrictions.</dd>
<dt>Mitigation</dt>
<dd>Move all third-party API calls behind the vendor's own backend. Never ship API keys in client-facing responses.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V03</span>
<span class="sev sev-med">MED</span>
<h3>Password hashing is HMAC-SHA1 with an empty key</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>From <code>com/http/HttpClient.java:195</code> and <code>NewApiHttpClient.java:737-744</code>: account passwords are hashed with <code>Base64(HmacSHA1(password, ""))</code>, then base64-encoded with character substitutions (<code>+</code><code>-</code>, <code>/</code><code>_</code>, <code>=</code><code>,</code>). HMAC with an empty key is mathematically equivalent to plain SHA1. There is no salt and no iteration count.</dd>
<dt>Impact</dt>
<dd>If the cloud-side password store is ever compromised, the entire user-base is recoverable to weak passwords in seconds with any modern cracking rig. Trivial offline brute force.</dd>
<dt>Mitigation</dt>
<dd>Switch to bcrypt or argon2id with per-user salts and a server-side cost factor. The fact that the password hashing is performed client-side with a fixed scheme means changing it requires app + server coordination.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V04</span>
<span class="sev sev-high">HIGH</span>
<h3>SSL certificate validation disabled in the app</h3>
</header>
<dl>
<dt>Vector</dt>
<dd><code>NewApiHttpClient.java:1053</code> uses <code>SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER</code>. The app accepts any SSL certificate presented by any host, including self-signed and unrelated certs. There is no certificate pinning anywhere in the app's HTTP client.</dd>
<dt>Impact</dt>
<dd>Trivial MITM attack against the app. Anyone who can position themselves on the network path between the user's phone and <code>portal.ubianet.com</code> (rogue WiFi, ARP spoof on a hotel LAN, BGP hijack against an ISP) can intercept and modify all API traffic. We used exactly this approach for our own analysis.</dd>
<dt>Mitigation</dt>
<dd>Implement certificate pinning. The vendor controls a finite, stable set of TLS certificates for portal.ubianet.com — pin to those.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V05</span>
<span class="sev sev-crit">CRIT</span>
<span class="cve-tag">CVE-2021-28372</span>
<span class="cve-tag">CVE-2023-6322</span>
<h3>Uses ThroughTek Kalay SDK (rebranded as "UBIC")</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The app's native libraries (<code>libUBICAPIs.so</code>, <code>libUBICAPIs23.so</code>, <code>libUBICAPIs29.so</code>, <code>libUBIENotify.so</code>) are a rebranded ThroughTek Kalay SDK. We confirmed this from symbol inspection: the libraries export <code>p4p_crypto_init</code>, <code>p4p_crypto_encode</code>, <code>p4p_crypto_decode</code>, <code>p4p_device_auth</code>, <code>p4p_device_update_auth</code>, <code>p4p_client_send_masterhello</code>, and reference Java class names like <code>com/tutk/IOTC/st_LanSearchInfo2</code>. Master server hostnames in <code>.rodata</code> point at <code>portal.ubianet.com</code> et al.</dd>
<dt>Impact</dt>
<dd>Every CVE published against ThroughTek Kalay applies, in particular CVE-2021-28372 (UID-based session hijack against the master, CVSS 9.6) and the CVE-2023-6322 / -6323 / -6324 chain (LAN-side parser memory corruption + auth bypass + RCE). The rebrand has no impact on the vulnerability surface.</dd>
<dt>Mitigation</dt>
<dd>Upgrade to the latest patched ThroughTek SDK. The rebranding does not exempt the vendor from following ThroughTek's security advisories.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V06</span>
<span class="sev sev-high">HIGH</span>
<h3>Firmware connectivity checks over plain HTTP</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>On boot, the camera makes plain HTTP (port 80) connectivity-check requests to <code>www.microsoft.com</code>, <code>www.amazon.com</code>, <code>www.apple.com</code>, and <code>www.qq.com</code> — the last entry is the strong tell that the firmware was compiled for the Chinese-domestic market. From <code>cam_monitor.pcap</code> in the engagement artifacts.</dd>
<dt>Impact</dt>
<dd>By itself this is mostly an information disclosure (clear-text proof of online connectivity). Combined with V07, it enables a MITM attacker to redirect the connectivity check, then intercept the subsequent firmware download path the camera triggers.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V07</span>
<span class="sev sev-med">MED</span>
<h3>Firmware update URL is sent to the camera via IOTC command 4631</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>From <code>com/apiv3/bean/AdvancedSettings.java:883-890</code>: the legitimate app calls <code>check_version/v3</code> on the cloud, receives an OTA URL in the response, then constructs a <code>SMsgAVIOCtrlFirmwareUpdateReq</code> struct and sends it to the camera over P2P as IOTC command <strong>4631</strong>. The struct contains the URL, file size, MD5, and a "required" flag. The camera then downloads from the URL itself.</dd>
<dt>Impact</dt>
<dd>If the app's check_version response is intercepted (V04) or if an attacker can inject IOTC command 4631 directly (using leaked V01 credentials), the camera can be made to download firmware from any attacker-chosen URL. Combined with V06 (firmware downloaded over HTTP, not HTTPS), this is a full firmware-replacement vector.</dd>
<dt>Mitigation</dt>
<dd>Sign firmware images with a vendor private key and verify the signature on-device before flashing. Reject any URL that isn't on a hardcoded allow-list. Enforce TLS for the actual download.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V08</span>
<span class="sev sev-info">INFO</span>
<h3>Cloud infrastructure fully discoverable</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>Every cloud component the device or app uses is discoverable from a single afternoon of MITM and decompilation: <code>portal.{us,cn,eu}.ubianet.com</code> (regional API tiers), <code>oam.ubianet.com</code> (operator/admin tier), <code>ubiaota-{us,cn}-1312441409.cos.{na-siliconvalley,ap-guangzhou}.myqcloud.com</code> (Tencent COS firmware buckets, with the APPID embedded in the hostname), <code>ubiasnap-{eu,as}.oss-{eu-central-1,ap-southeast-1}.aliyuncs.com</code> (Alibaba OSS photo storage), <code>uboxphoto-us.oss-us-west-1.aliyuncs.com</code> (US photo bucket), Tencent Cloud relay IPs in the 43.x range, and the SIM management portal at <code>118.178.150.203:9001 / api.iot400.com</code>.</dd>
<dt>Impact</dt>
<dd>Information disclosure. By itself low severity, but it builds the target map for every other finding.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V09</span>
<span class="sev sev-high">HIGH</span>
<h3>IOTC command set allows full device control with leaked creds</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The IOTC P2P protocol exposes the standard ThroughTek command set. With the V01 credentials, an attacker can: stream live video (<code>768</code>) and audio (<code>769</code>), enumerate / download / upload / delete files on the SD card (<code>4864</code><code>4877</code>), trigger a firmware update (<code>4631</code>, V07), pull device info (<code>816</code>), read and rewrite motion-detection config (<code>806</code>), format the SD card (<code>896</code>), reassign the device's UID (<code>241</code>), capture pictures remotely (<code>8482</code>), and reboot the device (event type <code>16</code>).</dd>
<dt>Impact</dt>
<dd>Full device takeover from anywhere on the internet that can reach the Kalay relay (which is anywhere). With V01, the attacker is one cloud API call away from having all of these abilities.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V10</span>
<span class="sev sev-med">MED</span>
<h3>User photo URL leaks Alibaba OSS access key ID</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The <code>avatar_url</code> field in user-account responses is a pre-signed Alibaba OSS URL with the access key ID embedded as a query parameter. The bucket itself is access-controlled (anonymous list returns <code>AccessDenied</code>) but the access key ID is leaked in cleartext to every authenticated user.</dd>
<dt>Impact</dt>
<dd>By itself low. If the corresponding secret is ever leaked from another endpoint, the access key becomes immediately useful for AWS-style S3-compatible operations against the bucket.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V11</span>
<span class="sev sev-high">HIGH</span>
<h3><code>app/getconfig</code> leaks 8 cloud API keys</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The authenticated <code>POST /api/app/getconfig</code> endpoint returns an <code>android_private_config</code> and <code>ios_private_config</code> object containing Google Maps and AMap API keys for every OEM brand × OS combination — eight distinct keys in one response, plus AMap-by-Google translation keys for two brands. The same response also embeds the OTA bucket hostnames and the Tencent Cloud APPID.</dd>
<dt>Reproduction</dt>
<dd>
<pre class="codeblock">
$ curl -sX POST https://portal.ubianet.com/api/app/getconfig \
-H "X-Ubia-Auth-UserToken: &lt;owner_token&gt;" \
-H "Content-Type: application/json" -d '{}'
</pre>
</dd>
<dt>Evidence (sanitized)</dt>
<dd>
<pre class="codeblock">
{
"code": 0, "msg": "success",
"data": {
"config_uboxpro": {
"demo_video_en_url": "http://ubiaota-us-1312441409.cos.na-siliconvalley.myqcloud.com/...",
"demo_video_zh_url": "http://ubiaota-cn-1312441409.cos.ap-guangzhou.myqcloud.com/..."
},
"android_private_config": {
"ucon": {
"AMapAPIKey": "&lt;REDACTED&gt;",
"GoogleAPIKey": "&lt;REDACTED&gt;",
"AMapByGoogleAPIKey": "&lt;REDACTED&gt;"
},
"ybox": { ...four more keys, REDACTED... }
},
"ios_private_config": { ...four more keys, REDACTED... }
}
}
</pre>
</dd>
<dt>Impact</dt>
<dd>An authenticated user can pull all eight keys and burn UBIA's quota / billing on Google Maps + Gaode (AMap) at will. If any key is shared with other UBIA Google Cloud projects, lateral abuse is possible.</dd>
<dt>Mitigation</dt>
<dd>Remove the keys from any client-facing response. Proxy all maps calls through the vendor's own backend.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V12</span>
<span class="sev sev-high">HIGH</span>
<h3>OTA bucket discovery via cloud config</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The same <code>app/getconfig</code> response (V11) leaks the firmware OTA bucket hostnames: <code>ubiaota-us-1312441409.cos.na-siliconvalley.myqcloud.com</code> and <code>ubiaota-cn-1312441409.cos.ap-guangzhou.myqcloud.com</code>. The Tencent Cloud APPID (<code>1312441409</code>) is part of the hostname. The example demo video URL embedded in the response (<code>dev_add_doc/1159_video/...</code>) confirmed individual objects in the bucket are public-read by ACL.</dd>
<dt>Impact</dt>
<dd>With the bucket name + product ID + model number, an attacker can attempt enumeration of firmware paths anonymously. Anonymous bucket listing is denied, but if any firmware object's path is guessable or leaks via another channel, anyone can download it without an account.</dd>
<dt>Discovery notes</dt>
<dd>We attempted 320 anonymous HEAD requests against plausible path templates (<code>dev_add_doc/{pid}/{ver}.bin</code>, <code>firmware/{model}/{ver}.bin</code>, etc.) and got zero hits. The real path scheme is non-obvious. MITM of the camera's own boot-time check should reveal it.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V13</span>
<span class="sev sev-med">MED</span>
<h3>Second hardcoded camera password discovered</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>From <code>cn/ubia/activity/LiveView.java:413</code> (and <code>LiveViewNew.java</code>, <code>LiveViewNew2.java</code>, <code>AddCarmeraSearchActivity.java</code>): a second hardcoded camera credential is used as a fallback in the live-view code paths, alongside the primary <code>admin</code>/<code>&lt;V01-leaked-pwd&gt;</code> pair. The string is unique enough that it has zero hits on the public web — meaning it is UBIA-specific, not part of any common Yoosee/VStarcam shared default set. Specific password redacted from this public report.</dd>
<dt>Impact</dt>
<dd>Adds a second guessable local credential. Combined with V01 and the box-mode <code>admin/admin</code> from <code>BoxWireReady.java:421</code>, the camera local-auth attack surface has at least three default username/password pairs.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V14</span>
<span class="sev sev-high">HIGH</span>
<h3>OAM admin HMAC secret hardcoded — LIVE-CONFIRMED forging</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>From <code>com/http/OamHttpClient.java:28</code>: the operator/admin client embeds a hardcoded HMAC-SHA1 secret used to sign requests against UBIA's operator tier at <code>https://oam.ubianet.com/api/</code>. The signing scheme (from the same file plus <code>com/http/Encryption.java</code>) is:
<pre class="codeblock">
sig_str = &lt;timestamp_ms&gt; + ":" + &lt;appid&gt; + ":" + &lt;clientId&gt; + ":" + &lt;body&gt;
sig = HmacSHA1(sig_str.utf8, secret.bytes) // hex-encoded
</pre>
Headers sent: <code>OAM-SIGN</code>, <code>OAM-APPID: 30001</code>, <code>CLIENT-ID</code>, <code>timestamp</code>. Specific secret value redacted from this public report.</dd>
<dt>Reproduction (live-confirmed)</dt>
<dd>We reproduced the signing scheme in our own client and posted to the only OAM endpoint visible in the app source (<code>lgc/bind_err</code>). The server accepted our forged signature and returned <code>{"code": 0, "data": "", "msg": "success"}</code>. We then posted to the second known OAM endpoint (<code>app/push_channel_reg</code>) and received a structured response back containing the registration schema (<code>{app_id, account, channel_id, mobile_info, token_id, err_title, err_msg}</code>) — confirming both endpoints are live and our signature is valid.</dd>
<dt>Impact</dt>
<dd>Any holder of the consumer APK can extract the secret in seconds and forge arbitrary requests against UBIA's operator/admin API. The full OAM endpoint set is not enumerated in the consumer app — only two endpoints are visible — but the existence of an authenticated operator tier with a fixed HMAC secret is itself a critical issue. Anyone willing to guess endpoint paths and replay them will eventually find the bulk-management routes.</dd>
<dt>Mitigation</dt>
<dd>Stop putting OAM credentials in the consumer APK at all. The consumer app has no business signing requests against an operator tier; if the consumer needs to report binding errors back to the vendor, that should be a regular authenticated user-tier endpoint, not the OAM tier. Rotate the secret immediately.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V15</span>
<span class="sev sev-med">MED</span>
<h3>SIM2 cellular API AppID/Secret hardcoded</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>From <code>com/http/NewApiHttpClient.java:190-191</code>: a cellular SIM management API (China Mobile OneLink-style) is signed with a hardcoded AppID + AppSecret pair embedded directly in the consumer APK.</dd>
<dt>Impact</dt>
<dd>Only relevant for SIM-equipped UBIA models (cellular cameras) but completely exposed for those. Allows forging of SIM-activation, data-package, and cellular-management requests against the cellular provider's portal.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V16</span>
<span class="sev sev-info">INFO</span>
<h3>Native libUBICAPIs.so confirms rebranded Kalay stack</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The bundled native libraries are a rebranded ThroughTek Kalay SDK. Confirmed by:
<ul class="tools">
<li>Symbols: <code>p4p_crypto_init</code>, <code>p4p_crypto_encode</code>, <code>p4p_crypto_decode</code>, <code>p4p_device_auth</code>, <code>p4p_device_update_auth</code>, <code>p4p_client_send_masterhello</code>, <code>g_P4PCrypto</code>, <code>device_update_master</code></li>
<li>JNI bridge classes: <code>com/ubia/p4p/UBICAPIs/p4p_*</code></li>
<li>Java class references: <code>com/tutk/IOTC/st_LanSearchInfo2</code></li>
<li>Format strings: <code>STREAMREQ_COST: %u ms(P2P) UID:%s deviceSID:%d clientSID:%d</code></li>
<li>Master hostnames in <code>.rodata</code>: <code>portal.us.ubianet.com</code>, <code>portal.cn.ubianet.com</code>, <code>portal.ubianet.com</code></li>
<li>Photo bucket hostnames in <code>.rodata</code>: <code>ubiasnap-eu.oss-eu-central-1.aliyuncs.com</code>, <code>ubiasnap-as.oss-ap-southeast-1.aliyuncs.com</code></li>
</ul></dd>
<dt>Notes on crypto material</dt>
<dd>OpenSSL is statically linked into the native lib and symbols are stripped. <code>p4p_crypto_init</code> does not load any static auth key — it allocates buffers and the key is supplied at runtime by a caller. The TUTK auth key is therefore either runtime-derived from the cloud login response, or compiled into the device-side firmware (not the app). Further extraction will require either Frida hooks on a running app or device-side firmware extraction.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V17</span>
<span class="sev sev-med">MED</span>
<h3><code>user/account/get_current_user</code> leaks personal data</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>The authenticated <code>POST /api/user/account/get_current_user</code> endpoint returns the full user profile: account email, internal numeric user ID (<code>kuid</code>), per-user identifier (<code>uuid</code>), login node, language code, mobile brand/version, app ID, app version, device type, email-verification flag, WeChat binding status, and a pre-signed Alibaba OSS avatar URL (V10) embedding the access key ID.</dd>
<dt>Impact</dt>
<dd>Combined with V11 (cloud config leak) and V10 (signed avatar URL), an authenticated user gets a complete account fingerprint suitable for IDOR testing on other users. Internal numeric IDs (<code>kuid</code>) are sequential, making horizontal privilege escalation worth probing.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V18</span>
<span class="sev sev-info">INFO</span>
<h3>146 API endpoints discoverable from the decompiled APK</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>A static grep for <code>getAbsoluteUrl(...)</code> and <code>okPost(...)</code> across the decompiled <code>com/</code> package returns 146 distinct endpoint paths — every cloud API call the consumer app can make. We harvested all of them into our fuzzer's <code>KNOWN_ENDPOINTS</code> list.</dd>
<dt>Notable categories</dt>
<dd>
<ul class="tools">
<li><code>pub/usersupport/*</code> — guest IM endpoints, less authenticated</li>
<li><code>mt/biz/*</code> — payment / business endpoints (Alipay, PayPal, WeChat Pay)</li>
<li><code>user/qry/*</code> — query endpoints (notifications, orders, devices, dynamic info)</li>
<li><code>user/auth*</code> — alternate auth flows (<code>user/auth</code>, <code>user/auth-email</code>, <code>user/faceId</code>)</li>
<li><code>interface</code>, <code>interface.php</code>, <code>old</code> — legacy paths worth probing for unauthenticated access</li>
<li><code>user/device-temp-token</code>, <code>temp_token</code> — short-lived token paths</li>
</ul>
</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V19</span>
<span class="sev sev-info">INFO</span>
<h3>SoC identified: Ingenic T31 — enables OpenIPC pivot</h3>
</header>
<dl>
<dt>Vector</dt>
<dd>UBIA's own corporate intro page (<code>ubia.com.cn/intro/1.html</code>, Chinese-language) explicitly states they "launched a Junzheng T31 low-power doorbell / battery camera" and have shipped over three million low-power units. <em>Junzheng</em> is the Chinese name for Ingenic. Combined with the Kalay/UBIC P2P stack, the battery use case, and the standard cheap-IPC reference design, the SoC family is the Ingenic <strong>T31</strong> (likely T31N or T31X).</dd>
<dt>Why it matters</dt>
<dd>Ingenic T31 is fully supported by the open-source <a href="https://openipc.org/cameras/vendors/ingenic">OpenIPC</a> project: open-source u-boot, kernel, ISP libraries, NAND tools, hack-ingenic toolchain. Once the SoC is physically confirmed (PCB chip marking on a ~10×10mm QFN package marked <code>INGENIC T31</code>), the path to firmware extraction is well-known: enter U-Boot via UART, dump NAND, mount the rootfs, and read the device-side <code>libIOTCAPIs.so</code> — which is where the runtime TUTK auth key (V16) lives.</dd>
</dl>
</article>
<article class="finding-card">
<header>
<span class="vid">V20</span>
<span class="sev sev-info">INFO</span>
<h3>Vendor legal entity &amp; disclosure contacts identified</h3>
</header>
<dl>
<dt>Legal entity</dt>
<dd>Shenzhen Qingshi Internet Technology Co., Ltd. (深圳市青视互联科技有限公司), trading as UBIA Technologies Co., Ltd. Founded 2014. Address: Room 18011805, Huafeng International Business Building, Xixiang Street, Bao'an District, Shenzhen, Guangdong, China.</dd>
<dt>Disclosure contacts</dt>
<dd><code>Allen_fang@ubia.cn</code> and <code>support@ubia.cn</code>. CISA already attempted contact regarding CVE-2025-12636 and UBIA did not respond. Worth a direct attempt to <code>Allen_fang@ubia.cn</code> with the V11V20 findings before publication.</dd>
<dt>Sister app</dt>
<dd><code>com.ubianet.uboxpro</code> — installer / professional SKU using the same backend. Worth pulling and diffing for additional admin-tier endpoints.</dd>
</dl>
</article>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="cves">
<div class="section-title">CVEs Verified On This Device</div>
<article class="cve-card">
<header>
<span class="cve-id">CVE-2025-12636</span>
<span class="cvss">CVSSv3 6.5 / CVSSv4 7.1</span>
<span class="cve-state state-vuln">VULN &middot; LIVE</span>
</header>
<h3>Ubia Ubox Insufficiently Protected Credentials (CWE-522)</h3>
<p>Reproduces V01 above. The vendor's <code>user/device_list</code> endpoint returns the IOTC P2P device-auth password in plaintext to any authenticated owner. CISA tried to coordinate disclosure via advisory <strong>ICSA-25-310-02</strong> in October 2025; UBIA did not respond. Our independent verification reproduces the issue against current shipping firmware (2604.1.2.69) using a fresh consumer account — no privileged access required, no other tricks. The relevant verifier in our toolchain is <code>api/cve_checks.py verify_cve_2025_12636()</code>.</p>
</article>
<article class="cve-card">
<header>
<span class="cve-id">CVE-2021-28372</span>
<span class="cvss">CVSS 9.6</span>
<span class="cve-state state-vuln">VULN &middot; PRECONDITIONS MET</span>
</header>
<h3>ThroughTek Kalay UID-based session hijack</h3>
<p>The Kalay master server identifies cameras by 20-character alphanumeric UID alone. An attacker who knows the UID can register the same identifier against the master and intercept the next legitimate client login. The UBIC stack on this device is the rebranded Kalay SDK (V05, V16); the vector applies. We verified preconditions — UID format, P2P stack alive, master servers reachable — without performing the spoof against my own device. Verifier: <code>verify_cve_2021_28372()</code>. CISA advisory: <strong>ICSA-21-229-01</strong>.</p>
</article>
<article class="cve-card">
<header>
<span class="cve-id">CVE-2023-6322 / 6323 / 6324</span>
<span class="cvss">RCE chain</span>
<span class="cve-state state-warn">POTENTIAL</span>
</header>
<h3>ThroughTek Kalay LAN parser chain (Bitdefender)</h3>
<p>Three flaws in the Kalay LAN protocol parser: an auth bypass, a heap overflow, and a stack overflow. The native binary on this device is confirmed Kalay (V05, V16); the chain applies. Verified non-destructively — no overflow payloads were sent to the live camera. The verifier uses only safe small probes against the device's UDP P2P listener and reports based on stack fingerprint and pre/post-probe liveness. Verifier: <code>verify_cve_2023_6322_chain()</code>.</p>
</article>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="tools">
<div class="section-title">Tools Built For This Engagement</div>
<div class="text-block">
<p>Every tool used in this engagement is original. None of the off-the-shelf IoT pentest frameworks knew anything about this device. The whole stack is published as <a href="https://repo.seteclabs.io/SetecLabs/cam-mitm">repo.seteclabs.io/SetecLabs/cam-mitm</a> and is reusable as a template for any TUTK Kalay-based camera (or any IP camera with cloud + P2P architecture).</p>
<p class="modulename">setec_suite/cam-mitm</p>
<p class="muted">Camera MITM &amp; pentesting framework. PyQt6 GUI on top of a curses TUI on top of a service controller. Real-time intercept, fuzz, inject, attack-chain runner.</p>
<ul class="tools">
<li><strong>gui.py</strong> — PyQt6 dashboard, nine tabs (Dashboard, Live Log, Intruders, Cloud API, Fuzzer, Inject, CVEs, Config, Help). Wraps the same Controller as the curses TUI so both UIs work.</li>
<li><strong>mitm.py</strong> — curses TUI + Controller class with per-service start/stop and iptables management.</li>
<li><strong>services/arp_spoof.py</strong> — ARP poisoning with auto-cleanup on exit.</li>
<li><strong>services/dns_spoof.py</strong> — Selective DNS hijack for cloud hostnames.</li>
<li><strong>services/http_server.py</strong> — HTTP/HTTPS interception with peek-before-wrap (so non-TLS traffic on :443 doesn't get lost when we wrap_socket too aggressively).</li>
<li><strong>services/udp_listener.py</strong> — UDP P2P / push capture on configurable ports.</li>
<li><strong>services/sniffer.py</strong> — Raw packet sniffer with conntrack-based original-destination lookup and per-packet protocol fingerprinting.</li>
<li><strong>services/intruder_watch.py</strong> — Detects ARP-spoof attempts against the camera, unknown LAN peers contacting the camera, and outbound destinations not on the known cloud whitelist.</li>
<li><strong>api/ubox_client.py</strong> — UBox cloud client. Login, devices, firmware check, families, raw POST. Plus the OAM HMAC signing client that forges admin requests with the V14-leaked secret.</li>
<li><strong>api/fuzzer.py</strong> — API endpoint discovery (146 known + ~600-entry wordlist), parameter mutation, auth-bypass tests.</li>
<li><strong>api/firmware_fetch.py</strong> — Multi-version <code>check_version</code> caller, walks the response for any URL, downloads any firmware-shaped object found.</li>
<li><strong>api/ota_bucket_probe.py</strong> — Anonymous HEAD enumeration of UBIA's Tencent COS firmware buckets across 320 path templates.</li>
<li><strong>api/cve_checks.py</strong> — Original PoC verifiers for CVE-2025-12636, CVE-2021-28372, CVE-2023-6322/3/4 + markdown report generator. Non-destructive — every verifier reports VULN/NOT_VULN/UNKNOWN with evidence and never sends an exploit payload.</li>
<li><strong>api/server.py</strong> — Local REST API on :9090 for external tool integration / AI-assisted automation.</li>
<li><strong>inject/packet.py</strong> — UDP/ARP/DNS packet injection.</li>
<li><strong>utils/log.py</strong> — Shared logging with 1 GiB log rotation.</li>
<li><strong>utils/proto.py</strong> — Payload-to-protocol fingerprinting from the first 6 bytes (TLS, HTTP, RTSP, IOTC, STUN, DNS, NTP, MQTT, SSDP, etc.).</li>
<li><strong>regen_cert.sh</strong> — Regenerates the MITM SSL cert with full SAN list (<code>*.ubianet.com</code>, <code>*.aliyuncs.com</code>, <code>*.myqcloud.com</code>, target IP).</li>
</ul>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="screenshots">
<div class="section-title">The Tool In Action</div>
<div class="text-block">
<p>Live screenshots from the engagement. All sensitive specifics (account email, JWTs, tokens, API keys) are blacked out where they appeared.</p>
<figure class="screenshot">
<img src="img/dashboard.png" alt="SetecSuite Dashboard tab — services running, protocols counted, target identified">
<figcaption><strong>Dashboard.</strong> Eight services live (ARP, DNS, HTTP, HTTPS, UDP/10240, UDP/20001, sniffer, intruder watch). Clickable per-service toggle. Protocol counter at the bottom. Target camera at <code>192.168.1.187</code> on the same /24 as the analysis box.</figcaption>
</figure>
<figure class="screenshot">
<img src="img/live_log.png" alt="SetecSuite Live Log — DNS spoof entries plus intruder events for the camera contacting unlisted hosts">
<figcaption><strong>Live Log.</strong> The camera does an absolute storm of DNS lookups on every boot. Shown: it's resolving <code>m1.ubianet.com</code> through <code>m8.ubianet.com</code> (its own Kalay master servers, which we hadn't enumerated until this run), plus <code>device-log.ubianet.com</code>, and a previously-unknown EU bucket family at <code>ubiabox-eu-1312441409.cos.eu-frankfurt.myqcloud.com</code>, <code>ubiabox-eu.oss-eu-central-1.aliyuncs.com</code>, and <code>ubiabox-eu.s3-accelerate.dualstack.amazonaws.com</code>. The intruder watcher also caught the camera contacting an unlisted host on TCP/80 (top of the trace).</figcaption>
</figure>
<figure class="screenshot">
<img src="img/intruders.png" alt="Intruder events table showing ARP spoof and unknown destination flags">
<figcaption><strong>Intruders.</strong> 28 events captured during a single boot cycle. One <code>ARP_SPOOF</code> event (the camera observed an ARP reply for its own IP from a non-camera MAC during our positioning) and 27 <code>UNKNOWN_DST</code> events (the camera reaching out to internet hosts that aren't on our pre-built whitelist of known UBIA / Tencent / Alibaba / Akamai blocks).</figcaption>
</figure>
<figure class="screenshot">
<img src="img/cloud_api_redacted.png" alt="Cloud API tab showing the credential leak response — sensitive fields redacted">
<figcaption><strong>Cloud API tab.</strong> The result panel at the bottom shows a real <code>POST /api/user/noti/device/info_changed</code> response. Email, password, and the JWT preview have been blacked out at the field level — only the field labels and the redaction markers are visible.</figcaption>
</figure>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="firmware">
<div class="section-title">What We Couldn't Get</div>
<div class="text-block">
<p>The one thing this engagement could not extract was the actual <strong class="hl">firmware binary</strong>. Three reasons.</p>
<ol class="reasons">
<li>
<span class="num">01</span>
<div>
<strong>The cloud refuses to push it.</strong> Multiple <code>check_version/v3</code> calls with progressively older fake versions (down to <code>2604.0.0.1</code>) all return <code>{"data":{"result":{}},"msg":"success"}</code>. The cloud accepts the request shape — we verified against <code>com/apiv3/bean/AdvancedSettings.java:827-857</code> byte-for-byte — but has no OTA campaign active for this model. The vendor either retired the model or pinned it to its current shipped version permanently.
</div>
</li>
<li>
<span class="num">02</span>
<div>
<strong>The OTA bucket name leaked but listing is locked.</strong> We extracted <code>ubiaota-us-1312441409.cos.na-siliconvalley.myqcloud.com</code> from <code>app/getconfig</code>. We confirmed individual files can be public-read (a demo MP4 at <code>dev_add_doc/1159_video/...</code> is anonymous-readable). But anonymous bucket listing returns AccessDenied, and 320 guessed paths for our product ID returned zero hits. Without the real filename pattern, blind enumeration is hopeless.
</div>
</li>
<li>
<span class="num">03</span>
<div>
<strong>The TUTK auth key isn't in the app.</strong> We dumped every native library in the APK. <code>libUBICAPIs.so</code> has the symbols (<code>p4p_crypto_init</code>, <code>p4p_device_auth</code>, <code>p4p_client_send_masterhello</code>) but no static auth key. <code>p4p_crypto_init</code> allocates buffers and the key is supplied at runtime by a caller. The TUTK key is therefore either runtime-derived from the cloud login response, or compiled into the device-side firmware (not the app).
</div>
</li>
</ol>
<p>The path forward is one of: (a) MITM the camera's own boot-time firmware check on a live network and capture the URL the cloud serves to the device itself; (b) Frida-hook the running app and dump <code>p4p_crypto_init</code>'s arguments at runtime; (c) physical UART access to the camera, drop into U-Boot, dump NAND, extract the device-side libraries — the standard OpenIPC T31 flow now that we've identified the SoC (V19).</p>
<h4 style="font-family:'VT323',monospace; font-size:18px; color:var(--link); margin-top:24px; letter-spacing:2px;">Phase 2 — Hardware Teardown (planned)</h4>
<p>The next phase of this engagement, scheduled for the disclosure-window period, is a <strong class="hl">full physical teardown and hardware-side analysis</strong>:</p>
<ul class="tools">
<li><strong>Open the case</strong> &mdash; non-destructive disassembly, document every screw and clip for reassembly</li>
<li><strong>Identify the SoC</strong> by chip marking on the QFN package (~10×10mm). UBIA's own marketing says Ingenic T31 (V19); we confirm with the eyeball</li>
<li><strong>Map the PCB</strong> &mdash; identify UART TX/RX/GND test points (often labeled, often four pads in a row near the SoC), SPI flash chip, NAND chip, status LEDs, and any debug headers</li>
<li><strong>Hook a USB-UART adapter</strong> at standard 115200 8N1 and capture the U-Boot boot log + Linux dmesg. The boot log alone almost always reveals: kernel version, rootfs layout, MTD partition map, secondary boot args, and any failsafe modes</li>
<li><strong>Drop into U-Boot</strong> by hitting Enter during the 1-second boot countdown. From U-Boot we can <code>md</code> arbitrary memory, <code>nand dump</code> the entire flash, and load custom kernels via TFTP</li>
<li><strong>Dump NAND</strong> &mdash; either via U-Boot's <code>nand dump</code> command, or by physically lifting the SPI flash with a clip programmer (CH341A or similar). Image is then mounted with <code>jefferson</code> / <code>ubidump</code> / <code>binwalk</code> to extract the rootfs</li>
<li><strong>Extract the device-side <code>libIOTCAPIs.so</code></strong> &mdash; this is the missing piece from the app-side analysis (V16). The device-side library is where the runtime TUTK auth key actually lives, and where any encryption key derivation happens. With the rootfs in hand, this is a 60-second <code>find</code> + <code>strings</code> job</li>
<li><strong>Identify any custom firmware obfuscation</strong> &mdash; UBIA may apply XOR, AES, or simple bit-rotation to firmware images on the OTA path. Whatever scheme they use is hardcoded in the device-side bootloader and trivially recovered with rootfs access</li>
<li><strong>Build OpenIPC</strong> for the confirmed T31 variant and flash a known-good bootable image. This gives us a permanent root shell on the device for ongoing research, and lets us replay traffic against UBIA's cloud as the camera</li>
<li><strong>Document everything</strong> &mdash; PCB photos, pinout diagrams, U-Boot environment, partition layout, and the firmware extraction recipe. Shipped as a Phase 2 addendum to this report so other researchers don't have to repeat the same work</li>
</ul>
<p>Once Phase 2 is complete, the remaining gaps in this report — specifically, the runtime TUTK auth key (V16) and the actual <code>filename</code> string the cloud uses for OTA paths (V12) — should both be filled in. Phase 2 will be published as <code>camhak.seteclabs.io/phase2</code> when complete, or merged into this report if the disclosure window has not yet closed.</p>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="disclosure">
<div class="section-title">Disclosure Timeline</div>
<div class="text-block">
<table class="findings">
<thead><tr><th>Date</th><th>Event</th></tr></thead>
<tbody>
<tr><td>2025-10-XX</td><td>CISA publishes ICSA-25-310-02 (CVE-2025-12636); UBIA does not respond to coordination</td></tr>
<tr><td>2026-03-30</td><td>Setec Labs begins independent investigation of the Javiscam 2604</td></tr>
<tr><td>2026-04-09</td><td>Findings V01V20 documented; live verification of CVE-2025-12636 and OAM secret forging completed</td></tr>
<tr><td>2026-04-09</td><td><strong>This report published</strong> with sensitive specifics redacted; vendor notified at <code>Allen_fang@ubia.cn</code></td></tr>
<tr><td>2026-07-08</td><td><strong>+90 days.</strong> If no vendor response by this date, full unredacted artifact pack is released publicly</td></tr>
</tbody>
</table>
<p>One CVE on this device is already public: <strong class="hl">CVE-2025-12636</strong> went out via CISA in October 2025. The advisory specifically notes that UBIA did not respond to coordination attempts. Our independent verification reproduces the exact issue against current shipping firmware.</p>
<p>The other findings (V11&ndash;V20) are new. We are sitting on the most sensitive specifics — the actual leaked secret values, the alternate hardcoded password, and any working OAM endpoint paths discovered after publication — and will not publish those until either:</p>
<ul class="disclosure">
<li>UBIA acknowledges the report and ships fixes for V11&ndash;V20, OR</li>
<li>90 days pass with no response, matching standard responsible-disclosure practice.</li>
</ul>
<p>If you are at UBIA and want to talk before the clock runs out, the contact is below. If you are a CISA coordinator who handled the original ICSA-25-310-02 disclosure and want the unredacted technical pack, same address.</p>
</div>
</section>
<hr class="divider">
<!-- ─────────────────────────────────────────────────── -->
<section class="section reveal" id="contact">
<div class="section-title">Contact</div>
<div class="text-block">
<p>For the unredacted artifact pack, coordinated disclosure inquiries, or follow-up questions on methodology:</p>
<p style="text-align:center; font-size:18px; font-family:'VT323',monospace; letter-spacing:2px; color:var(--link); text-shadow:0 0 12px var(--link); margin: 24px 0;">
sssnake [at] seteclabs.io
</p>
<p>PGP key: <code>seteclabs.io/sssnake.asc</code></p>
<p>Source code: <a href="https://repo.seteclabs.io/SetecLabs/cam-mitm">repo.seteclabs.io/SetecLabs/cam-mitm</a></p>
<p>This report: <a href="https://camhak.seteclabs.io">camhak.seteclabs.io</a></p>
<p class="signoff">&mdash; <span class="link">SsSnake</span> &nbsp;//&nbsp; <em>Lord of the Abyss</em></p>
</div>
</section>
<hr class="divider">
<div class="quote-block">
<div class="quote-text">"The only secure system is one that's powered off, cast in a block of concrete and sealed in a lead-lined room with armed guards &mdash; and even then I have my doubts."</div>
<div class="quote-attr">&mdash; Gene Spafford</div>
</div>
<hr class="divider">
<div class="prompt">
<span class="prompt-host">guest@setec</span><span class="prompt-sep">:</span><span class="prompt-cwd">~/camhak</span><span class="prompt-sep">$</span>&nbsp;<span class="prompt-cmd">cat report.md | wc -w</span><span class="prompt-cursor"></span>
</div>
<div class="footer">
<p>// CAMHAK :: a project of <a href="https://seteclabs.io">Setec Labs</a> //</p>
<p>// Source: <a href="https://repo.seteclabs.io/SetecLabs/cam-mitm">repo.seteclabs.io/SetecLabs/cam-mitm</a> //</p>
<p>// 2026 SsSnake / Setec Labs &middot; CC-BY-4.0 //</p>
</div>
</div>
<script src="boot.js"></script>
</body>
</html>

730
style.css Normal file
View File

@@ -0,0 +1,730 @@
/* ─────────────────────────────────────────────────────────
camhak.seteclabs.io — extends seteclabs.io/style.css
Aesthetic: phosphor CRT, elevated. Boot sequence, decrypt
reveals, pulsing severity badges, glitch hero, custom cursor.
───────────────────────────────────────────────────────── */
:root {
--green: #00ff41;
--green-dim: #00cc33;
--green-dark: #009922;
--green-deep: #004d11;
--bg: #050605;
--bg-light: #0c0e0c;
--bg-soft: #08140a;
--border: #00ff4133;
--text: #00ff41;
--text-dim: #00aa2a;
--link: #00ffaa;
--link-hover: #ffffff;
--red: #ff3344;
--red-glow: #ff334466;
--amber: #ffb000;
--info: #5ec8ff;
}
html, body {
background: var(--bg);
cursor: crosshair;
}
/* override the inherited body for sharper black */
body {
background: radial-gradient(ellipse at center, #061008 0%, #020302 70%, #000 100%);
position: relative;
overflow-x: hidden;
}
/* ─── ATMOSPHERICS ──────────────────────────────────────── */
/* fine grain noise */
.grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9998;
opacity: 0.10;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.95' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.25 0 0 0 1 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}
/* corner vignette */
.vignette {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9997;
background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.7) 100%);
}
/* CRT bezel — subtle inset shadow simulating screen curvature */
.bezel {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9996;
box-shadow:
inset 0 0 120px rgba(0, 30, 0, 0.6),
inset 0 0 60px rgba(0, 255, 65, 0.05);
border-radius: 12px;
}
/* ─── HUD ───────────────────────────────────────────────── */
#hud {
position: fixed;
top: 14px;
right: 18px;
z-index: 100;
font-family: 'Share Tech Mono', monospace;
font-size: 11px;
color: var(--text-dim);
text-align: right;
background: rgba(0, 10, 0, 0.6);
border: 1px solid var(--green-deep);
padding: 8px 12px;
letter-spacing: 1px;
text-shadow: 0 0 6px var(--green-deep);
}
.hud-row {
display: flex;
justify-content: flex-end;
gap: 8px;
margin: 1px 0;
}
.hud-key {
color: var(--text-dim);
opacity: 0.7;
}
.hud-val {
color: var(--green);
}
.hud-blink {
color: var(--green);
animation: hud-pulse 1.4s ease-in-out infinite;
}
@keyframes hud-pulse {
0%, 100% { opacity: 0.3; text-shadow: 0 0 2px var(--green); }
50% { opacity: 1; text-shadow: 0 0 12px var(--green), 0 0 4px var(--green); }
}
/* ─── BOOT TERMINAL ─────────────────────────────────────── */
#boot {
font-family: 'Share Tech Mono', monospace;
font-size: 11px;
color: var(--text-dim);
margin: 30px 0 10px 0;
min-height: 8em;
white-space: pre-wrap;
text-shadow: 0 0 6px var(--green-deep);
letter-spacing: 0.5px;
line-height: 1.5;
}
#boot .ok { color: var(--green); }
#boot .warn { color: var(--amber); }
#boot .err { color: var(--red); }
#boot .cur { animation: cur 0.8s steps(1) infinite; }
@keyframes cur {
50% { opacity: 0; }
}
/* ─── HERO LOGO + GLITCH ───────────────────────────────── */
#ascii-logo {
text-align: center;
margin: 18px 0 6px 0;
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.05;
color: var(--green);
white-space: pre;
text-shadow:
0 0 4px var(--green),
0 0 14px var(--green),
0 0 30px var(--green-deep),
0 0 50px var(--green-deep);
letter-spacing: 1px;
position: relative;
animation: hero-fade-in 1.4s ease-out 0.4s both;
}
@keyframes hero-fade-in {
0% { opacity: 0; filter: blur(8px); transform: translateY(-6px); }
60% { opacity: 1; filter: blur(0); }
100% { opacity: 1; filter: blur(0); transform: translateY(0); }
}
/* RGB-split glitch on hover */
.glitch {
position: relative;
}
.glitch::before, .glitch::after {
content: attr(data-text);
position: absolute;
top: 0; left: 0; right: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: var(--green);
pointer-events: none;
opacity: 0;
}
#ascii-logo:hover {
animation: glitch-shake 0.4s linear;
}
@keyframes glitch-shake {
0%, 100% { transform: translate(0,0); }
10% { transform: translate(-2px, 1px); filter: hue-rotate(-15deg); }
20% { transform: translate(2px, -1px); }
30% { transform: translate(-1px, 2px); filter: hue-rotate(15deg); }
40% { transform: translate(1px, -2px); }
50% { transform: translate(-2px, -1px); }
60% { transform: translate(2px, 1px); filter: hue-rotate(-25deg); }
70% { transform: translate(-1px, -2px); }
80% { transform: translate(1px, 2px); }
90% { transform: translate(0,0); filter: hue-rotate(0); }
}
#tagline {
text-align: center;
font-family: 'VT323', monospace;
font-size: 22px;
color: var(--green);
margin: 12px 0 6px 0;
text-shadow: 0 0 18px var(--green), 0 0 4px var(--green);
letter-spacing: 4px;
text-transform: uppercase;
animation: hero-fade-in 1.2s ease-out 0.9s both;
}
.byline {
text-align: center;
color: var(--text-dim);
font-size: 11px;
letter-spacing: 3px;
text-transform: uppercase;
margin-bottom: 12px;
animation: hero-fade-in 1.2s ease-out 1.2s both;
}
/* ─── NAV ───────────────────────────────────────────────── */
nav {
text-align: center;
margin: 18px 0;
padding: 10px 0;
border-top: 1px dashed var(--green-deep);
border-bottom: 1px dashed var(--green-deep);
animation: hero-fade-in 1s ease-out 1.5s both;
}
nav a {
color: var(--link);
text-decoration: none;
margin: 0 10px;
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
position: relative;
transition: color 0.2s, text-shadow 0.2s;
}
nav a:hover {
color: #fff;
text-shadow: 0 0 12px var(--green), 0 0 2px #fff;
}
nav a:hover::before {
content: '> ';
color: var(--green);
}
/* ─── DIVIDERS ──────────────────────────────────────────── */
.divider {
border: none;
border-top: 1px solid var(--green-deep);
margin: 24px 0;
opacity: 0.55;
position: relative;
}
.divider::after {
content: '';
position: absolute;
left: 50%;
top: -3px;
width: 8px;
height: 5px;
background: var(--green);
transform: translateX(-50%);
box-shadow: 0 0 8px var(--green);
}
.divider-heavy {
border: none;
border-top: 2px solid var(--green);
margin: 28px 0;
box-shadow: 0 0 12px var(--green-deep);
}
/* ─── SECTIONS w/ DECRYPT REVEAL ────────────────────────── */
.section {
margin: 30px 0;
padding: 18px 22px;
border-left: 2px solid var(--green-deep);
position: relative;
}
.section::before {
content: '';
position: absolute;
left: -2px; top: 0; bottom: 0;
width: 2px;
background: var(--green);
box-shadow: 0 0 12px var(--green);
transform: scaleY(0);
transform-origin: top;
transition: transform 0.6s ease-out;
}
.section.in-view::before { transform: scaleY(1); }
.section-title {
font-family: 'VT323', monospace;
font-size: 26px;
color: var(--green);
margin-bottom: 18px;
text-shadow: 0 0 14px var(--green-dark), 0 0 4px var(--green);
letter-spacing: 3px;
}
.section-title::before {
content: '> ';
color: var(--text-dim);
}
.section-title .counter {
font-size: 14px;
color: var(--text-dim);
letter-spacing: 2px;
}
/* Reveal animation for sections — one-shot when in-view */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 0.7s ease-out, transform 0.7s ease-out;
}
.reveal.in-view {
opacity: 1;
transform: translateY(0);
}
/* ─── TEXT BLOCKS ──────────────────────────────────────── */
.text-block {
color: var(--green-dim);
font-size: 14px;
line-height: 1.7;
}
.text-block p { margin-bottom: 14px; }
.text-block .muted { color: var(--text-dim); font-size: 12px; }
.text-block .small { font-size: 11px; }
.hl {
color: var(--green);
text-shadow: 0 0 8px var(--green-dark);
font-weight: bold;
}
code {
font-family: 'Share Tech Mono', monospace;
background: var(--bg-soft);
padding: 1px 6px;
border: 1px solid var(--green-deep);
color: var(--link);
font-size: 12px;
white-space: nowrap;
}
.codeblock {
background: linear-gradient(180deg, #050b06 0%, #030503 100%);
border: 1px solid var(--green-deep);
border-left: 3px solid var(--green);
padding: 16px 20px;
margin: 14px 0;
font-family: 'Share Tech Mono', monospace;
font-size: 12.5px;
color: var(--green-dim);
white-space: pre;
overflow-x: auto;
line-height: 1.6;
box-shadow:
inset 0 0 24px rgba(0, 80, 20, 0.18),
0 0 16px rgba(0, 100, 30, 0.05);
}
.codeblock .k { color: var(--green); text-shadow: 0 0 6px var(--green-dark); }
.codeblock .dim { color: var(--text-dim); }
/* ─── FINDINGS TABLE ───────────────────────────────────── */
table.findings {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 13px;
background: rgba(0, 12, 4, 0.4);
border: 1px solid var(--green-deep);
}
table.findings th {
text-align: left;
padding: 9px 12px;
border-bottom: 1px solid var(--green);
color: var(--green);
font-family: 'VT323', monospace;
font-size: 17px;
letter-spacing: 2px;
text-transform: uppercase;
text-shadow: 0 0 8px var(--green-dark);
background: rgba(0, 30, 10, 0.6);
}
table.findings td {
padding: 7px 12px;
border-bottom: 1px dashed var(--green-deep);
color: var(--green-dim);
vertical-align: middle;
}
table.findings tbody tr {
transition: background 0.15s;
}
table.findings tbody tr:hover {
background: rgba(0, 60, 18, 0.35);
}
table.findings tbody tr:hover td { color: var(--green); }
table.findings .col-id { width: 50px; color: var(--text-dim); font-family: 'Share Tech Mono', monospace; }
table.findings .col-sev { width: 90px; }
/* severity badges */
.sev {
display: inline-block;
padding: 2px 8px;
font-family: 'VT323', monospace;
font-size: 13px;
letter-spacing: 1.5px;
border: 1px solid currentColor;
background: rgba(0, 0, 0, 0.4);
}
.sev-crit {
color: var(--red);
text-shadow: 0 0 8px var(--red);
border-color: var(--red);
animation: sev-pulse 1.6s ease-in-out infinite;
}
@keyframes sev-pulse {
0%, 100% { box-shadow: 0 0 0 var(--red-glow), inset 0 0 0 var(--red-glow); }
50% { box-shadow: 0 0 16px var(--red-glow), inset 0 0 8px var(--red-glow); }
}
.sev-high { color: var(--amber); text-shadow: 0 0 6px var(--amber); border-color: var(--amber); }
.sev-med { color: var(--green); text-shadow: 0 0 6px var(--green-dark); }
.sev-info { color: var(--info); text-shadow: 0 0 6px var(--info); border-color: var(--info); }
.cve-tag {
display: inline-block;
font-size: 10px;
font-family: 'Share Tech Mono', monospace;
color: var(--info);
border: 1px solid var(--info);
padding: 1px 5px;
margin-left: 4px;
letter-spacing: 1px;
vertical-align: middle;
}
.redact {
background: var(--red);
color: #000;
padding: 0 6px;
font-weight: bold;
letter-spacing: 1.5px;
font-family: 'Share Tech Mono', monospace;
font-size: 11px;
}
/* ─── CVE CARDS ────────────────────────────────────────── */
.cve-card {
margin: 18px 0;
padding: 16px 20px;
border: 1px solid var(--green-deep);
background: linear-gradient(180deg, rgba(0, 20, 6, 0.6), rgba(0, 8, 2, 0.4));
position: relative;
box-shadow: inset 0 0 30px rgba(0, 60, 18, 0.1);
}
.cve-card::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: var(--green);
box-shadow: 0 0 14px var(--green);
}
.cve-card header {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 8px;
font-family: 'Share Tech Mono', monospace;
}
.cve-id {
color: var(--link);
font-size: 14px;
font-weight: bold;
letter-spacing: 1px;
text-shadow: 0 0 8px rgba(0, 255, 170, 0.5);
}
.cvss {
color: var(--text-dim);
font-size: 11px;
letter-spacing: 1px;
}
.cve-state {
margin-left: auto;
font-size: 11px;
letter-spacing: 1.5px;
padding: 2px 8px;
border: 1px solid currentColor;
}
.state-vuln {
color: var(--red);
text-shadow: 0 0 8px var(--red-glow);
animation: sev-pulse 1.8s ease-in-out infinite;
}
.state-warn {
color: var(--amber);
text-shadow: 0 0 6px var(--amber);
}
.cve-card h3 {
font-family: 'VT323', monospace;
font-size: 19px;
color: var(--green);
margin: 6px 0 10px 0;
letter-spacing: 1.5px;
text-shadow: 0 0 8px var(--green-dark);
}
.cve-card p {
color: var(--green-dim);
font-size: 13px;
line-height: 1.7;
}
/* ─── TOOLS LIST ───────────────────────────────────────── */
ul.tools, ul.disclosure {
list-style: none;
padding: 0;
margin: 14px 0;
}
ul.tools li, ul.disclosure li {
margin: 7px 0;
padding: 5px 0 5px 22px;
position: relative;
color: var(--green-dim);
border-bottom: 1px dashed var(--green-deep);
transition: background 0.15s, padding-left 0.2s;
}
ul.tools li::before, ul.disclosure li::before {
content: '\BB';
position: absolute;
left: 4px;
top: 5px;
color: var(--link);
text-shadow: 0 0 6px var(--link);
}
ul.tools li:hover, ul.disclosure li:hover {
background: rgba(0, 60, 18, 0.2);
padding-left: 28px;
color: var(--green);
}
ul.tools li strong {
color: var(--link);
font-family: 'Share Tech Mono', monospace;
}
.modulename {
font-family: 'VT323', monospace;
font-size: 18px;
color: var(--link);
letter-spacing: 2px;
margin-top: 14px;
text-shadow: 0 0 10px rgba(0, 255, 170, 0.4);
}
/* ─── REASONS (numbered list with big numerals) ────────── */
ol.reasons {
list-style: none;
padding: 0;
margin: 16px 0;
}
ol.reasons li {
display: flex;
gap: 18px;
margin: 16px 0;
padding: 12px 14px;
border-left: 1px solid var(--green-deep);
background: rgba(0, 12, 4, 0.3);
}
ol.reasons li .num {
font-family: 'VT323', monospace;
font-size: 42px;
color: var(--green);
text-shadow: 0 0 14px var(--green-dark), 0 0 4px var(--green);
flex-shrink: 0;
line-height: 1;
}
ol.reasons li > div {
font-size: 13px;
color: var(--green-dim);
line-height: 1.7;
}
/* ─── QUOTE BLOCK ──────────────────────────────────────── */
.quote-block {
margin: 28px auto;
padding: 26px 28px;
border: 1px solid var(--green-deep);
background:
radial-gradient(ellipse at top, rgba(0, 80, 20, 0.18), transparent 60%),
rgba(0, 8, 2, 0.6);
text-align: center;
position: relative;
max-width: 720px;
}
.quote-block::before { content: '/*'; position: absolute; top: 6px; left: 12px; color: var(--text-dim); font-size: 12px; }
.quote-block::after { content: '*/'; position: absolute; bottom: 6px; right: 12px; color: var(--text-dim); font-size: 12px; }
.quote-text {
font-family: 'Share Tech Mono', monospace;
font-size: 14px;
color: var(--green);
font-style: italic;
line-height: 1.85;
text-shadow: 0 0 6px var(--green-dark);
}
.quote-attr {
margin-top: 12px;
font-size: 11px;
color: var(--text-dim);
letter-spacing: 2px;
text-transform: uppercase;
}
.signoff {
text-align: right;
margin-top: 26px;
font-size: 13px;
color: var(--text-dim);
letter-spacing: 2px;
font-family: 'Share Tech Mono', monospace;
}
.signoff .link { color: var(--link); text-shadow: 0 0 8px rgba(0, 255, 170, 0.4); }
/* ─── SCREENSHOTS ───────────────────────────────────── */
figure.screenshot {
margin: 22px 0;
padding: 14px;
background: linear-gradient(180deg, rgba(0, 20, 6, 0.5), rgba(0, 6, 2, 0.4));
border: 1px solid var(--green-deep);
border-left: 3px solid var(--green);
box-shadow: inset 0 0 30px rgba(0, 60, 18, 0.1);
}
figure.screenshot img {
display: block;
max-width: 100%;
height: auto;
border: 1px solid var(--green-dark);
box-shadow:
0 0 18px rgba(0, 255, 65, 0.12),
0 0 4px rgba(0, 255, 65, 0.25);
margin: 0 auto 12px auto;
/* gentle phosphor glow on hover */
transition: box-shadow 0.3s, filter 0.3s;
}
figure.screenshot img:hover {
box-shadow:
0 0 28px rgba(0, 255, 65, 0.25),
0 0 8px rgba(0, 255, 65, 0.4);
filter: brightness(1.05);
}
figure.screenshot figcaption {
font-family: 'Share Tech Mono', monospace;
font-size: 12px;
color: var(--green-dim);
line-height: 1.7;
padding: 6px 10px 0 10px;
border-top: 1px dashed var(--green-deep);
}
figure.screenshot figcaption strong {
color: var(--link);
text-shadow: 0 0 6px rgba(0, 255, 170, 0.3);
}
figure.screenshot figcaption code {
font-size: 11px;
}
/* ─── PROMPT FOOTER ────────────────────────────────────── */
.prompt {
font-family: 'Share Tech Mono', monospace;
font-size: 13px;
color: var(--green);
margin: 30px 0 6px 0;
padding: 8px 0;
border-top: 1px dashed var(--green-deep);
text-shadow: 0 0 6px var(--green-dark);
}
.prompt-host { color: var(--link); }
.prompt-sep { color: var(--text-dim); }
.prompt-cwd { color: var(--green); }
.prompt-cmd { color: var(--green); }
.prompt-cursor {
display: inline-block;
background: var(--green);
color: var(--bg);
animation: cur 0.8s steps(1) infinite;
width: 9px;
margin-left: 2px;
}
/* ─── FOOTER ────────────────────────────────────────────── */
.footer {
text-align: center;
font-size: 11px;
color: var(--text-dim);
margin: 18px 0 26px 0;
letter-spacing: 2px;
font-family: 'Share Tech Mono', monospace;
}
.footer p { margin: 4px 0; }
.footer a {
color: var(--link);
text-decoration: none;
}
.footer a:hover { color: #fff; text-shadow: 0 0 8px var(--green); }
/* ─── RESPONSIVE ──────────────────────────────────────── */
@media (max-width: 720px) {
#hud { display: none; }
#ascii-logo { font-size: 7px; }
#tagline { font-size: 16px; letter-spacing: 2px; }
.section { padding: 14px; }
.section-title { font-size: 22px; }
ol.reasons li { flex-direction: column; gap: 8px; }
ol.reasons li .num { font-size: 32px; }
}