上一篇 ThinkPHP 的 POP 链调试,这次 WordPress 6.9.4(本地授权靶机),源码逐行核实。一句话:RCE 不是新漏洞,是同一个 batch 错位 + 同一个注入点,从”读”变”写”——盲注当”眼睛”偷数据,这篇当”手”伪造数据,再借平台机制洗成真权限。
0x01 承接:同一个错位,换三样东西
错位机制见 check 篇:batch 路由按 matches[] 匹配 handler,探针让 matches[] 短一格 → 载体请求偷到下一个路由的 handler → author_exclude 原样进 SQL。从读变写只改三处:
| check(只读) | RCE(写) | |
|---|---|---|
| 注入语句 | SELECT IF(...SLEEP...) | 1) AND 1=0 UNION ALL SELECT 伪造行... -- - |
| 内层载体 | /wp/v2/users?... | /wp/v2/widgets?author_exclude=...&per_page=-1&orderby=none&context=view |
| 额外请求 | 无 | 追加 2 次 POST /wp/v2/users(创建管理员) |
两个关键开关:
- 载体从 users 换 widgets:阶段一校验查”载体自己的路由参数表”,表里没有的参数被跳过——users 表有
per_page(拦),widgets 没有(放行); per_page=-1是读写切换的总开关:widgets 返回全量行,伪造行才有机会进缓存。
外层还有一次同样的错位:[PRIMER, postsCarrier, /batch/v1] 里 /batch/v1 只负责”献出 handler”,真正执行内层批的是前面的载体。
0x02 六环总览
环1 SQL 注入 → 内存对象投毒 UNION 伪造行被 get_results() 取回,
update_post_caches() 写进内存缓存(可信!)
环2 oEmbed 缓存 → 持久化写入 oEmbed 渲染时把伪造字段经 wp_update_post
"稀疏合并"写进数据库 ← 假数据落地成真
环3 层级循环检测 → 状态转换 伪造的父子关系触发修环,post 状态 future → publish
环4 Customizer 发布 → 权限提升 发布流程把当前用户切成管理员
环5 parse_request 动态钩子 → REST 重入 第二次修环触发钩子,重入 REST API
环6 创建管理员 → webshell → RCE 管理员身份 POST /wp/v2/users 建号,登录传马
环 2 最巧:7 条毒化记录一次注入(1 条 seed + 一组 changeset,含两条父子环)把后续每一环要的”数据形态”都预置好,全靠 WordPress 机制接力——只写一次,平台洗白六次。
调试经验两条:断点按环分组(25 个点一条链,条件断点按 URL 片段过滤,否则批处理一进来就淹没);三层超时是调试杀手(PHP/网关/浏览器逐层超时,debug 一会就中断不是错觉,--sleep 判据要算上断点停驻时间)。
0x03 check vs RCE 对照(收尾)
| 维度 | check 盲注 | RCE 六环 |
|---|---|---|
| 注入的角色 | 眼睛(读) | 手(写) |
| 关键语句 | SELECT IF(...SLEEP...) | UNION ALL SELECT 伪造行 |
| 剩余攻击面 | 数据库内容 | 内存缓存 → 数据库 → 权限系统 |
| 调试重心 | 响应时间差 | 25 个断点的状态传递 |
最大启发:注入点的价值上限不取决于注入本身,而取决于取回数据的代码怎么用——update_post_caches 无条件信任结果那一刻,数据库边界即权限边界。
相关旧文
- SQL 注入深挖、PHP 代码审计入门靶场复盘
- 环境配置:VSCode + Xdebug 调试 PHP(线上,断点环境同款)