到底什么叫突变xss

这里只引入两篇文章

(中文版翻译)[https://www.anquanke.com/post/id/219089]
(英文原版)[https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/]
(good)[https://jorianwoltjer.com/blog/p/research/mutation-xss]
不建议看其他文章

前言

xss乃大势所趋,同样是一个很好的对web手的考察,总不能还不会xss吧,但是总是学那一套也学不会什么东西,学一点新又不新的东西,突变xss
对于xss的学习,这里插一嘴,为什么比赛这么多啊,多到赛后都不想复现了()

DOMPurity

正式学习突变xss前,先了解一下什么是DOMPurity?
它是一款用于过滤html防范xss的库,简单来讲,你传入一段html,它会把它解析为DOM树,然后DOMPurity会遍历DOM树中的所有元素以及属性,删除不在白名单的所有节点,接着再序列回html,然后赋值给innerHtml,被浏览器解析

1
div.innerHTML = DOMPurify.sanitize(htmlMarkup)

这种序列化、重新解析的往复操作并不一定能返回原始的DOM树(比如会”善良”的补足标签),因此有可能产生突变型xss

突变xss

在正式学习之前,我们得了解几个所谓的边角特性,看看他们到底有什么魔力

嵌套form元素

要知道,HTML标准中规定,不允许在一个表单元素(form元素)中嵌套另一个表单元素。
如果发现存在嵌套,解析器会尝试”修正”结构,最终第二个 form 标签在 DOM 中不被当作独立子表单创建。这样会影响原本以其为父的子节点切换父节点,这种解析之间,就会发生类似雪崩的一系列变化

MathML命名空间

这边都不会详细介绍,只是引入这个概念()

payload

DOMA

1
<form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>

第一次,服务器会如何解析呢?
从左到右 token 化并处理
先进入form,建立了一个form₁(HTML 命名空间),并form pointer = form₁(记录当前打开的 form)
然后进入math,进入 MathML 命名空间,创建 math(MathML)
然后进入mtext,(MathML 文本集成点)。
注意:mtext 的子节点通常默认被解析为 HTML 命名空间(除非子节点是某些特殊元素触发例外)。

然后闭合form(即把 form pointer 设为 null),在此之后再创建form是允许的
因为 form pointer 刚被设为 null,可以创建一个新的 form₂ —— 而这个 form₂ 此刻处在 mtext(MathML)内部的位置
因此它被插入为 mtext 的子节点。但由于它的父是一个 HTML 表单节点(form₂ 是 HTML 元素),因此 form₂ 与其所有子节点都属于 HTML 命名空间(在第一次解析的上下文中)。

因此 mglyph 在第一次解析后 DOM A 中处于 HTML 命名空间
在 HTML 命名空间的 style 元素内,随后的 img src onerror=… 只是style 的文本内容(在 HTML 的 style 标签里不再解析成元素),因此目前 DOM A 中并没有真正的 img 元素节点,只有一段文本。

1
2
3
4
5
6
7
form₁ (HTML)
└─ math (MathML)
└─ mtext (MathML)
└─ form₂ (HTML) ← CREATED (form pointer was null)
└─ mglyph (HTML) ← IN HTML HERE
└─ style (HTML)
└─ text: "<img src onerror=alert(1)>" (just text in HTML-style)

DOMpurify返回的字符串

1
<form><math><mtext><form><mglyph><style></math><img src onerror=alert(1)></style></mglyph></form></mtext></math></form>

包含嵌套的 form 标签
至于为什么这样是因为
“第一次解析”时,由于 浏览器实际上已经把两个 form 变成嵌套关系了。

关键在 HTML 解析算法是基于栈的,不会“跳回去”找兄弟节点。
浏览器只会在「当前 open element」(这里是 mtext)下放置新标签。
你看到的源码里,虽然肉眼觉得两个 form 是平级的,但解析器不会退栈到外层 form,因为前面有 math mtext 没关
要理解一个核心概念

因为在遇到 </form> 时,浏览器只是把 form pointer 清空,并没有回退 DOM 栈
于是第二个 form 自然就成了 <mtext> 的子节点。

DOMB

遇见嵌套情况,导致第二个 form 的 start-tag 在此处 被忽略(或不创建新的 form 元素),于是 mglyph 不再有 form₂ 作为父,而直接成为 mtext 的子节点——这一步把 mglyph 的命名空间从 HTML 变成 MathML(因为 mtext 的直接子元素按规范会进入 MathML,mglyph 在这种位置是 MathML)
HTML 规范中明确:当 mglyph 是 mtext 的直接子节点时,它属于 MathML 命名空间(这是对 mglyph 的特殊例外)。
所以现在 mglyph 的 namespace 由 HTML → MathML。
接着 style 在 MathML/foreign-content 上下文中,其内部的文本不再被视为“纯文本”,而会按 foreign-content 的规则被重新解析(在 MathML integration/text integration 的特定规则下,style 的内容可以被解释成子节点或被实体解析)。

因此序列化时候作为文本的 <img src onerror=...> 在第二次解析过程中被解析成真正的元素(最终会进入 HTML 命名空间或被插回到文档树),从而生成真正的 <img onerror=...> 节点并执行

1
2
3
4
<mtext> (MathML)
└─ mglyph (MathML)
├─ style (MathML)
└─ img (HTML) ← break out from foreign content

也许你会想,既然<style>变换了命名空间,那么<img>呢?
<img> 属于 HTML 的“break out 标签”;
解析器看到它时,会立即退出 MathML 命名空间,在 HTML 命名空间里重新创建一个 <img> 元素

有点逃逸的意思在吧,二次解析

结语

有点囫囵吞枣了,只是简单的通了一下,原文内容不少,可以看看,Mutation XSS 还是比较复杂的,需要深入理解才能掌握,也许以后会改第二版内容()

这里引出第二种内容

1
<form><math><mtext></form><form><mglyph><svg><mtext><title><path is="</title><img src onerror=alert(origin)>">