文章总结: 本文剖析了影响Redis6.2至8.8的三组认证后RCE漏洞,利用Stream双重释放、TDigest堆溢出及TopK野指针释放构造任意读写原语,劫持hashFunction实现无破坏性代码执行。利用无需DEBUG模式且服务可恢复,但受TCP分片限制仅适用于SSRF或本地回环场景。建议通过ACL禁用RESTORE等高危命令实现根本防御。 综合评分: 95 文章分类: 漏洞分析,渗透测试,WEB安全,漏洞POC
【漏洞分析】Redis 6.2–8.8 认证后 RCE 漏洞深度分析与复现
原创
博智非攻研究院 博智非攻研究院
博智非攻研究院
2026年7月28日 14:40 江苏
在小说阅读器读本章
去阅读
影响范围:Redis 6.2.22 / 7.4.9 / 8.6.4 / 8.8.0 / 8.8.1(覆盖 6.x – 8.x 全线版本)
漏洞类型:认证后堆利用 → 远程代码执行
利用前提:Redis AUTH凭据 + 本地回环投递(SSRF / 容器提权)
核心特点:无破坏性利用,执行后服务恢复正常;无需DEBUG 模式
2026 年 7 月,安全研究团队 berabuddies 公开了一系列影响 Redis 6.2 至 8.8 全线版本的认证后远程代码执行漏洞。三组漏洞分别利用 Stream 消费者组共享 NACK 双重释放、RedisBloom TDigest 堆溢出、以及 TopK 野指针释放,在 jemalloc 堆上构建任意内存读写原语,最终劫持控制流执行系统命令。所有利用方式均为无破坏性——服务器在代码执行后仍可恢复正常运行。
本文包含完整的漏洞原理分析、利用链详解与实际 RCE 复现输出。
一、漏洞概述
| 序号 | 漏洞类型 | 影响版本 | CVE 关联 | 补丁状态 | | — | — | — | — | — | | 1 | Stream NACK Double Free | 6.2.22 / 7.4.9 / 8.6.4 | CVE-2026-25243 补丁绕过 | 8.8.0(PR #15081)修复 | | 2 | TDigest 堆溢出 | 8.8.0 | 新发现(无 CVE) | 未修复 | | 3 | TopK 野指针释放 | 8.8.0 / 8.8.1 | CVE-2026-25589 补丁绕过 | 未修复 |
利用前置条件:
1.需要有效的 Redis 认证凭据(AUTH命令)
2.目标需开放 EVAL、RESTORE、XGROUP等命令
3.漏洞 2/3 还需 RedisBloom 模块(Redis 8.x 默认捆绑)
4.对 jemalloc 堆布局敏感,推荐在空实例上利用
二、漏洞原理分析
2.1 漏洞1:Stream 消费者组共享 NACK 双重释放
Redis Stream 的 RDB 加载逻辑(rdb.c)在处理消费者组的 PEL(Pending Entries List)时存在缺陷。当一条消息被两个消费者引用,但全局 PEL 大小仅为 1 时,两个消费者的 NACK 会指向同一个 streamNACK结构体(24 字节,jemalloc tcache bin24)。
// 攻击流程 — 触发 Double Free
RESTORE <crafted_stream> // 精心构造的 Stream:alice 和 bobxx 两个消费者
// 全局 PEL=1 → 两个消费者共享同一个 streamNACK
XGROUP DELCONSUMER alice // 第一次释放 streamNACK
XGROUP DELCONSUMER bobxx // 第二次释放同一块内存 → Double Free!
为什么是补丁绕过:CVE-2026-25243 最初的修复尝试未能完全消除多消费者共享同一 NACK 的场景。本文中的 RDB payload 构造方式成功绕过了该修复,直到 Redis 8.8.0(PR #15081)才从根本上修复。影响 Redis 6.2.22、7.4.9、8.6.4。
2.2 漏洞2:TDigest 堆溢出(RedisBloom 模块)
Redis 8.8.0 捆绑的 RedisBloom 模块中,TDigestRdbLoad函数在加载 TDigest 数据结构时存在线性堆溢出。攻击者通过 RESTORE传入精心构造的 payload:声明 compression=1(Redis 据此仅分配 16 条目空间),但 cap字段被设置为远大于实际分配量的值。写入循环受 cap控制,数据线性溢出覆写相邻堆块中 victim tdigest 结构体的关键字段。
| 字段 | 正常值 | 溢出后 | 作用 | | — | — | — | — | | nodes_mean | 原始地址 | X(攻击者指定) | 指向读目标地址 | | nodes_weight | 原始地址 | Y(攻击者指定) | 指向写目标地址 | | merged_nodes | 原始值 | k | 控制读取 qword 数量 | | cap | 原始值 | 0x40000000 | 绕过边界检查 |
溢出后的 victim struct 被劫持为”Oracle“——正常的 DUMP命令从 nodes_mean指向的地址读取数据(任意读),TDIGEST.ADD命令向其写入数据(任意写)。截至 PoC 发布时,该漏洞尚未修复。
2.3 漏洞3:TopK 野指针释放(RedisBloom 模块)
RedisBloom 模块的 TopKRdbLoad函数对 CVE-2026-25589 的修复不完整。错误处理路径中仅将 floor(heapSize/24)个 item 指针清零,但延迟清理函数 TopK_Destroy释放全部 k个指针。
// CVE-2026-25589 补丁绕过 — TopK_Destroy 越界读取
heapSize = 48, k = 3
// 补丁只清零了 floor(48/24) = 2 个 item 指针
// 但 TopK_Destroy 释放全部 k=3 个 heap[i].item
// heap+56 读取到相邻 slab 的 robj.ptr
// → 活 sds 被 je_free() 释放 → 野指针释放(Use-After-Free)
当相邻 slab 恰好是攻击者喷射的 kvobj(RAW 字符串键),偏移 8 处的 robj.ptr被意外释放,但 kvobj 仍然存活——造成经典的 Use-After-Free。RedisBloom v8.8.0 和 v8.8.2 均受影响,截至发布时未修复。
三、利用链分析
3.1 通用利用模式
三组漏洞的利用链遵循相同的通用模式:
堆漏洞触发 → 堆喷占位 → 篡改内存结构 → 构造任意读写原语
→ 信息泄漏(PIE + libc)→ 控制流劫持 → 代码执行 → 环境恢复
关键信息泄漏路径(三组漏洞通用):
1. PIE 基址:Lua string.format的 CClosure 对象 → 读取 str_format函数地址 → 计算 PIE 基址
2. libc 基址:读取 write@GOT→ 得到 libc 基址 → 计算 system地址
3. 控制流目标:读取 server.db→ 遍历数据库数组 → 找到空 db 的 dict 指针
3.2 漏洞1 利用链详解(以 Redis 6.2.22 为例)
阶段一:G2 单释放 UAF → 任意读(PIE 泄漏)
-
RESTORE精心构造的 Stream → 两个消费者(alice / bobxx)共享一个
streamNACK(24B) -
XGROUP DELCONSUMER alice→ 释放该 NACK(24B 进入 tcache bin24)
-
喷射 22 字节键名的
SET命令 → 键名 sds(type5,1+22+1=24B)抢占释放的 NACK 内存 -
键名内容嵌入伪造的
streamConsumer指针→ NACK 的consumer指针被劫持 -
XPENDING命令回显
nack->consumer->name→ 读取伪造 consumer 中指定地址的内容 → 任意内存读取 -
通过 Lua
string.format的 CClosure 泄漏 PIE 基址,通过write@GOT泄漏 libc 基址
阶段二:Double Free → Fake DictEntry → 任意读写
-
重复 double free(
RESTORE→DELCONSUMER×2)→ 释放 stream 的dictEntry(同样 24B) -
22B 键名喷射抢占释放的 dictEntry → 键名内容嵌入 fake key / value / next 指针
-
伪造的
robj(type=RAW, refcount=1)挂在BUFkey 的 sds 内部 -
GETRANGE k001→ 读取 fake robj 指向的任意地址(任意读)
-
SETRANGE k001→ 写入 fake robj 指向的任意地址(任意写)
阶段三:Endgame → RCE
-
泄漏
db[0].dict指针,验证dictType == dbDictType -
扫描空数据库
db[N](通过D + k*96偏移遍历,测试dictType匹配) -
在 BUF sds 内构造 fake
dictType:hashFunction = system(libc 地址),keyCompare = dictSdsKeyCompare(合法值) -
SETRANGE将
db[N].dict->type覆写为 fake dictType 地址(7 字节写入) -
SET dummykey+
GET <shell_command>→system(key)→ RCE -
恢复
dictType原值,删除临时 key,恢复save/slowlog配置
3.3 漏洞2 利用链详解(TDigest 堆溢出)
阶段一:堆喷与溢出
-
创建 2000个
TDIGEST.CREATE实例(compression=1 → 每个 80B struct + 2×128B 数组) -
jemalloc 将后续 victim 紧凑排列:128B 数组 run 紧邻 80B struct run 下方
-
DEL v794→ 其 struct(80B)和 arrays(2×128B)进入 tcache 顶部(class-80 和 class-128 bin)
-
RESTOREcrafted payload → jemalloc 复用刚释放的块 → 数据线性溢出覆盖上方相邻 victim struct
-
被覆写字段:
nodes_mean=X、nodes_weight=Y、merged_nodes=k、cap=0x40000000
Oracle 的含义
这里借用了密码学中的”Oracle”概念——攻击者造出一个可以被反复”查询”的通道。正常 Redis 命令(DUMP、TDIGEST.ADD)本意是导出/更新 tdigest 数据,但攻击者通过堆溢出破坏了受害者 struct 的内部指针字段,使得这些命令的行为被劫持,变成了读/写任意内存地址的工具。
阶段二:构造任意内存读写通道(Oracle)
-
TDIGEST.INFO遍历所有 victim → 找到
cap == 0x40000000的受害者 key -
任意读
:
DUMP victim→TDigestRdbSave从nodes_mean(即 X)地址开始序列化k个 qword 作为 RDB 数据返回 -
任意写
:
TDIGEST.ADD victim <value>→ 将攻击者传入的 double 值写入nodes_mean(即 X)地址,同时将常量 1 写入nodes_weight(即 Y)地址 -
每次读/写后,
DEL中间 key → 重新执行RESTORE+ 堆溢出 → 用新的 X / Y / k 参数重建通道,实现反复多次的任意地址读写
阶段三:信息泄漏与 RCE
-
读取
CClosure+32→str_format→ PIE 基址 -
读取
write@GOT→ libc 基址 →system地址 -
读取
server.db→ db 数组 →db[1].keyskvstore -
验证
kvstore.dtype.hashFunction == dictSdsHash -
覆写
hashFunction为jmp *0x78(%rdi)gadget(hdr_iter_next) -
GET "<cmd> #<pad><system_addr>"→
dictFind→ gadget → system(cmd) -
恢复
hashFunction,删除临时 key
3.4 漏洞3 利用链详解(TopK 野指针释放)
阶段一:野指针释放与 UAF
-
喷射 800个 kvobj:22 字符键名 + 245 字符 value(value sds 为 jemalloc class-256)
-
RESTOREcrafted TopK payload(
heapSize=48, k=3, heap="H"*48) -
RESTORE 失败 → 错误路径 →
TopK_Destroy读取heap+56(超出实际堆大小,进入相邻 slab) -
heap+56处恰好是某个喷射 kvobj 的
robj.ptr(偏移 8 字节处) -
je_free(robj.ptr)→ 活 value sds 被释放 → kvobj 仍存活 → Use-After-Free
阶段二:UAF 占位 → 内存别名(Aliasing)→ 任意读写
内存别名(Memory Aliasing)核心机制
TopK_Destroy释放了受害 key 的 value sds(class-256 堆块,地址记为 V)。攻击者执行 SET k2(220 字符键名 → kvobj 本身分配在 class-256)→ jemalloc 将 k2 的 kvobj 分配到 V。此时同一块物理内存 V被两个 key 以不同视角看待:
受害 key:认为 V 是自己的 value 字符串内容
k2:其 kvobj 元数据(robj结构体,含 type / encoding / refcount / ptr 字段)也在 V 上
-
GETRANGE victim 0 23→ 读取 V 处 24 字节 → 偏移 16(
K2_PTR_OFF)恰好是 k2 的robj.ptr→ 泄漏 k2 value sds 地址(记为 S2) -
SETRANGE victim 16 <8字节>→ 改写 V+16 处的
robj.ptr→ 将 k2 的 value 指针重定向到攻击者指定的任意地址 T -
此后
GETRANGE k2 0 N→ 从地址 T 读取 N 字节(任意读) -
SETRANGE k2 0 <data>→ 向地址 T 写入数据(任意写)
通道能工作的原因:k2 的 value 是 RAW 编码且 refcount=1,GETRANGE/ SETRANGE直接操作 robj.ptr指向的 sds 缓冲区,没有额外的编解码跳转。
阶段三:信息泄漏与 RCE
-
Lua
string.formatCClosure →str_format→ PIE 基址 -
write@GOT→ libc 基址 →
system -
server.db→ db 数组
-
在 k2 的 value sds(S2)中构造 fake
dictType,hashFunction = jmp *0x78(%rdi)gadget -
找到可写的 db dict → 覆写
dict->type为 S2 -
GET "<cmd> #<pad><system_addr>"→ gadget → system(cmd)
-
恢复
dict->type,删除临时 key
3.5 终局:hashFunction 劫持 → system()
三组漏洞的终局(Endgame)都采用相同的控制流劫持技术——覆写数据库 dict 的 hashFunction指针:
控制流劫持流程
-
通过任意读找到一个空数据库
db[N],验证其dictType -
构造 fake
dictType,将hashFunction覆写为system(或 JMP gadget) -
SET dummykey→ 使
dict.used > 0(避免dictFind因used==0短路) -
GET "<shell_command>"→
dictFind调用hashFunction(key)→ 实际执行system("<shell_command>") -
执行完毕后恢复
dictType原值,删除临时 key → 服务器恢复正常
漏洞 2 和漏洞 3 使用了额外的 JMP gadget(hdr_iter_next中的 jmp *0x78(%rdi)),在 key 字符串偏移 0x78 处嵌入 system地址实现间接跳转。key 格式为 "<cmd> #<padding><system_addr>",#后内容被 shell 当作注释忽略,system实际只执行 #前的命令部分。
3.6 各漏洞的读写原语差异
| 漏洞 | 堆原语 | 读写构造方式 |
| — | — | — |
| 漏洞1(Stream) | Double Free | 喷射 22B 键名抢占 → 伪造 dictEntry → GETRANGE/SETRANGE |
| 漏洞2(TDigest) | 线性堆溢出 | 覆写 victim struct 指针 → DUMP/TDIGEST.ADD |
| 漏洞3(TopK) | 野指针释放 → UAF | 内存别名占位 → 劫持 robj.ptr→ GETRANGE/SETRANGE |
四、RCE 复现
4.1 环境
Redis 8.8.0Docker(含 RedisBloom 模块,默认捆绑)
利用脚本:T88_exploit.py(TopK 野指针释放 → RCE,CVE-2026-25589 补丁绕过)
4.2 编译 CRC64 签名库
利用脚本需要 CRC64 签名库来生成合法的 RDB payload 校验和:
4.3 执行利用
4.4 RCE 验证
五、实战考量
5.1 TCP 分片:最大的实战限制
本系列堆漏洞对 jemalloc 堆布局极端敏感。跨网络利用时,受限于网卡 MTU(通常 1500 字节),巨大的 Payload 会被 TCP 分片。这导致 Redis 在接收数据时频繁触发 querybuf动态扩容与内存释放,彻底破坏漏洞利用所需的精确堆布局,引发段错误(SIGSEGV)导致目标崩溃。
5.2 利用场景评估
| 场景 | 可行性 | 说明 | | — | — | — | | SSRF 攻击链 | 极高 | Web 应用 SSRF → gopher/dict 协议本地投递 — 最核心场景 | | 本地提权 / 容器逃逸 | 高 | 已获低权限 Shell,通过回环地址提权至 Redis 运行权限 | | K8s Sidecar 提权 | 高 | 同 Pod 本地容器间通信走 loopback | | 内网横向移动 | 极低 | TCP 分片导致堆布局不可控,目标大概率崩溃 | | 公网暴露 Redis | 极低 | 网络抖动使堆布局完全不可控 |
5.3 优势与局限
优势
-
覆盖面广
:三组漏洞覆盖 6.2.x – 8.8.x 全线主流版本
-
无破坏性
:利用后服务器不崩溃,执行后自动恢复环境
-
无需 DEBUG
:7.4/8.x 的 PoC 完全脱离 DEBUG 命令
-
补丁绕过能力强
:漏洞1 和漏洞3 均为已公开 CVE 补丁的二次绕过
-
利用稳定
:空实例成功率接近 100%(6.2: 10/10, 7.4: 5/5, 8.6: 25/25)
局限
-
网络敏感
:只能通过回环地址利用,跨网络几乎 100% 导致崩溃
-
需要凭据
:不具备未授权 RCE 能力,需先获取 AUTH 密码
-
并发干扰
:生产环境中其他客户端的并发操作会破坏堆布局
-
堆残留
:利用后堆中残留假结构体和悬空指针,不建议执行
FLUSHALL或SAVE -
版本依赖
:强依赖特定 libc/jemalloc 版本偏移,非官方构建需事先校准
六、修复建议
首要措施:ACL 限制高危命令
# redis.conf — 禁用利用链依赖的关键命令
rename-command RESTORE ""
rename-command DEBUG ""
# 或使用 ACL(Redis 6.0+)按用户精细控制
ACL SETUSER app -RESTORE -DEBUG -XGROUP on >password ~*
三组漏洞均依赖 RESTORE命令注入恶意 RDB payload。禁用该命令可从根本上阻断攻击。
| 措施 | 效果 | | — | — | | ACL 禁用 RESTORE / DEBUG / XGROUP | 阻断 payload 注入 → 根治 | | 升级 Redis 版本 | 8.8.0 修复漏洞1,但漏洞2/3 截至发布仍未修复 | | 使用强密码 + 定期轮换 | 提高认证门槛,减少凭据泄露风险 | | 网络隔离(禁止外部直连) | 配合 SSRF 防护 → 阻断最主要攻击入口 | | SSRF 防护(阻断 gopher/dict 协议) | 阻止 Web 层通过 SSRF 投递 Payload | | 监控异常命令模式 | 大量 RESTORE + XGROUP DELCONSUMER 组合为高危信号 |
参考
- berabuddies/redis-poc(PoC 及利用脚本)
- CVE-2026-25243(Stream consumer-group shared-NACK double free)
- CVE-2026-25589(TopK wild free)
- Redis PR #15081(漏洞1 修复,合并至 8.8.0)
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:博智非攻研究院 博智非攻研究院 博智非攻研究院《【漏洞分析】Redis 6.2–8.8 认证后 RCE 漏洞深度分析与复现》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论