Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-25243 — Stable POC for CVE-2026-25243 (Redis RESTORE double-free -> remote code execution) | Kitploit
Tools/GitHubGitHub/captain-woof/cve-2026-25243
Vulnerability AnalysisExploitationPost-ExploitationPenetration TestingRed TeamingDatabase SecurityBinary Exploitation
GitHubcaptain-woof/cve-2026-25243

CVE-2026-25243

Stable POC for CVE-2026-25243 (Redis RESTORE double-free -> remote code execution)

View Repository
111 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-25243 — Redis RESTORE double-free → remote code execution

Verified against Rocky Linux 8.10, aarch64, Redis 8.6.2, jemalloc 5.3.0.

Reference: https://www.zeroday.cloud/blog/redis-cve-2026-25243-deep-dive

TLDR; Stable exploit, works against variety of OS distro and architecture.


Executive Summary

What is this? A memory corruption vulnerability in Redis that lets an authenticated attacker run arbitrary commands as the Redis user. The attack is real-world and requires just a single RESTORE command — a normal Redis operation, not admin-only. This exploit demonstrates full RCE in under one second.

Impact? Any authenticated Redis client can trigger it, and the damage is total: arbitrary code execution in the Redis process (often running as root in containers). There is no way to mitigate without patching Redis itself.

How does it work at a glance? Redis has a serialization feature (RESTORE) that takes a blob of binary data and reconstructs it as a Redis object. The code that validates the blob's format and the code that deserializes it disagree on how to parse certain sequences — a bug that the attacker exploits to corrupt the heap. Once the heap is corrupted, the attacker gains the ability to read and write any memory address in the Redis process, and from there hijacks the server's internal state to execute a shell command.

The real exploit technique: This is not a simple crash. It's a : corrupt → overlap → arbitrary R/W → information leak → find the server struct → hijack function pointers → RCE. The exploit runs 9 stages and requires leaking multiple addresses at runtime, parsing binary structures, and detecting memory aliasing. What makes it work across architectures (x86-64, aarch64, etc.) is that all the addresses are , not assumed.

heap exploitation chain
leaked from the target itself

1. The vulnerability — in detail

CVE-2026-25243 is a pair of double-free bugs reachable from a single authenticated RESTORE command. RESTORE key ttl <serialized-value> deserializes an attacker-controlled RDB blob; both bugs live in the gap between the validator that checks the blob and the converter that materializes it.

Bug 1 — legacy zipmap conversion (CWE-415, the path this exploit uses). The zipmap validator (zipmapValidateIntegrity()) and the converter (zipmapNext()) disagree about a redundant length encoding. The small length 4 can legally be written in the long five-byte form FE 04 00 00 00. The validator consumes one number of bytes, the converter another — a 4-byte parsing desynchronisation. The converter therefore walks a different structure than the one that was validated, lpSafeToAdd() fails after the field has already been inserted into the dictionary, and the cleanup path frees the field twice: once via dictRelease() and again via sdsfree().

Bug 2 — stream consumer PEL loading (CWE-415). In rdbLoadStreamConsumersGroup(), a consumer PEL containing a duplicate entry ID makes the second raxTryInsert() fail, which calls streamFreeNACK() on a streamNACK that is still owned by the group's global PEL. Freed twice. (Selectable with --vuln-type stream.)

Either bug hands the attacker a chunk of memory that is simultaneously free and referenced — the classic starting point for a heap-overlap exploit.

Impact: an authenticated Redis client (no admin rights, RESTORE is a normal data command) gains arbitrary code execution as the redis user — root in the default container image.

2. How the exploit works

Nine stages, each of which turns a weaker primitive into a stronger one:

StagePrimitive gainedMechanism
0target profileINFO server / INFO memory → version, arch, distro, pid, executable path, start time, allocator
1double freemalformed zipmap (or stream) RESTORE
2two keys sharing memoryspray marker keys onto the freed chunk, detect aliasing, then overwrite one key's SDS header through its twin to inflate it to a 1 MB "memview"
3arbitrary R/Wfind an INCRBYFLOAT object inside the memview, hijack its ptr field: GETRANGE/SETRANGE on that key now read/write any address
4image pointerscan heap backwards for a value inside the redis-server image
5&serverwalk down to the ELF header, parse program headers, dump the writable segment, match server.pid
6payload in memorywrite "/bin/sh", "-c", "<cmd>" plus an argv array into the memview
7hijacked structoverwrite server.executable, server.exec_argv, and server.enable_debug_cmd
8RCEDEBUG CRASH-AND-RECOVER → restartServer() → execve(server.executable, server.exec_argv, environ)

How to trigger

root@kitploit:~
python3 exploit.py --host 127.0.0.1 --port 6379 \
    --password mypassword --cmd 'id > /tmp/pwned123.txt'

Verify:

root@kitploit:~
cat /tmp/pwned123.txt
# uid=0(root) gid=0(root) groups=0(root)

3. Change log

2026-08-06 — portability, reliability and speed rework

Starting point: the exploit was x86-64-only and died in stage 3 on the aarch64 target. End state: full RCE on aarch64 Rocky Linux 8.10 in under one second, 116 Redis commands.

a) Runtime target fingerprinting (new, stage 0). Nothing about the target is assumed any more. INFO server + INFO memory yield the Redis version, CPU architecture (from the os: line), distro family (inferred from gcc_version), allocator, and — most importantly — three validation anchors: process_id, executable, and the exact stat_starttime (server_time_usec/1e6 - uptime_in_seconds). Later stages compare against these instead of guessing.

b) Architecture-independent memory layout. The four hardcoded x86-64 constants (BINARY_ADDR_MIN/MAX, HEAP_ADDR_MIN/MAX) are replaced by a per-architecture table (ARCH_PROFILES) covering x86_64, aarch64 (both 39- and 48-bit VA), riscv64, ppc64le and s390x, with both the ET_EXEC and ET_DYN placements for each, plus a wide generic fallback for anything unlisted. This was the actual reason the exploit failed on this target: the leaked pointer 0x0000ffff8a5fdf32 is a perfectly good aarch64 mmap address that the x86-64 range check rejected.

c) Consensus-based leak validation (stage 3). Rather than trusting a hardcoded heap window, the scan now collects every structurally valid 1337.NNNNNN object in the memview and requires at least two of them to derive the same memview base address (ptr - offset_of_value). In practice 502 candidates agree, which is proof no range table can offer. The confirmed pointer then calibrates the heap window at runtime. Format validation was also moved before the (round-trip-expensive) write-control test.

d) Stage 3 scan bound (bug fix). The scan ran to a hardcoded 10 MB while the memview is 1 MB, so it read past the end, got an empty reply and aborted with AssertionError: Empty data from memview. It is now bounded by the memview's real STRLEN, reads 256 KB per round-trip instead of 64 KB, and the pointless 6×1s retry-sleep loop is gone.

e) Stage 5 rewritten: ELF-guided, crash-free (the big one). The old implementation scanned forward from an image pointer, probing addresses and reading whatever length a garbage SDS header claimed. On this target it walked straight off the end of the read-only segment into the unmapped hole at 0x715000 and killed the server (SIGSEGV in getrangeCommand → memcpy). Blind scanning cannot be made safe. The replacement is deterministic:

  1. Find the image base. Walk down page by page from the lowest leaked image pointer. The probe is free: the first five bytes of every ELF64 image are 7f 45 4c 46 02, and sdslen() takes its flags byte from ptr[-1] — so pointing the hijacked object at base+5 makes e_ident[EI_CLASS]=0x02 the flags byte, i.e. SDS_TYPE_16, whose length is the uint16 at base+0 = 0x457f (0x7f45 big-endian). A STRLEN of exactly 17791 is the ELF signature. No local copy of the binary is needed — the header is read out of the target's own memory.
  2. Parse the program headers to get the exact runtime bounds of every PT_LOAD segment (handling the ET_DYN load bias for PIE targets). Every subsequent read is clamped to a real mapping, so the unmapped-hole crash is now structurally impossible.
  3. Forge one SDS header in a zeroed slot of the writable segment, which makes the whole of .data/.bss readable in a handful of round-trips instead of hundreds of thousands of byte probes. The overwritten bytes are saved and restored.
  4. Match server.pid against the pid from INFO — an exact 8-byte equality test — then confirm by dereferencing server.executable and comparing the string to INFO's executable. The old code accepted a loose seven-field shape heuristic; the struct is now positively identified.

f) Stage 4 hardened. The Lua validator takes the full per-arch range list (so a non-PIE image at 0x400000 and a PIE image at 0xaaaa… are both recognised) and excludes the calibrated heap window. It returns several candidates instead of one, so a bad pick costs a retry rather than the run.

g) Stage 7 self-verifying. enable_debug_cmd was located by a hardcoded stat_starttime - 0x3c. Now the expected stat_starttime value is known exactly from INFO (a 3-second window instead of 30 days), the struct read window grew from 4 KB to 32 KB (stat_starttime sits at offset 0x9e0, well past the old limit), and — decisively — each candidate offset is verified with a live oracle: set the byte, send DEBUG SET-ACTIVE-EXPIRE 1, and see whether the server accepts it. Wrong guesses are restored before the next attempt, so the flag is found on any build rather than assumed. -0x3c is still tried first and confirmed correct for 8.6.2 (offset 0x9a4).

h) Writes actually land (stage 5). setrangeCommand() calls dbUnshareStringValue(), which duplicates the value unless encoding == RAW && refcount == 1. The encoding byte is now zeroed before the first write through the hijacked pointer, so writes reach the target address instead of a private copy.

i) Payload simplified. All backconnect/reverse-shell machinery, the ASCII banner and the appended ;sleep 5 were removed. The payload is exactly /bin/sh -c '<--cmd>' and nothing else. --cmd defaults to id > /tmp/pwned123.txt.

j) Speed. Stage 4 collects 3 candidates instead of 8; stage 5 replaces ~10^5 byte probes with ~40 bulk reads; stage 3 uses 256 KB reads and skips round-trips for candidates that fail local validation. Whole chain: 116 commands, <1 s.

Result: uid=0(root) gid=0(root) groups=0(root) in /tmp/pwned123.txt on the target container.

Portability notes

  • Architecture is detected, not assumed. The ELF-based stage 5 is arch-neutral by construction (it reads the target's own program headers) and handles both PIE and non-PIE images, little- and big-endian.
  • Distro is reported for the operator's benefit; the exploit has no functional dependency on it. The only filesystem assumption is /bin/sh, which POSIX and the FHS require.
  • Version: RDB version and stream struct size are selected from redis_version (7.x and 8.x supported). Struct field offsets (executable=24, exec_argv=32) follow from the LP64 ABI, and enable_debug_cmd is discovered and verified at runtime rather than hardcoded.
  • 32-bit targets are rejected explicitly in stage 0 (the payload builds 64-bit pointers) instead of failing obscurely later.

2026-08-06 (later) — stability hardening after repeat-run testing

Measured 13/13 successful runs on the default zipmap path (5 + 8 back-to-back), each completing in ≤1 second. Three issues surfaced only under repeated execution and are now fixed:

k) SAVE race in stage 0. A run could abort with ERR Background save already in progress when a previous run's (or redis' own) background save was still running. SAVE is now retried for up to 15 seconds and, failing that, the run proceeds without the checkpoint instead of aborting.

l) Reconnect while the target restarts (--connect-retries, default 10). A failed attempt leaves the heap corrupted, so the next run's FLUSHALL frees the poisoned chunks and takes the server down. It restarts seconds later and is perfectly exploitable, so stage 0 now reconnects and retries rather than failing. Our own validation errors (unsupported version/architecture) are never retried. This removed the intermittent "stage 0 failed with an empty error" seen at roughly 1 run in 3 during stress testing.

m) sizeof(streamNACK) corrected for 8.6.x. The --vuln-type stream path sprayed the wrong jemalloc size class because the struct was assumed to be 24 or 32 bytes. In 8.6.2 it is 64 bytes (delivery_time, delivery_count, consumer, cgroup_ref_node, streamID id, pel_prev, pel_next). With the correct size the stream path now reaches stage 5 instead of failing at stage 2 with "key overlap not found".

Known limitations

  • --vuln-type stream is not reliable on 8.6.2. With the size fix it gets through the double-free, the overlap, the R/W primitive and the ELF parse, then destabilizes the keyspace: the server dies in setrangeCommand reading o->ptr at NULL+8, i.e. a key lookup returns a corrupted object. The 64-byte chunk it frees is shared with other live allocations, which makes it far more collateral-heavy than the zipmap path. Use the default --vuln-type zipmap, which is 13/13.
  • --random-heap-massage (100k random keys first) succeeds but not invariably — the sprayed heap sometimes places the double-freed chunk where no marker key lands. Re-running succeeds.
  • Verified on aarch64 / Rocky Linux 8.10 / Redis 8.6.2 (non-PIE ET_EXEC) only. The x86-64 and PIE paths are implemented and arch-neutral by construction but have not been executed against a live target in this session.
Download Tool