【CVE-2026-23479】AI 发现 Redis 中已有 2 年历史的 RCE 漏洞
原创
骨哥说事
骨哥说事
骨哥说事
2026年6月4日 09:48
上海
在小说阅读器读本章
去阅读
| *声明:*文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。 |
防走失:https://gugesay.com/
不想错过任何消息?设置星标↓ ↓ ↓
Xint Code 在 Redis 的阻塞客户端代码路径中发现了一个释放后使用漏洞,可导致远程代码执行。
CVE-2026-23479 - Redis unblockClientOnKey 函数中的释放后使用漏洞导致远程代码执行
CVE-2026-23479 是 Redis 阻塞客户端代码路径内部的一个释放后使用漏洞,允许经过身份验证的用户在 Redis 主机上执行任意操作系统命令。
该释放后使用情况发生在 unblockClientOnKey() 函数中(src/blocked.c),该函数在未检查客户端是否因副作用而被释放的情况下,继续访问客户端结构体之前,就调用了 processCommandAndResetClient()。
该漏洞由Xint Code(一个完全自主的、由人工智能驱动的安全分析工具)发现,并在 ZeroDay.Cloud 2025 大会(2025年12月10-11日,伦敦)上演示了可用的远程代码执行利用。Redis 团队已于2026年5月5日在 7.2.x、7.4.x、8.2.x、8.4.x 和 8.6.x 等多个发布系列中推送了修复补丁。
既然补丁已发布,本文将通过分析根本原因、详细剖析完整的利用链,并提供修复指导。
感谢 Redis 安全团队的认可以及对此问题的彻底解决。
如果你在生产环境中运行 Redis,请立即采取行动。大多数经过身份验证的 Redis 连接都可以触发此释放后使用漏洞,并在主机系统上执行任意代码。
哪些版本受影响
运行 7.2.0 或更高版本的大多数Redis部署都存在此漏洞,包括任何默认用户保留其开箱即用权限的部署。该漏洞在 7.2-rc1 版本中引入,并于 7.2.0 版本普遍可用。修复补丁已于2026年5月5日覆盖所有五个维护中的发布系列。
| 发布系列 | 受影响版本 | 已修复版本 |
|---|---|---|
| Redis 7.2.x | 7.2.0 – 7.2.13 | 7.2.14 |
| Redis 7.4.x | 7.4.0 – 7.4.8 | 7.4.9 |
| Redis 8.2.x | 8.2.0 – 8.2.5 | 8.2.6 |
| Redis 8.4.x | 8.4.0 – 8.4.2 | 8.4.3 |
| Redis 8.6.x | 8.6.0 – 8.6.2 | 8.6.3 |
所需权限
触发该漏洞需要一个具有特定权限集的经过身份验证的会话:通过 CONFIG SET maxmemory-clients 调整客户端内存限制,通过 EVAL 执行 Lua 脚本,发出流命令(XREAD、XADD),以及执行基本的 SET/GET 操作。
- 以 ACL 术语表示,即需要
@admin、@scripting、@stream和@read/@write权限。 - 在默认的 Redis 部署中,默认用户拥有所有这些权限,而在实践中,这些权限通常整合在单个应用程序或运维人员角色中。
- 完全拒绝
CONFIG命令的部署可以阻止此特定利用链,但并不能消除底层漏洞的存在。
生态系统暴露情况
Wiz的分析显示,80%的云环境使用Redis。在这些Redis实例中,接近85%在配置时没有设置密码,这极大地增加了攻击面。
攻击流程与影响
攻击者需要一个拥有 CONFIG SET、EVAL、流命令和基本 SET/GET 权限的已认证Redis会话。利用过程分三个阶段:信息泄露、触发释放后使用、以及代码执行。
- 信息泄露:攻击者运行一个单行Lua脚本,打印
redis.call的字符串表示形式,泄露一个堆指针,为后续阶段提供锚点。 - 触发释放后使用并控制堆内存:攻击者调整客户端内存限制,将一个内存占用量大的客户端置于流(stream)上阻塞等待,然后降低内存限制并“唤醒”流。驱逐(eviction)路径会在调用中途释放被阻塞的客户端;
unblockClientOnKey()函数继续操作已被释放的内存。随后,一个SET操作\\用攻击者控制的伪造客户端结构体重新“占领”了”\\那块已被释放的内存区域。 - 代码执行:当 Redis 访问这个已被覆写的“客户端”结构以执行其正常的内存计数时,
updateClientMemoryUsage()会使用攻击者控制的字段对一个越界地址执行递减操作(decrement)。该利用技术将此原语瞄准一个GOT(全局偏移表)表项,将strcasecmp()的地址重定向到system(),从而使攻击者能够以Redis守护进程的权限执行任意命令:完全访问每个数据库中的每个键、配置文件中的凭据,以及能够访问相邻服务的网络位置。
技术深度剖析
CVE-2026-23479 是 unblockClientOnKey() 函数中的一个释放后使用漏洞(src/blocked.c)。
存在漏洞的代码
当一个键事件满足先前阻塞的命令时,会调用 unblockClientOnKey()。如果该客户端有已排队的命令(设置了 CLIENT_PENDING_COMMAND 标志),该函数会通过 processCommandAndResetClient() 分派该命令,然后继续操作同一个客户端指针 c:
static void unblockClientOnKey(client *c, robj *key) {
...
if (c->flags & CLIENT_PENDING_COMMAND) {
c->flags &= ~CLIENT_PENDING_COMMAND;
c->flags |= CLIENT_REEXECUTING_COMMAND;
...
processCommandAndResetClient(c);
if (!(c->flags & CLIENT_BLOCKED)) {
if (c->flags & CLIENT_MODULE) {
moduleCallCommandUnblockedHandler(c);
} else {
queueClientForReprocessing(c);
}
}
exitExecutionUnit();
afterCommand(c);
c->flags &= ~CLIENT_REEXECUTING_COMMAND;
...
}
}
processCommandAndResetClient() 的头部注释明确警告,处理命令的副作用可能导致客户端被释放:
/* The function returns C_ERR in case the client was freed as a side effect
* of processing the command, otherwise C_OK is returned. */
int processCommandAndResetClient(client *c) {
调用者忽略了返回值。 如果 processCommandAndResetClient() 释放了 c,那么后续对 c 的每次访问(读取标志位、调用 queueClientForReprocessing(c) 和 afterCommand(c)、以及最后的 flags &= ~CLIENT_REEXECUTING_COMMAND 写入操作)都将在已被释放的内存上进行。
该漏洞由两个不同的提交引入。PR #11012(2023年1月)重构了取消阻塞的路径,通过正常的命令管道分派排队的命令,从而引入了对 processCommandAndResetClient()的未检查调用。在当时,这个调用是该函数对 c 所做的最后一件事,因此丢弃其文档中提到的 C_ERR 返回值并无影响。两个月后,PR #11568(2023年3月)将这次分派包装在执行单元和解除阻塞处理器的记账逻辑中,增加了在分派之后对 c->flags 的读取、对 queueClientForReprocessing(c) 的调用以及 afterCommand(c) 的调用。这两个变更组合在一起,导致了释放后使用漏洞。
为何这个漏洞可被利用
两种特性使得这个释放后使用漏洞能够直接转化为武器。
第一,攻击者可以在释放后利用“确定性内存回收窗口”。在 processCommandAndResetClient() 返回后,unblockClientOnKey() 会立即访问 c,但由于 Redis 的 zfree()并不会清零内存,所以已释放的内存槽中仍存有原始客户端的残留数据,因此这些读取操作会看到看似合理的数据,函数返回时不会崩溃。其中一次读取会将 c(现已释放的指针)传递给 queueClientForReprocessing(),该函数将这个已释放的指针添加到 server.unblocked_clients 列表中,以便在同一轮事件循环结束时处理。 这个间隙为攻击者提供了一个确定性的内存回收窗口:Redis 是单线程的,并且按顺序处理连接中的命令,因此攻击者如果在唤醒命令后立即管道化发送一个 SET key value 命令,就能在Redis清空该队列之前,用一个新的SDS(简单动态字符串) 重新占据那块刚被释放的内存区域。
第二,官方的 Redis Docker 镜像发布的 redis-server 只启用了部分RELRO(重定向只读),这使得运行时 GOT 的延迟绑定部分(.got.plt)是可写的。结合可控制的客户端结构体,部分 RELRO 将一个由攻击者选择的单次递减操作,变成了一个完整的通过 GOT 覆写实现代码执行的利用原语。
利用步骤详解
阶段一:信息泄露。攻击者调用一个单行Lua脚本,将Redis端的可调用对象转换为其字符串表示形式:
EVAL \"return tostring(redis.call)\" 0
返回的字符串 function: 0x... 包含一个Lua堆指针。减去固定偏移量可得到Redis堆中一个稳定的地址。
阶段二:触发释放后使用并回收堆内存。
- 设置内存限制:攻击者首先设置较高的内存限制并禁用客户端输出缓冲区限制,让目标客户端可以积累大量待回复数据而不至于过早被驱逐。
CONFIG SET maxmemory-clients <high>
CONFIG SET client-output-buffer-limit \"normal 0 0 0\"
SET <big-key> <几兆字节的'A'>
2. “受害者”(evictee)连接:在第二个连接上,攻击者管道化发送一个 MULTI/EXEC 块,其中排入大量 GET <big-key> 调用并且不读取响应,使得该客户端的待输出缓冲区膨胀。
3. 阻塞于流:同一个连接发出一个针对攻击者控制的流的阻塞命令,如 XREAD BLOCK 0 STREAMS <stream-key> 0-0。此时该客户端在等待流的更新,处于阻塞状态。
4. 触发连接:在第三个连接上,攻击者通过一个事务原子性地降低内存限制并唤醒被阻塞的客户端。
MULTI
CONFIG SET maxmemory-clients 1
XADD <stream-key> * f v
EXEC
XADD 会标记stream-key为就绪状态,当 EXEC 完成后,Redis会处理就绪列表并调用 unblockClientOnKey()。在随后的 processCommandAndResetClient() 调用中,evictClients() 会将内存占用量大的“受害者”客户端驱逐并释放。 控制权返回 unblockClientOnKey(),它继续访问已释放的 c。这些访问由于内存未被清零而得以幸存。其中一次访问调用了 queueClientForReprocessing(c),将已释放的指针加入待处理列表 server.unblocked_clients。
5. 内存回收:紧接着,触发连接的管道化 SET 命令运行,它会申请一块新的SDS内存来存储值,而这个SDS对象很有可能就分配在刚刚释放的那个客户端结构体所占用的内存区域,从而“回收”了那块内存。SET 的值是一个精心构造的伪造客户端结构体的字节数据。
SET reclaim:<rand> <fake-client-bytes>
这个伪造的结构体有两个任务:一是让 processUnblockedClients() 在处理时认为它是一个有效的客户端;二是引导后续的 updateClientMemoryUsage() 调用中的越界写入操作瞄准GOT表。攻击者将伪造的客户端标志位恰好设置为 CLIENT_PENDING_COMMAND,这会引导执行流程走到 updateClientMemoryUsage()。其他可能引发崩溃的字段会被清零或填充为泄露的堆指针。关键是两个字段:last_memory_type 成为越界索引,而 last_memory_usage 成为递减的量。
阶段三:GOT表覆写与命令执行。当Redis开始处理 server.unblocked_clients 列表,并将那个已被覆写的“客户端”当做真实客户端处理时,它会在 updateClientMemoryUsage() 中执行例行的内存计数更新:
void updateClientMemoryUsage(client *c) {
...
server.stat_clients_type_memory[c->last_memory_type] -= c->last_memory_usage;
...
}
server 是一个位于 .data 段的全局变量。此处,c->last_memory_type 和 c->last_memory_usage 完全来自攻击者控制的伪造客户端结构体。结果是在指向 server 全局变量的任意偏移处执行了一个任意大小的8字节递减操作。由于部分RELRO,GOT表仍是可写的。攻击者通过精心设定这两个值,将GOT表中 strcasecmp 的入口地址重写为 system 函数的地址。
当Redis下一个命令被解析时,它会调用 strcasecmp()(现在实际上调用了 system()),命令字符串将作为shell命令被执行!
修复方案
立即打补丁
该修复已提交到上游,并于2026年5月5日随 Redis 7.2.14、7.4.9、8.2.6、8.4.3 和 8.6.3 版本一同发布。
请升级到当前所在发布系列的已修复次版本。在稳定系列内的次版本升级通常设计为兼容的,停留在旧版本比升级风险更高。
云托管的Redis服务可能会在其自己的时间表上推出修复版本。请查阅你云服务提供商的公告。
如果暂时无法打补丁
在安排打补丁期间,请采取以下措施降低风险:
- 收紧网络访问:确保Redis不直接暴露在互联网;绑定到回环地址或私有子网,并为远程访问启用TLS。
- 收紧ACL:优先使用每用户ACL,而非单一的共享默认用户;从任何不严格需要的角色中移除
@admin、CONFIG和@scripting权限。完整的利用链要求从一个会话中能访问CONFIG SET、EVAL、流命令和SET/GET。 - 尽可能禁用脚本:如果工作负载不使用Lua脚本,拒绝对
@scripting类别的访问,以消除第一阶段的信息泄露。 - 凭据管理:轮换广泛分发的Redis凭据;优先使用每服务身份和短时凭据。
负责任的披露时间线
- 2025年12月10-11日: Xint Code 团队在 ZeroDay.Cloud 2025大会上发现并演示了该漏洞。
- 2026年5月5日: 修复补丁提交至上游仓库。
- 2026年5月5日: 修复版作为 Redis 7.2.14、7.4.9、8.2.6、8.4.3 和 8.6.3 正式发布。
- 2026年5月5日: CVE-2026-23479 漏洞信息公开发布。
最后的思考
CVE-2026-23479 是一个源自未检查的驱逐(eviction)副作用的隐蔽漏洞。该漏洞随 Redis 7.2.0 版本于2023年8月发布,在超过两年的时间里一直可被利用,期间历经了多次安全审查却未被发现。
该问题最终由Xint Code(一款专为大型代码库深度发现漏洞而设计的AI安全分析工具)发现并得以演示。
参考资料
- ZeroDay.Cloud 赛事概览与名人堂
- Wiz: ZeroDay.Cloud 2025 幕后故事
- 修复提交记录 (src/blocked.c)
- Xint Code 发布公告及设计合作伙伴信息
- Redis ACL 文档
- GitHub 安全公告 GHSA-93m2-935m-8rj3
原文:https://www.zeroday.cloud/blog/redis-cve-2026-23479-deep-dive
- END -
感谢阅读,如果觉得还不错的话,动动手指给个三连吧~
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论