在这篇博文中,我将解释我最近在DOMPurify--流行的HTML过滤库中的绕过。简而言之,DOMPurify的工作是将一个不受信任的HTML片段删除所有可能导致跨站点脚本(XSS)的元素和属性。

这是Bypass:

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

相信我,这段话中没有一个元素是多余的。
为了理解为什么这段代码能够工作,我需要给你介绍一下HTML规范中的一些有趣的功能,我使用这些功能来进行Bypass工作。

DOMPurify的用法

我们先从基础知识开始,解释一下DOMPurify通常是如何使用的。假设我们在htmlMarkup中有一个不受信任的HTML,我们想把它分配给某个div,我们使用下面的代码来使用DOMPurify对它进行过滤并分配给div。

div.innerHTML = DOMPurify.sanitize(htmlMarkup)

就 HTML 的解析和序列化以及对 DOM 树的操作而言,在上面的简短片段中发生了以下操作。

  • 1.htmlMarkup被解析到DOM树中。
  • 2.DOMPurify对DOM树进行过滤(简而言之,这个过程就是要走遍DOM树中的所有元素和属性,并删除所有不在允许列表中的节点)。
  • 3.DOM树被序列化回HTML标记。
  • 4.分配到innerHTML后,浏览器再次解析HTML标记。
  • 5.解析后的DOM树被追加到文档的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元素

其中一种情况与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>标签时,解析器需要记录它是用一个表单元素指针打开的(规范中是这样称呼的)。如果指针不是空的,那么就不能创建表单元素。
  • 当你结束一个< form>标签时,表单元素指针总是被设置为null。

因此,回到这个片段。

<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命名空间;但是如果解析器遇到< svg>或< math>元素,那么它就会分别 "切换 "到SVG和MathML命名空间。而这两个命名空间都会产生外来内容。

在外来内容标记中,与普通HTML中的解析方式不同。这一点在< style>元素的解析上可以最清楚的表现出来。在HTML命名空间中,< style>只能包含文本,不能有子元素,而且HTML实体不被解码。而在外来内容中就不一样了:外来内容的< style>可以有子元素,实体也会被解码。

考虑以下标签。

<style><a>ABC</style><svg><style><a>ABC

它被解析成以下的DOM树。

注意:从现在开始,本博文中DOM树中的所有元素都将包含一个命名空间。所以html style意味着它是HTML命名空间的< style>元素,而svg style意味着它是SVG命名空间的< style>元素。

由此产生的DOM树证明了我的观点:html style只有文本内容,而svg style则像普通元素一样被解析。

继续往下看,可能很想做某个观察。那就是:如果我们在<svg>或<math>里面,那么所有的元素也都在非HTML命名空间。但事实并非如此。在HTML规范中,有一些元素叫做</math></svg>MathML文本集成点HTML集成点。而这些元素的子元素都有HTML命名空间(下面我列举了某些例外)。
请看下面的例子。

<math>
<style></style>
<mtext><style></style>

它被解析成以下DOM树。

请注意,作为math直接子元素的style元素是在MathML命名空间,而mtext中的style元素是在HTML命名空间。而这是因为mtext是MathML文本集成点,并使解析器切换命名空间。

MathML文本集成点是。

  • math mi
  • math mo
  • math mn
  • math ms

HTML集成点是:

  • math annotation-xml 如果它有一个叫做编码的属性,其值等于text/html 或application/xhtml+xml
  • svg foreignObject
  • svg desc
  • svg title

我一直以为MathML文本集成点或HTML集成点的子元素都默认有HTML命名空间。我是真是大错特错! HTML规范中说,MathML文本集成点的子节点默认为HTML命名空间,但有两个例外:mglyph和malignmark。而且只有当它们是MathML文本集成点的直接子元素时才会出现这种情况。

我们用下面的标签来检查一下。

<math>
<mtext>
<mglyph></mglyph>
<a><mglyph>

请注意,作为mtext的直接子元素的mglyph是在MathML命名空间,而作为html a元素的子元素的mglyph是在HTML命名空间。

假设我们有一个 "当前元素",我们想确定它的命名空间。我整理了一些经验法则。

  • 当前元素在其父元素的命名空间中,除非满足以下几点条件。
  • 如果当前元素是< svg>或< math>,而父元素在HTML命名空间,那么当前元素分别在SVG或MathML命名空间。
  • 如果当前元素的父元素是HTML集成点,则当前元素在HTML命名空间,除非是< svg>或< math>。
  • 如果当前元素的父元素是MathML集成点,那么当前元素在HTML命名空间,除非它是< svg>、< math>、< mglyph>或< malignmark>。
  • 如果当前元素是< 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命名空间。

当我在HTML规范中找到这个关于mglyph的宝石时,我立刻知道这就是我一直在寻找的滥用html形式突变绕过sanitizer的方法。

DOMPurify bypass

所以让我们回到绕过DOMPurify的Payload。

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

payload利用错误嵌套的html表单元素,并包含mglyph元素。它产生的DOM树如下。

这个DOM树是无害的。所有元素都在DOMPurify的允许列表中。注意,mglyph是在HTML命名空间。而那个看起来像XSS payload的片段只是html样式中的一个文本。因为有一个嵌套的html形式,我们可以很肯定这个DOM树在rearsing时是会被突变的。

所以DOMPurify在这里没有任何作用,而是返回一个序列化的HTML。

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

这个片段有嵌套的表单标签。所以当它被分配给innerHTML时,它被解析成以下DOM树。

因此,现在第二个 html 表单没有创建,mglyph 现在是 mtext 的直接子元素,这意味着它在 MathML 命名空间中。正因为如此,style也在MathML命名空间中,因此它的内容不被视为文本。然后,</math>关闭了< math>元素,现在img是在HTML命名空间中创建的,从而导致XSS。

总结

综上所述,这个绕过之所以能够实现,是因为几个因素。

  • DOMPurify的典型用法使得HTML标记被解析两次。
  • HTML规范有一个怪癖,使得创建嵌套表单元素成为可能。但是,在重新解析的时候,第二个表单会消失。
  • mglyph和malignmark是HTML规范中的特殊元素,在某种程度上,如果它们是MathML文本集成点的直接子元素,那么它们就属于MathML命名空间,尽管其他标签默认都属于HTML命名空间。
  • 利用以上这些方法,我们可以创建一个标记,其中有两个表单元素和mglyph元素,这些元素最初是在HTML命名空间,但在重新解析时却在MathML命名空间,使得后续的样式标签要进行不同的解析,导致XSS。

Cure53对我的Bypass推送更新后,又发现了一个。

<math><mtext><table><mglyph><style><math><table id="</table>"><img src onerror=alert(1)">

我把它留给读者,让读者自己去弄清楚为什么这个payload能用。提示:根本原因和我发现的bug一样。

这个bypass也让我意识到,以下形式

div.innerHTML = DOMPurify.sanitize(html)

是容易发生突变XSS的设计,再找一个实例只是时间问题。我强烈建议给DOMPurify传递RETURN_DOM或RETURN_DOM_FRAGMENT选项,这样就不会执行序列化-解析的往返操作。

最后说明一下,我在为即将到来的XSS学院远程培训准备材料时发现了DOMPurify绕过。虽然还没有正式宣布,但细节(包括议程)将在两周内公布。我将讲授有趣的XSS技巧,重点是打破解析器和过滤器。如果你已经知道你有兴趣,请联系我们training@securitum.com,我们将为你预定座位!

作者:Michał Bentkowski Michał Bentkowski
原文地址:https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖