在这篇博文中,我将解释我最近在DOMPurify--流行的HTML过滤库中的绕过。简而言之,DOMPurify的工作是将一个不受信任的HTML片段删除所有可能导致跨站点脚本(XSS)的元素和属性。
这是Bypass:
<form>
<math><mtext>
</form><form>
<mglyph>
<style></math><img src onerror=alert(1)>
相信我,这段话中没有一个元素是多余的。
为了理解为什么这段代码能够工作,我需要给你介绍一下HTML规范中的一些有趣的功能,我使用这些功能来进行Bypass工作。
我们先从基础知识开始,解释一下DOMPurify通常是如何使用的。假设我们在htmlMarkup中有一个不受信任的HTML,我们想把它分配给某个div,我们使用下面的代码来使用DOMPurify对它进行过滤并分配给div。
div.innerHTML = DOMPurify.sanitize(htmlMarkup)
就 HTML 的解析和序列化以及对 DOM 树的操作而言,在上面的简短片段中发生了以下操作。
让我们在一个简单的例子上看看。假设我们的初始标记是A<img src=1 onerror=alert(1)>B。在第一步中,它被解析成以下的树。
然后,DOMPurify对其进行过滤,留下以下DOM树。
然后将其序列化为。
A<img src="1">B
而这就是DOMPurify.sanitize返回的内容。然后在分配给innerHTML时,浏览器会再次解析这些标记。
该DOM树与DOMPurify工作的DOM树相同,然后将其附加到文档中。
所以简而言之,我们的操作顺序如下:解析➡️序列化➡️解析。按照直觉可能是序列化一棵DOM树并再次解析它应该总是返回初始的DOM树。但事实完全不是这样。在HTML规范中甚至有一节关于序列化HTML片段的警告。
It is possible that the output of this algorithm [serializing HTML], if parsed with an HTML parser, will not return the original tree structure. Tree structures that do not roundtrip a serialize and reparse step can also be produced by the HTML parser itself, although such cases are typically non-conforming.
重要的启示是,序列化-解析前后并不能保证返回原始DOM树(这也是被称为mXSS(突变XSS)的根本原因)。虽然通常这些情况是由于某种解析器/序列化器错误造成的,但至少有两种符合规范的变种情况。
其中一种情况与FORM元素有关。在HTML中,它是一个相当特殊的元素,因为它本身不能嵌套。规范中明确规定,它不能嵌套FORM为其子元素。
这可以在任何浏览器中确认,并使用以下标记。
<form id=form1>
INSIDE_FORM1
<form id=form2>
INSIDE_FORM2
这将产生以下DOM树。
第二种形式在DOM树中完全被省略了,就像它从来没有出现过一样。
现在是有趣的部分。如果我们继续阅读HTML规范,它实际上给出了一个例子,说明只要有一个稍有破绽的标记和错误的嵌套标签,就有可能创建嵌套表单。这里是(直接摘自规范)。
<form id="outer"><div></form><form id="inner"><input>
它产生了以下DOM树,其中包含一个嵌套的表单元素。
这不是任何特定浏览器的bug,而是直接来自HTML规范,并在解析HTML的算法中进行了描述。下面是大意。
因此,回到这个片段。
<form id="outer"><div></form><form id="inner"><input>
一开始,表单元素指针被设置为id="external"的那个。然后是一个div,</form>结束标签将表单元素指针设置为null。因为它是空的,所以可以创建下一个id="inner"的表单;而且因为我们当前在div中,所以我们实际上有一个嵌套在表单中的表单。
现在,如果我们尝试序列化产生的DOM树,我们将得到以下标记。
<form id="outer"><div><form id="inner"><input></form></div></form>
注意,这个标记不再有任何错误嵌套的标记。而当再次解析该标记时,就会创建以下DOM树。
所以这就是一个证明,序列化-再解析 前后并不能保证返回原始DOM树。而更有趣的是,这基本上是一个符合规范的突变。
自从我意识到这个怪癖的那一刻起,我就非常确定,一定可以通过某种方式滥用它来绕过HTML sanitizers。而在很长时间没有得到任何利用它的想法后,我终于偶然发现了HTML规范中的另一个怪癖。不过在说具体的怪癖本身之前,先说说我最喜欢的HTML规范的潘多拉盒子:
外部内容就像一把瑞士军刀,可以用来突破解析器和过滤器。我在之前的DOMPurify绕过以及Ruby sanitize库的绕过中使用了它。
HTML解析器可以创建一个包含三个命名空间元素的DOM树。
在外来内容标记中,与普通HTML中的解析方式不同。这一点在<style>元素的解析上可以最清楚的表现出来。在HTML命名空间中,<style>只能包含文本,不能有子元素,而且HTML实体不被解码。而在外来内容中就不一样了:外来内容的<style>可以有子元素,实体也会被解码。</p> <p>考虑以下标签。</p> <pre><code><style><a>ABC</style><svg><style><a>ABC</code></pre> <p>它被解析成以下的DOM树。<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20201013135036-04342e32-0d18-1.png" alt=""></p> <p>注意:从现在开始,本博文中DOM树中的所有元素都将包含一个命名空间。所以html style意味着它是HTML命名空间的<style>元素,而svg style意味着它是SVG命名空间的<style>元素。</p> <p>由此产生的DOM树证明了我的观点:html style只有文本内容,而svg style则像普通元素一样被解析。</p> <p>继续往下看,可能很想做某个观察。那就是:如果我们在<svg>或<math>里面,那么所有的元素也都在非HTML命名空间。但事实并非如此。在HTML规范中,有一些元素叫做<strong>MathML文本集成点</strong>和<strong>HTML集成点</strong>。而这些元素的子元素都有HTML命名空间(下面我列举了某些例外)。<br> 请看下面的例子。</p> <pre><code><math> <style></style> <mtext><style></style></code></pre> <p>它被解析成以下DOM树。<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20201013135140-2a6c5962-0d18-1.png" alt=""><br> 请注意,作为math直接子元素的style元素是在MathML命名空间,而mtext中的style元素是在HTML命名空间。而这是因为<strong>mtext是MathML文本集成点</strong>,并使解析器切换命名空间。</p> <p>MathML文本集成点是。</p> <ul> <li>math mi</li> <li>math mo</li> <li>math mn</li> <li>math ms</li> </ul> <p>HTML集成点是:</p> <ul> <li>math annotation-xml 如果它有一个叫做编码的属性,其值等于text/html 或application/xhtml+xml</li> <li>svg foreignObject</li> <li>svg desc</li> <li>svg title</li> </ul> <p>我一直以为MathML文本集成点或HTML集成点的子元素都默认有HTML命名空间。我是真是大错特错! HTML规范中说,MathML文本集成点的子节点默认为HTML命名空间,但有两个例外:mglyph和malignmark。而且只有当它们是MathML文本集成点的直接子元素时才会出现这种情况。</p> <p>我们用下面的标签来检查一下。</p> <pre><code><math> <mtext> <mglyph></mglyph> <a><mglyph></code></pre> <p><img src="https://xzfile.aliyuncs.com/media/upload/picture/20201013134331-06aa26e0-0d17-1.png" alt=""></p> <p>请注意,作为mtext的直接子元素的mglyph是在MathML命名空间,而作为html a元素的子元素的mglyph是在HTML命名空间。</p> <p>假设我们有一个 "当前元素",我们想确定它的命名空间。我整理了一些经验法则。</p> <ul> <li>当前元素在其父元素的命名空间中,除非满足以下几点条件。</li> <li>如果当前元素是<svg>或<math>,而父元素在HTML命名空间,那么当前元素分别在SVG或MathML命名空间。</li> <li>如果当前元素的父元素是HTML集成点,则当前元素在HTML命名空间,除非是<svg>或<math>。</li> <li>如果当前元素的父元素是MathML集成点,那么当前元素在HTML命名空间,除非它是<svg>、<math>、<mglyph>或<malignmark>。</li> <li>如果当前元素是<b>、<big>、<blockquote>、<body>、<br>、<center>、<code>、<dd>、<div>、<dl>、<dt>、<em>、<embed>、<h1>之一。<h2>, <h3>, <h4>, <h5>, <h6>, <head>, <hr>, <i>, <img>, <li>, <listing>, <menu>, <meta>, <nobr>, <ol>, <p>, <pre>, <ruby>, <s>, <small>。<span>、<strong>、<strike>、<sub>、<sup>、<table>、<tt>、<u>、<ul>、<var>或<font>,并定义了颜色、面或大小属性,那么,堆栈上的所有元素都会被关闭,直到看到MathML文本整合点、HTML整合点或HTML命名空间中的元素。然后,当前元素也在HTML命名空间。</li> </ul> <p>当我在HTML规范中找到这个关于mglyph的宝石时,我立刻知道这就是我一直在寻找的滥用html形式突变绕过sanitizer的方法。</p> <h2>DOMPurify bypass</h2> <p>所以让我们回到绕过DOMPurify的Payload。</p> <pre><code><form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)></code></pre> <p>payload利用错误嵌套的html表单元素,并包含mglyph元素。它产生的DOM树如下。<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20201013134425-26f725a6-0d17-1.png" alt=""></p> <p>这个DOM树是无害的。所有元素都在DOMPurify的允许列表中。注意,mglyph是在HTML命名空间。而那个看起来像XSS payload的片段只是html样式中的一个文本。因为有一个嵌套的html形式,我们可以很肯定这个DOM树在rearsing时是会被突变的。</p> <p>所以DOMPurify在这里没有任何作用,而是返回一个序列化的HTML。</p> <pre><code><form><math><mtext><form><mglyph><style></math><img src onerror=alert(1)></style></mglyph></form></mtext></math></form></code></pre> <p>这个片段有嵌套的表单标签。所以当它被分配给innerHTML时,它被解析成以下DOM树。<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20201013134453-37f61d76-0d17-1.png" alt=""></p> <p>因此,现在第二个 html 表单没有创建,mglyph 现在是 mtext 的直接子元素,这意味着它在 MathML 命名空间中。正因为如此,style也在MathML命名空间中,因此它的内容不被视为文本。然后,</math>关闭了<math>元素,现在img是在HTML命名空间中创建的,从而导致XSS。</p> <h2>总结</h2> <p>综上所述,这个绕过之所以能够实现,是因为几个因素。</p> <ul> <li>DOMPurify的典型用法使得HTML标记被解析两次。</li> <li>HTML规范有一个怪癖,使得创建嵌套表单元素成为可能。但是,在重新解析的时候,第二个表单会消失。</li> <li>mglyph和malignmark是HTML规范中的特殊元素,在某种程度上,如果它们是MathML文本集成点的直接子元素,那么它们就属于MathML命名空间,尽管其他标签默认都属于HTML命名空间。</li> <li>利用以上这些方法,我们可以创建一个标记,其中有两个表单元素和mglyph元素,这些元素最初是在HTML命名空间,但在重新解析时却在MathML命名空间,使得后续的样式标签要进行不同的解析,导致XSS。</li> </ul> <p>Cure53对我的<a href="https://twitter.com/0xsapra?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1307929537749999616%7Ctwgr%5Eshare_3&amp;ref_url=https%3A%2F%2Fresearch.securitum.com%2Fmutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass%2F">Bypass</a>推送更新后,又发现了一个。</p> <pre><code><math><mtext><table><mglyph><style><math><table id="</table>"><img src onerror=alert(1)"></code></pre> <p>我把它留给读者,让读者自己去弄清楚为什么这个payload能用。提示:根本原因和我发现的bug一样。</p> <p>这个bypass也让我意识到,以下形式</p> <pre><code>div.innerHTML = DOMPurify.sanitize(html)</code></pre> <p>是容易发生突变XSS的设计,再找一个实例只是时间问题。我强烈建议给DOMPurify传递RETURN_DOM或RETURN_DOM_FRAGMENT选项,这样就不会执行序列化-解析的往返操作。</p> <p>最后说明一下,我在为即将到来的<strong>XSS学院</strong>远程培训准备材料时发现了DOMPurify绕过。虽然还没有正式宣布,但细节(包括议程)将在两周内公布。我将讲授有趣的XSS技巧,重点是打破解析器和过滤器。如果你已经知道你有兴趣,请联系我们[email protected],我们将为你预定座位!</p> <p>作者:Michał Bentkowski Michał Bentkowski<br> 原文地址:<a href="https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/">https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/</a></p> </style>