Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/captain-woof/cve-2026-25243
漏洞分析漏洞利用后渗透利用渗透测试红队数据库安全二进制利用
GitHubcaptain-woof/cve-2026-25243

CVE-2026-25243

CVE-2026-25243 的稳定 POC(Redis RESTORE 双重释放 -> 远程代码执行)

查看仓库
111个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2026-25243 — Redis RESTORE 双重释放 → 远程代码执行

已在 Rocky Linux 8.10、aarch64、Redis 8.6.2、jemalloc 5.3.0 上验证。

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

TLDR;稳定利用,适用于多种操作系统发行版和架构。


执行摘要

这是什么? 这是 Redis 中的一个内存破坏漏洞,允许经过身份验证的攻击者以 Redis 用户身份运行任意命令。该攻击在现实世界中可行,且只需一条 RESTORE 命令——这是正常的 Redis 操作,并非仅限管理员。该利用可在不到一秒内实现完整 RCE。

影响? 任何经过身份验证的 Redis 客户端都可以触发它,且破坏是彻底的:在 Redis 进程中执行任意代码(在容器中通常以 root 身份运行)。若不修补 Redis 本身,则无法缓解。

它是如何工作的(概览)? Redis 有一个序列化功能(RESTORE),它接收二进制数据 blob 并将其重建为 Redis 对象。验证 blob 格式的代码与反序列化它的代码在如何解析某些序列上存在分歧——攻击者利用这个 bug 破坏堆。一旦堆被破坏,攻击者便能够读写 Redis 进程中的任意内存地址,并由此劫持服务器的内部状态以执行 shell 命令。

真正的利用技术: 这不是一次简单的崩溃。而是一个堆利用链:破坏 → 重叠 → 任意读写 → 信息泄漏 → 找到服务器结构体 → 劫持函数指针 → RCE。该利用运行 9 个阶段,需要运行时泄漏多个地址、解析二进制结构并检测内存别名。它能跨架构(x86-64、aarch64 等)工作的原因在于所有地址都是从目标自身泄漏的,而非假设。


1. 漏洞——详细分析

CVE-2026-25243 是一对可通过单条经过身份验证的 RESTORE 命令触发的双重释放 bug。RESTORE key ttl <serialized-value> 会将攻击者控制的 RDB blob 反序列化;这两个 bug 都存在于检查 blob 的与将其实体化的之间的间隙中。

验证器
转换器

Bug 1 — 传统 zipmap 转换(CWE-415,本利用所用路径)。 zipmap 验证器(zipmapValidateIntegrity())和转换器(zipmapNext())在冗余长度编码上存在分歧。小长度 4 可以合法地以五字节长形式 FE 04 00 00 00 写入。验证器消耗一种字节数,转换器消耗另一种——造成 4 字节的解析失步。因此,转换器遍历的是与已验证结构不同的结构,lpSafeToAdd() 在字段已经插入字典后失败,清理路径会释放该字段两次:一次通过 dictRelease(),另一次通过 sdsfree()。

Bug 2 — 流消费者 PEL 加载(CWE-415)。 在 rdbLoadStreamConsumersGroup() 中,包含重复条目 ID 的消费者 PEL 会使第二次 raxTryInsert() 失败,该函数会对此前仍由组全局 PEL 拥有的 streamNACK 调用 streamFreeNACK()。被释放两次。(可通过 --vuln-type stream 选择。)

这两个 bug 中的任何一个都会为攻击者提供一块同时空闲且仍被引用的内存——堆重叠利用的经典起点。

影响: 经过身份验证的 Redis 客户端(无需管理员权限,RESTORE 是普通数据命令)能够以 redis 用户身份执行任意代码——在默认容器镜像中为 root。

2. 利用的工作原理

九个阶段,每个阶段都将较弱的原语转化为更强的原语:

阶段获得的原语机制
0目标画像INFO server / INFO memory → 版本、架构、发行版、pid、可执行文件路径、启动时间、分配器
1双重释放畸形 zipmap(或 stream)RESTORE
2两个键共享内存在释放的 chunk 上喷洒标记键,检测别名,然后通过其孪生键覆盖一个键的 SDS 头,将其膨胀为 1 MB 的“memview”
3任意读写在 memview 中找到一个 INCRBYFLOAT 对象,劫持其 ptr 字段:对该键执行 GETRANGE/SETRANGE 现在可以读写任意地址
4进程镜像指针在堆中向后扫描,寻找 redis-server 镜像内的值
5&server向下遍历到 ELF 头,解析程序头,导出可写段,匹配 server.pid
6内存中的载荷将 "/bin/sh", "-c", "<cmd>" 以及 argv 数组写入 memview
7被劫持的结构体覆盖 server.executable、server.exec_argv 和 server.enable_debug_cmd
8RCEDEBUG CRASH-AND-RECOVER → restartServer() → execve(server.executable, server.exec_argv, environ)

如何触发

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

验证:

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

3. 变更日志

2026-08-06 — 可移植性、可靠性和速度重构

起点:该利用仅支持 x86-64,并且在 aarch64 目标上的第 3 阶段崩溃。最终状态:在 aarch64 Rocky Linux 8.10 上不到一秒内实现完整 RCE,仅 116 条 Redis 命令。

a) 运行时目标指纹识别(新增,阶段 0)。 不再对目标做任何假设。INFO server + INFO memory 可得到 Redis 版本、CPU 架构(来自 os: 行)、发行版系列(从 gcc_version 推断)、分配器,以及——最重要的是——三个验证锚点:process_id、executable 和精确的 stat_starttime(server_time_usec/1e6 - uptime_in_seconds)。后续阶段会与这些值对照,而不是猜测。

b) 与架构无关的内存布局。 四个硬编码的 x86-64 常量(BINARY_ADDR_MIN/MAX、HEAP_ADDR_MIN/MAX)被一个按架构划分的表(ARCH_PROFILES)取代,该表涵盖 x86_64、aarch64(包括 39 位和 48 位虚拟地址)、riscv64、ppc64le 和 s390x,并为每种架构提供 ET_EXEC 和 ET_DYN 两种布局,此外还有一个宽泛的通用回退方案用于任何未列出的架构。这正是该利用在此目标上失败的实际原因:泄漏的指针 0x0000ffff8a5fdf32 是一个完全有效的 aarch64 mmap 地址,却被 x86-64 的范围检查拒绝了。

c) 基于共识的泄漏验证(阶段 3)。 扫描不再信任硬编码的堆窗口,而是收集 memview 中每一个结构上有效的 1337.NNNNNN 对象,并要求至少两个对象推导出相同的 memview 基地址(ptr - offset_of_value)。实际上有 502 个候选者达成一致,这是任何范围表都无法提供的证明。确认后的指针随后在运行时校准堆窗口。格式验证也移到(往返开销昂贵的)写控制测试之前。

d) 阶段 3 扫描边界(缺陷修复)。 扫描此前一直运行到硬编码的 10 MB,而 memview 只有 1 MB,因此它读取超过末尾,得到空回复并以 AssertionError: Empty data from memview 中止。现在它受 memview 实际 STRLEN 限制,每次往返读取 256 KB 而不是 64 KB,并且毫无意义的 6×1 秒重试睡眠循环已被移除。

e) 阶段 5 重写:ELF 引导、无崩溃(重头戏)。 旧实现从镜像指针向前扫描,探测地址并读取垃圾 SDS 头声称的任何长度。在此目标上,它径直越过只读段末端,进入 0x715000 处的未映射空洞,导致服务器崩溃(getrangeCommand → memcpy 中的 SIGSEGV)。盲扫描无法变得安全。替代方案是确定性的:

  1. 找到镜像基址。 从最低的泄漏镜像指针逐页向下遍历。探测是无成本的:每个 ELF64 镜像的前五个字节都是 7f 45 4c 46 02,而 sdslen() 从 ptr[-1] 获取其 flags 字节——因此将劫持对象指向 base+5 会使 e_ident[EI_CLASS]=0x02 成为 flags 字节,即 SDS_TYPE_16,其长度为 base+0 处的 uint16 = 0x457f(大端 0x7f45)。STRLEN 恰好为 17791 就是 ELF 签名。不需要本地副本——头部是从目标自身内存中读出的。

  2. 解析程序头,以获取每个 PT_LOAD 段的精确运行时边界(处理 PIE 目标的 ET_DYN 加载偏置)。之后每次读取都被限制在真实的内存映射内,因此未映射空洞崩溃在结构上已不可能发生。

  3. 伪造一个 SDS 头,位于可写段的一个清零槽位中,这使得整个 .data/.bss 只需几次往返即可读取,而不是数十万次字节探测。被覆盖的字节会被保存并恢复。

  4. 将 server.pid 与 INFO 中的 pid 进行匹配——一个精确的 8 字节相等性测试——然后通过解引用 server.executable 并将字符串与 INFO 的 executable 比较来确认。旧代码接受宽松的七字段形状启发式;现在该结构体被确定性地识别。

f) 阶段 4 加固。 Lua 验证器接受完整的按架构范围列表(因此位于 0x400000 的非 PIE 镜像和位于 0xaaaa… 的 PIE 镜像都能被识别),并排除已校准的堆窗口。它返回多个候选者而不是一个,因此糟糕的选择只花费一次重试,而不是整个运行。

g) 阶段 7 自我验证。 enable_debug_cmd 此前通过硬编码的 stat_starttime - 0x3c 定位。现在预期的 stat_starttime 值可以从 INFO 精确得知(3 秒窗口而不是 30 天),结构体读取窗口从 4 KB 扩大到 32 KB(stat_starttime 位于偏移 0x9e0,远超旧限制),而且——决定性的是——每个候选偏移都通过实时 oracle 验证:设置该字节,发送 DEBUG SET-ACTIVE-EXPIRE 1,查看服务器是否接受。错误的猜测会在下一次尝试前被恢复,因此该标志可在任何构建上被找到,而非假定。-0x3c 仍会首先尝试,并已确认对 8.6.2(偏移 0x9a4)正确。

h) 写入真正生效(阶段 5)。 setrangeCommand() 调用 dbUnshareStringValue(),除非 encoding == RAW && refcount == 1,否则该函数会复制该值。现在在通过劫持指针进行第一次写入之前,encoding 字节会被清零,因此写入会到达目标地址,而不是私有副本。

i) 载荷简化。 所有反向连接/反弹 shell 机制、ASCII 横幅以及附加的 ;sleep 5 均被移除。载荷恰好是 /bin/sh -c '<--cmd>',没有其他内容。--cmd 默认为 id > /tmp/pwned123.txt。

j) 速度。 阶段 4 收集 3 个候选者而不是 8 个;阶段 5 用约 40 次批量读取替代约 10^5 次字节探测;阶段 3 使用 256 KB 读取,并跳过未通过本地验证的候选者的往返。整个链路:116 条命令,<1 秒。

结果: 在目标容器的 /tmp/pwned123.txt 中写入 uid=0(root) gid=0(root) groups=0(root)。

可移植性说明

  • 架构是检测出来的,而非假设。基于 ELF 的阶段 5 在构造上与架构无关(它读取目标自身的程序头),并同时支持 PIE 和非 PIE 镜像、小端和大端。
  • 发行版仅为操作者参考而报告;该利用对其没有功能性依赖。唯一的文件系统假设是 /bin/sh,这是 POSIX 和 FHS 所要求的。
  • 版本:RDB 版本和流结构体大小根据 redis_version 选择(支持 7.x 和 8.x)。结构体字段偏移(executable=24、exec_argv=32)遵循 LP64 ABI,而 enable_debug_cmd 在运行时发现并验证,而非硬编码。
  • 32 位目标会在阶段 0 被明确拒绝(载荷构造的是 64 位指针),而不是在之后以晦涩的方式失败。

2026-08-06(稍后)— 重复运行测试后的稳定性加固

在默认 zipmap 路径上测得 13/13 次成功运行(5 次 + 8 次连续),每次都在 ≤1 秒内完成。只有重复执行时才出现的三个问题现已修复:

k) 阶段 0 中的 SAVE 竞争。 当之前一次运行(或 redis 自身)的后台保存仍在进行时,一次运行可能以 ERR Background save already in progress 中止。现在 SAVE 最多重试 15 秒,如果仍然失败,运行会在没有检查点的情况下继续,而不是中止。

l) 目标重启期间重连(--connect-retries,默认 10)。 一次失败的尝试会使堆处于损坏状态,因此下一次运行的 FLUSHALL 会释放被污染的内存块并使服务器宕机。服务器会在几秒后重启,且完全可被利用,因此阶段 0 现在会重连并重试,而不是失败。我们自身的验证错误(不支持的版本/架构)绝不会重试。这消除了压力测试中大约每 3 次运行出现 1 次的间歇性“stage 0 failed with an empty error”问题。

m) sizeof(streamNACK) 已针对 8.6.x 修正。 --vuln-type stream 路径此前喷洒了错误的 jemalloc size class,因为该结构体被假定为 24 或 32 字节。在 8.6.2 中它是 64 字节(delivery_time、delivery_count、consumer、cgroup_ref_node、streamID id、pel_prev、pel_next)。有了正确的大小,stream 路径现在能达到阶段 5,而不是在阶段 2 以“key overlap not found”失败。

已知限制

  • --vuln-type stream 在 8.6.2 上并不可靠。 在大小修正后,它能完成双重释放、重叠、任意读写原语和 ELF 解析,但随后会破坏 keyspace:服务器在读取 NULL+8 处的 o->ptr 时死于 setrangeCommand,即键查找返回了损坏的对象。它所释放的 64 字节 chunk 与其他活跃分配共享,因此其附带破坏远高于 zipmap 路径。请使用默认的 --vuln-type zipmap,该路径 13/13 次成功。
  • --random-heap-massage(先写入 100k 个随机键)会成功,但并非总是如此——被喷洒的堆有时会将双重释放的 chunk 放在没有任何标记键落地的位置。重新运行即可成功。
  • 仅在 aarch64 / Rocky Linux 8.10 / Redis 8.6.2(非 PIE ET_EXEC) 上验证。x86-64 和 PIE 路径已实现,且构造上与架构无关,但本次会话中未针对实际目标执行。
下载工具