目录
一、什么是DOM破坏
二、例题1
三、多层关系
1.Collection集合方式
2.标签关系
3.三层标签如何获取
四、例题2
五、例题3
1.代码审计
2.payload分析
一、什么是DOM破坏
DOM破坏(DOM Clobbering)指的是对网页上的DOM结构进行不当的修改,导致页面行为异常、性能问题、安全风险或其他不良影响的情况。
就是⼀种将 HTML 代码注⼊⻚⾯中以操纵 DOM 并最终更改页面上 JavaScript 行为的技术
这里我们举一些例子就能更好地解释
可以看到打印的结果如下
通过打印<img>标签中的id或者name属性值,我们获取到了整个<img>标签
从中我们也发现了规律,直接打印x,y不管是id还是name都可以打印出来
而通过document来获取x,y只能打印出name属性的标签
window和直接打印的结果是一样的,都可以打印
下面这个例子可以看到cookie开始是空值,然后创建了一个div元素
在div里面添加了<img name=cookie>标签,然后添加到body里面去
这时候再打印cookie,发现变成了<img name="cookie">
这个例子成功地让本来为空值的cookie有了值,而且是我们可以控制的
然而得到一个标签对象并不是我们想要的,有些函数的参数并不是一个对象,而是字符串
这就需要函数在调用自己时,自己本身有一个ToString函数能够转换为字符串,然后让函数执行
所以我们需要一个自身拥有ToString函数的标签,而不是继承父类Object的ToString函数
可以看到一个对象调用父类的toString函数就会返回[object object],所以我们需要一个本身有toString函数的标签
通过下面的脚本过滤出了自身拥有toString函数的标签
HTMLAreaElement()和HTMLAnchorElement(),也就是<textarea>和<a>标签
所以这两个标签我们可以利用
二、例题1
然后我们来看一道例题Ok, Boomer. | XSS Warmups
XSS Game - Ok, Boomer | PwnFunction
这道题通过get参数将内容写入h2标签内,而且有过滤框架DOMPurify
这个过滤框架由安全团队cure53开发,以我们的技术很难绕过
但是注意setTimeout函数内的ok参数
这里的JS代码是没有任何关于ok参数的定义的,所以我们可以使用DOM破坏
构造ok参数,因为setTimeout函数执行字符串,所以需要用到<a>或者<textarea>标签
payload:
<a id=ok href="tel:alert(1)">a</a>
这里因为DOMPurify框架过滤了javascript,所以我们用tel,也可以执行script脚本
三、多层关系
如果我们需要获取一个标签下的子标签的内容,怎么获取呢?可以直接x.y吗?
显然不行,返回undefined
1.Collection集合方式
既然直接x.y不行,那我们可以用一个集合来做x,然后再获取y
此时x指代了一个集合,既有div也有a标签,然后再获取它的y属性
2.标签关系
要想知道哪些标签能直接调用x.y,可以通过一段代码来获取,但代码有点长,就直接说结果了
- form---button
- from---fieldset
- from---img
- from---image
- from---input
- from---object
- from---output
- from---select
- from---textarea
这九种组合可以直接调用x.y,来获取子标签内容
比如
3.三层标签如何获取
如果有三层标签,就需要要⽤到以上两种技巧来构建了
先分析x.y,x是一个集合,然后获取y,利用了第一种方法--集合方式,获取了第一个form标签
然后x.y.z,因为form和output标签存在关系,可以直接调用y.z,利用了第二种方法--标签关系
最后x.y.z.value就成功拿到output标签内的内容
四、例题2
首先审计代码,data为URL后的hash值,然后创建了一个div标签,把我们输入的hash值放进了div这个标签里面
第一个for循环拿出了div元素的所有后代元素,用el表示
定义了一个空数组attrs
第二个for循环拿出了后代元素的所有属性,用attr表示
然后将属性添加到attrs数组里
第三个for循环拿出了attrs数组里面的属性,用name表示
然后移除掉这个元素的该属性
最后将div添加到body里面去
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>alert(1)</script>不就行了吗
答案是不可以的,因为innerHTML考虑到安全问题将script过滤掉了
这道题可以使用DOM破坏和CSS来触发XSS
直接来看payload
<style>@keyframes x{}</style><form style="animation-name:x" onanimationstart="alert(1)"><input id=attributes><input id=attributes></form>
首先定义了一个CSS动画@keyframes x{},然后form表单里面调用这个样式
然后属性onanimationstart执行alert,意思是当animation动画开始时执行alert
CSS样式我已经忘得差不多了,但是大概是这样一个意思
我们再看后面的内容,一个form表单内包含两个id属性为attributes的input标签
看到这个id属性值应该能猜到这是什么作用了,没错,它就是用来破坏el.attributes属性的
上面我们说了,form和input标签是由关系的,可以直接调用x.y
所以代码中的el.attributes正好是我们的input标签,那为什么不用一个input标签,而是两个呢?
因为在for循环中el.attributes需要是可迭代的,而一个input标签只是一个对象,所以是不可迭代的
报错如下:
所以我们需要两个input标签来组成一个集合,这时,集合就是可迭代的了
根据代码,首先到style标签,没有属性可删,然后到form标签
因为迭代对象变成了input标签集合,此时attr就变成了undefined,所以attr.name也就不存在
此时attrs数组获取不到form标签里的任何属性,自然下面的for循环也不会删除form表单的任何属性
最后到我们的两个input标签,因为此时目的已经达到,删除了属性也无妨
最后成功绕过
五、例题3
XSS Game - Jason Bourne | PwnFunction
<script>
/* Helpers */
const bootstrapAlert = (msg, type) => {
return (`<div class="alert alert-${type}" role="alert">${DOMPurify.sanitize(msg)}</div>`)
}
document.getAlert = () => document.getElementById('alerts');
</script>
<script>
/* Welcome */
let name = (new URL(location).searchParams.get('name')) || "Pamela Landy";
document.write(
bootstrapAlert(`<b>Operation Treadstone</b>: Welcome <u>${name}</u>.`, 'info')
)
</script>
<!-- alerts -->
<div id="alerts"></div>
<script>
/* Handle to `#alert` */
let alerts = document.getAlert();
/* Treadstone Credentials */
let identification = Math.random().toString(36).slice(2);
let code = Math.floor(Math.random() * 89999 + 10000);
/* Default Credentials */
DEFAULTS = {};
DEFAULTS[identification] = code;
</script>
<script>
/* Optional Comment */
if (location.hash) {
let comment = document.createComment(decodeURI(location.hash).slice(1));
document.querySelector('#alerts').appendChild(comment);
}
</script>
<script>
/* Use `DEFAULTS` to init `SECRETS` */
SECRETS = DEFAULTS
/* Increment the `code` before the check */
let secretKey = new URL(location).searchParams.get('key') || "TREADSTONE_WEBB";
SECRETS[secretKey] += 1;
/* Authorization Check */
if (SECRETS[secretKey] === SECRETS[identification]) {
confirm(`Jesus Christ, it's Jason Bourne!`)
} else {
confirm(`You ain't David Webb!`)
}
</script>
1.代码审计
首先代码审计一波
第一个script块
首先定义了一个函数bootstrapAlert(msg,type),返回值是一个div标签里面包含着msg输入的东西,还使用了DOMPurify框架进行过滤
之后又定义了一个函数get.Alert,获取id为alerts的元素
第二个script块
首先通过GET传入name参数,如果没有传,默认Pamela Landy
然后调用bootstrapAlert函数,相当于这样
<div class="alert alert-info" role="alert">
<b>Operation Treadstone</b>
: Welcome <u>${name}</u>
</div>
然后一个id属性为alerts的div标签
第三个script块
首先将函数getAlert()的返回值赋给alerts
之后生成一个0,1的随机数,再将其转换为36进制的字符串,然后切片,去掉前两个字符
再使用Math.random()*89999+10000生成一个10000到89999的随机数赋给code
然后定义了一个DEFAULTS空对象
最后将code赋值给DEFAULTS对象里的identification属性
第四个script块
如果存在location.hash值
那就去掉前面的#号,将hash值取出来,然后以改内容创建注释赋值给comment
最后找到id属性为alerts的标签,将comment添加为它的子元素
第五个script块
首先把对象DEFAULTS赋给SECRETS
之后找到GET参数key,将它赋给secretKey,如果不存在默认为"TREADSTONE_WEBB"
并将secretKey值加1作为属性赋给SECRETS对象
然后if判断SECRETS[secretKey]与SECRETS[identification]是否相等
2.payload分析
初步分析下来我们可以传入三个参数,name、key、location.hash值
由于太菜,自己想了几十分钟没头绪,只有看答案来分析
?name=<img name=getAlert><form id=alerts name=DEFAULTS>&key=innerHTML#--><img src onerror=alert(1337)>
首先传入了一个<img name=getAlert>标签,上面提到getAlert是一个函数
然后传入了一个<form id=alerts name=DEFAULTS>,id和name属性都存在
key传入了innerHTML,可能是想构造.innerHTML,让标签传入
最后传入--><img src οnerrοr=alert(1337)>,前面的-->很有可能是用来闭合注释的
首先解释为什么要传入一个name属性为函数名的标签,是因为想让第三个script块报错
我们是先传入的name,getAlert函数在后面被调用,所以我们可以直接覆盖getAlert函数
此时getAlert就不是一个函数了,而是我们传入的标签,所以就会报错
这样一来整个第三个script代码块就都不会执行
那为什么要让它报错呢?因为报错后,DEFAULTS对象就定义不了
这样一来SECRETS变量就不会被赋值,而是接到我们的form标签,接到我们的标签那就简单了
传入的key值的主要作用是让SECRETS[secretKey]=form.innerHTML,触发我们的弹窗
这段代码很关键,作用是让我们的comment成为id属性为alerts的标签的子标签
document.querySelector('#alerts').appendChild(comment);
而此时的alerts有两个,一个是我们传入的form,还有一个是代码中的div,但是我们是先传入的form,标签顺序在div之前,所以appendChild只会将内容添加到第一个标签内
比如这里,拥有同样id属性的form和div标签,因为form标签在div标签前面,所以被添加了子节点
comment被添加到form后还有一个问题,我认为也是最难理解的,看下面两张图
这张图是没有添加key=innerHTML参数的
这张图是添加了key=innerHTML参数的
前面讲到SECRETS[secretKey]=form.innerHTML,那按照代码来是这样的
form.innerHTML=form.innerHTML+1,关键是这个1的作用是什么?
我的理解是当传入--><img src οnerrοr=alert(1)>时,前面的注释确实是闭合了,但是后面还有一个-->
此时如果没有1,那么-->就会跟在标签后面,而注释会选择最后的-->,所以img标签还是注释没有生效
如果有1,html会认为"-->1"是一个字符串,会换行到下一行,这样img标签就不会被注释掉了
最后触发弹窗