
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"
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.
Für den system-Abschluss enthält der Over-Read des kleinen Heaps keine libc-Pointer, daher kann libc nicht selbst abgeleitet werden — poc_rce_aslr_selfcal.py behält zwei libc-Versions-Offsets (DLIBC, SYSTEM_OFF). Der poc_rce_aslr_pie_rop.py-Abschluss entfernt diese Abhängigkeit vollständig: Derselbe Over-Read enthält durchaus PIE-Pointer (ein gemeinsames dictType erscheint wiederholt), sodass die PIE-Basis als most-common-PIE-value − DICTTYPE_OFF selbstkalibriert wird, und die Kette endet in execve@plt über einen JOP-Stack-Pivot (mov rbp,rdi; call *0x8(rax) → leave;ret verlagert rsp auf das vom Angreifer kontrollierte HGET-Feld, das die execve("/bin/sh","-c",<cmd>)-ROP-Kette ). Die einzigen buildspezifischen Konstanten sind die PIE-relativen Gadget-Offsets aus — extrahieren Sie sie pro Ziel mit /, genau wie unser früherer HLL-Exploit eine Gadget-Tabelle pro ELF-Build-ID hinterlegt. Es wird keine libc-Adresse verwendet.
screenshots/05-pie-rop-libc-free.png (der NO libc-Selbstkalibrierungslauf mit uid=0, der in sich geschlossenste Lauf), 01-rce-aslr-on.png (das uid=0-Beweisfoto), 02-reliability.png (5/5), 03-exploit-chain.png (der Code) und 04-debug-free-selfcal.png (der DEBUG-freie, selbstkalibrierende system-Lauf auf einem Standard-Redis).
RESTORE ist ein gewöhnlicher Befehl — auf einem nicht authentifizierten/offen erreichbaren Redis (kein requirepass) kann ihn jeder verbundene Client ausführen; auf einer authentifizierten Instanz jeder Benutzer, dem per ACL -restore nicht verweigert wird. Gleiches Zugriffsprofil wie die Datenstrukturbefehle, die von anderen Redis-RCEs verwendet werden.< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} — d. h. 6.2.x bis einschließlich aktuelles 8.x; Valkey ist nur DoS/Over-Read (seine field_added-Absicherung blockiert den Double-Free).Führen Sie ein Upgrade auf eine behobene Version durch. Falls das nicht möglich ist: Schränken Sie RESTORE ein (ACL … -restore), setzen Sie Redis niemals unauthentifiziert ein und deaktivieren Sie DEBUG.
Autorisierte Sicherheitsforschung, veröffentlicht zur Sensibilisierung von Verteidigern. Führen Sie dies nicht gegen Systeme aus, die Sie nicht besitzen oder für die Sie keine ausdrückliche Testgenehmigung haben.
| 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) |
redis-serverROPgadgetobjdump