• DOM Clobbering的原理及应用


    DOM Clobbering的原理及应用

    假设有一段代码,有一个按钮以及一段 js 脚本,如下所示:

    
    
    
      
      
    
    
    
      
      
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    现在请你用最短的代码,实现出点击按钮时会跳出 alert(1)这个功能。

    可以这样写:

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

    那如果要让代码最短,你的答案会是什么?

    0x02 DOM 与 window 的量子纠缠

    你知道 DOM 里面的东西,有可能影响到 window 吗?

    就是你在 HTML 里面设定一个有 id 的元素之后,在 JS 中就可以直接操作:

    <button id="btn">click mebutton>
    <script>
      console.log(window.btn) // 
    script>
    
    • 1
    • 2
    • 3
    • 4

    由于 JS 的作用域规则,你就算直接用 btn 也可以,因为在当前的作用域找不到时就会往上找,一路找到 window

    所以前面那道题的答案是:

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

    不需要 getElementById,也不需要 querySelector,只要直接用与 id 同名的变量去拿,就能得到。
    在这里插入图片描述

    而这个行为在 HTML 的说明文档中是有明确定义的,在 7.3.3 Named access on the Window object

    节选两个重点:

    1. the value of the name content attribute for all embed, form, img, and object elements that have a non-empty name content attribute
    2. the value of the id content attribute for all HTML elements that have a non-empty id content attribute

    也就是说除了 id 可以直接用 window 存取,embed, form, imgobject 这四个标签用 name 也可以操作:

    <embed name="a">embed>
    <form name="b">form>
    <img name="c" />
    <object name="d">object>
    
    • 1
    • 2
    • 3
    • 4

    但是知道这个有什么用呢?有,理解这个规则之后,可以得出一个结论:

    我们是有机会通过 HTML 元素来影响 JS 的!

    而把这个手法用在攻击上,就是标题的 DOM Clobbering。以前是因为这个攻击手段才第一次知道 clobbering 这个单词的,查了一下发现在计算机专业领域中有覆盖的意思,就是通过 DOM 把一些东西覆盖掉来达到攻击的手段。

    为了进一步分析 DOM Clobbering,假设我们有以下 JavaScript 代码

    if (window.test1.test2) {
        eval(''+window.test1.test2)
    }
    
    • 1
    • 2
    • 3

    如果我们想利用Dom Clobbering技巧来执行任意的js,需要解决两个问题

    1)利用html标签的属性id,很容易在window对象上创建任意的属性,但是我们能在新对象上创建新属性吗?

    2)怎么控制DOM elements被强制转为string之后的值,大多数的dom节点被转为string后是[object HTMLInputElement]。

    让我们从第一个问题开始。最常引用的解决方法是使用

    标签。标记的每个都属于后代,该属性引用name属性可以取到。考虑以下示例:

    
      
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    为了解决第二个问题,我创建了一个简短的 JS 代码,它遍历 HTML 中所有可能的元素并检查它们的toString方法是否继承自Object.prototype或以另一种方式定义。如果它们不继承自Object.prototype,那么可能[object SomeElement]会返回其他东西。

    Object.getOwnPropertyNames(window)
    .filter(p => p.match(/Element$/))
    .map(p => window[p])
    .filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)
    
    • 1
    • 2
    • 3
    • 4

    代码返回两个元素:HTMLAreaElement)和HTMLAnchorElement)。在元素的情况下,toString只返回一个href属性值。考虑这个例子:

    
    
    

    此时,似乎如果我们要解决原来的问题(即window.test1.test2通过 DOM Clobbering攻击),我们需要类似于以下的代码:

    • 1
    • 2
    • 3

    问题是它根本不起作用;test1.test2undefined。虽然元素确实成为 的属性

    ,但同样的情况不适合`。

    不过,这个问题有一个有趣的解决方案,它适用于基于 WebKit 和 Blink 的浏览器。假设我们有两个相同的元素id

    click!
    click2!
    
    • 1
    • 2

    那么我们在访问时会得到什么window.test1?直觉希望获得具有该 id 的第一个元素。然而,在 Chromium 中,我们实际上得到了一个HTMLCollection!

    这里特别有趣,我们可以HTMLCollection通过 index(01示例中)以及 访问其中的特定元素id。这意味着window.test1.test1实际上是指第一个元素。事实证明,设置name属性也会在HTMLCollection. 所以现在我们有以下代码:

    click!
    click2!
    
    • 1
    • 2

    我们可以通过name访问第二个awindow.test1.test2

    因此,回到eval(''+window.test1.test2)通过 DOM Clobbering进行利用的原始练习,解决方案是

    
    
    • 1

    ok 至此,前面基础知识铺垫完毕,我们继续看这道题

    这道题是一个为了防御xss攻击写的函数,但我们可以通过上述所学的DOM Clobbering进行绕过。

    <script>
         //http://127.0.0.1/domfilter/demo6.html#
         const data = decodeURIComponent(location.hash.substr(1));
         const root = document.createElement('div');
         root.innerHTML = data;
    
         //这里模拟了XSS过滤的过程,方法是移除所有属性
         for (let el of root.querySelectorAll('*')) {
             for (let attr of el.attributes) {
                 el.removeAttribute(attr.name);
             }
         }
         document.body.appendChild(root); 
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    代码分析:首先截取#号后面的值,然后创建一个div,然后将#号后面的值都赋值给div,然后使用querySelectorAll选取div下所有的子元素;然后获取子元素的属性,并将属性全部删除。输入格式为第一条注释。

    输入xss代码进行测试

    在这里插入图片描述
    我们发现输入的src被删除了,但是也没有出现弹窗。根据我们所写的防御代码,理论上应该所有的元素都要删除。但是为什么这里没有全部删除呢?

    这里就涉及到一个开发的知识了,以这个python代码为例

    a = [6, 5, 4, 3, 2, 1, 0]
    index = 0
    
    for i in a:
        print('a['+str(index)+'] = '+str(a[index])+':', a, end='')
        print(max(a), end=' = ')
        a.remove(max(a))
        print(a, end=' --> ')
        index = index + 1
        print('a[0]='+str(a[0]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行结果为:

    a[0] = 6: [6, 5, 4, 3, 2, 1, 0]-6 = [5, 4, 3, 2, 1, 0] --> a[0]=5
    a[1] = 4: [5, 4, 3, 2, 1, 0]-5 = [4, 3, 2, 1, 0] --> a[0]=4
    a[2] = 2: [4, 3, 2, 1, 0]-4 = [3, 2, 1, 0] --> a[0]=3
    a[3] = 0: [3, 2, 1, 0]-3 = [2, 1, 0] --> a[0]=2
    
    • 1
    • 2
    • 3
    • 4

    通过观察运行结果,我们发现实际的运行结果与理想的结果出现了差别,理想中的结果应该是将a列表中的所有数据都删除,但是实际的运行结果是并没有都删除,还剩下一部分列表[2, 1, 0]。

    产生原因:

    1. 当第一次循环时,参数i从列表a中获取索引为0的数据,即i = a[0] = 6 ,i获取到了6这个参数,然后发现列表中最大的值就是6,下一步便是将6这个参数从a列表中移除,此时a[0]对应的数据变成了5,完成了第一次循环
    2. 进行第二次循环,此时的索引在0的基础上加1变成了索引1,对应的数据为a[1] =4,此时再次从a列表中查找最大值,发现最大值为5,然后便是将5移除a列表,此时a列表中a[0]对应的数据变成了4,即a[0] = 4,完成了第二次循环
    3. 进行第三次循环,索引在原有的基础上再次加1,此时的索引变成了2,此时a[2] =2,再次从a列表中查找最大值,发现最大值为4,然后便是将4移除a列表,a[0]再次发生变化,a[0] = 3,完成第三次循环
    4. 进行第四次循环,索引再次加1变成了3,此时a[3] = 0,已经到了a列表的最后一位,这是最后一次循环了,再次从a列表中查找最大值,发现最大值为3,然后便是将3移除a列表,a[0]再次发生变化,a[0]= 2,完成第四次循环

    通过上面的步骤分析不难发现,因为索引每次循环都会在原有的基础上加1,并且因为删除了最大值的原因,索引中会自动填补删除掉的那个最大值所在索引的空缺,由最大值后面的那个值依次进行填补,造成索引一直在增加,但是索引的总数确实一直在减少。

    通过在浏览器中的调试中也可以查看我们所得的结论,如图所示逐步往下走。
    在这里插入图片描述
    然后我们就接着往下走。然后可以看到el获取的值是img,也就说说它已经成功获得到了这个标签。
    在这里插入图片描述
    然后接着继续走;然后attr获取到了第一个元素:src,之后,它执行了下面的移除操作。
    在这里插入图片描述
    接下来按照预期应该是回到标签内继续匹配元素onerror然后进行删除,但是当继续下一步的时候,attr取到的值是空的,就直接跳出循环,直接结束。
    在这里插入图片描述
    在这里插入图片描述
    根据我们之前所说的在进行循环的过程中,attr首先匹配到的是src元素,然后在循环过后直接删除,删除了之后,剩余的哪个onerror自动往前移动,onerror替代了src排的第一个的位置,它就变成了第一个,但是在刚刚循环的时候,已经把第一个给循环了,要去循环下一个的时候它没有了,所以循环结束了。

    绕过方法
    所以知道这个原理后,我们就可以将输入的元素打乱,因为他会删除固定位置的元素,所以我们让他删除后所剩下的是我们所需要的就可以了。
    将src元素写到第2位,函数在删除第一位的时候它变成第一位就会保留了,然后将onerror保存到第四位,那样在删除第一位后,原来的第三位变成了第二位,第四位就变成了第三位,第二位运行完后被删除,最开使的第四位就变成了第2位,但是我们已经执行完第二位了,所以后面就没有了,就跳出了循环。

    payload为:

    <img aaa='111' src='222' bbb='333' onerror='alert(1)'>
    
    • 1

    在这里插入图片描述
    成功弹窗

    改进过滤该如何绕过

    上述代码因为只使用了一个for循环导致出现了无法实现所有数据删除。所以我们将上述代码进行改进,使用两个for循环将循环和删除操作分开运行。就可以实现所有元素删除了,代码如下。

    const data = decodeURIComponent(location.hash.substr(1));;
        const root = document.createElement('div');
        root.innerHTML = data;
       
        // 这里模拟了XSS过滤的过程,方法是移除所有属性,sanitizer
        for (let el of root.querySelectorAll('*')) {
         let attrs = [];
         for (let attr of el.attributes) {
          attrs.push(attr.name);
         }
         for (let name of attrs) {
          el.removeAttribute(name);
         }
        }    
         document.body.appendChild(root); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述
    进行测试后,将我们输入的数据只剩一个img标签。那这种情况该怎么绕过呢。我提供两种绕过思路。

    1. 代码进入循环只删除无用数据
    2. 代码不进入循环直接执行恶意代码

    1.代码进入循环但不删除数据

    这种方式就可以使用到刚开始讲的那个DOM破坏的方式来进行。
    el执行的是attr,如果有一个元素可以劫持这个,那么删除的就不是atr而是里面的一个子元素。
    现测试一个例子:

    <body>
     <form id="x"action="">
            <img name="attributes">
     </form>
    </body>
    <script>
    	console.log(window.x.attributes)
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    根据上述内容,我们可以知道通过id我们可以打印出整个标签,也就是说,这里的x是上面的el;插入一个form之后,这个el就相当于是等于这个form的,而那个el.attributes相当于是哪个img。也就是让img进入循环,而在form中进行触发,这样就实现了进入循环,但是删除的是无用的标签。
    所以可以使用刚才的方法测试:

    但是它的结果显示是:el.attributes不是一个可迭代器,

    在这里插入图片描述
    可迭代对象有一个特征就是for循环,现在进入的只有一个元素,他是循环不了的,所以我们需要将他组成数组或者集合,
    而刚刚我们刚刚正好说了,如过id的值是相同的话会组成一个集合,而这个就刚好满足了刚刚咱们所需要的。所以它就可以写成下面的形式:

    <form id="x"action="">
            <img name="attributes">
            <img name="attributes">
     </form>
    
    • 1
    • 2
    • 3
    • 4

    这样的话,img标签就进入了循环删除,这样的话我们form里面还缺少一个触发的属性,而onfocus属性正好可以自动触发,但是它不是form属性,而是input下面。我们也可以将img换成input,这样也可以满足name相同的时候会变成一个集合。

    这个解决之后还需要自动聚焦,这个时候就需要一个自动聚焦的属性。

    tabindex:全局属性,以及它是否(在何处)参与顺序键盘导航。

    加上tabindex属性的话就可以把焦点聚集在input上,否则onfoucus是没有办法实现的。
    所以这样的话:我们就可以进行尝试:

    <form tabindex=1 onfocus="alert(1)" autofocus="true"><input name=attributes><input name=attributes></form>
    
    • 1

    这种是成功跳出弹窗的,但是因为这个是自动将你的鼠标自动对焦,所以会一直进行弹窗,所以我们可以在它执行成功一次之后将他移除。

    payload为:

    <form tabindex=1 onfocus="alert(1);this.removeAttribute('onfocus');" autofocus="true"><input name=attributes><input name=attributes></form>
    
    • 1

    在这里插入图片描述
    成功弹窗

    2. 代码不进入循环直接执行恶意代码

    这里使用的是两个svg标签,也就是使用来尽行绕过,它可以在过滤代码之前进行绕过,也就是说,它在代码的root.innerHTML = data;就已经执行了。
    在这里插入图片描述
    要解释这个的话首先要了解以下浏览器的渲染过程。
    也就是在DOM树构建完成之后,会触发DOMContentLoaded事件,接着就会加载脚本或者图片,然后执行全部加载完成后会触发load事件。

    使用img标签失败的原因是:它是先循环过滤了才可以进行弹窗,但是经过过滤后子元素就被过滤掉了。也就是说js阻塞了DOM树的构建;也可以说在script标签内的JS执行完毕以后,DOM树才会构建完成。

    但是我们对svg进行断点测试发现
    第一步:我们会发现他会先直接执行alert(1),再执行我们的过滤函数操作
    在这里插入图片描述
    在这里插入图片描述
    这样的话,它没有进入到循环删除就已经可以进行弹窗。接着往下走的话的话即便被删它也已经执行过了,所以也没有必要了。
    也就是说,这种嵌套的svg成功的原因是因为当页面为root.innerHtml赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load事件,最终成功执行代码。

  • 相关阅读:
    通航的桥
    【STM32单片机】俄罗斯方块游戏设计
    Openssl数据安全传输平台013:手写C/C++线程池 -C API封装为C++类 (待完善)
    TypeError: data.reduce is not a function:数据类型不匹配
    《深入浅出Vue.js》打卡Day1
    rust学习-any中的downcast和downcast_ref
    Jenkins从入门到精通面试题及参考答案(3万字长文)
    【Python小程序】浮点矩阵加减法
    单片机课程设计(Integrate就医服务平台/医院信息化平台)
    关于边缘联邦学习的研究方向以及现状综述阅读笔记
  • 原文地址:https://blog.csdn.net/m0_46467017/article/details/126117380