去年Proton Mail、Skiff 和 Tutanota被爆发现了漏洞,漏洞原因类似。
Proton Mail的XSS漏洞细节
代码存在一个严重的缺陷
packages/shared/lib/sanitize/purify.ts:
const LIST_PROTON_TAG = ['svg'];
// [...]
const sanitizeElements = (document: Element) => {
LIST_PROTON_TAG.forEach((tagName) => {
const svgs = document.querySelectorAll(tagName);
svgs.forEach((element) => {
const newElement = element.ownerDocument.createElement(`proton-${tagName}`);
// [...]
element.parentElement?.replaceChild(newElement, element);
});
});
};
此代码将<svg>
电子邮件中的元素替换为<proton-svg>
元素。它通过创建一个新元素,移动所有子元素,然后替换旧元素来实现这一点。
但HTML 有自己的解析规则,它可以包含具有不同解析规则的东西,例如MathML和SVG。它们看起来与 HTML 类似,因为它们也是从 XML 派生的,在如何解析它们方面存在一些关键差异。
HTML 和 SVG 之间差异的一个例子是元素<style>
。在 HTML 中,此元素包含原始文本,直到下一个结束</style>
标记。在 SVG 中,它包含子元素。当消毒剂在错误的环境下运行时,它可能会做出错误的决定。
这正是 Proton Mail 的漏洞中所发生的情况。安全审核首先看到一个 SVG 元素,并根据 SVG 上下文清理其子元素。之后,外部<svg>
标签被重命名为<proton-svg>
。由于这不是标准 HTML 或 SVG 标签,因此它会退回到 HTML 上下文中。这会导致浏览器解析结果的方式与安全审核处理结果不同。
攻击者可以通过以下POC测试此解析器差异漏洞:
首先会识别SVG上下文,并将<style>元素的内容解析为<a>元素。</style>隐藏在<a>元素的alt标记中,不会闭合<style>元素。由于<img>标记也隐藏在属性中,因此不会删除oneror事件处理程序。
将<svg>
元素重命名为时<proton-svg>
,解析结果如下所示:
由于<proton-svg>元素属于HTML上下文,<style>元素的解析规则发生了变化。内容现在被解析为原始文本,字节序列</style>的第一次出现就终止了元素。这导致出现<img>元素,该元素在渲染过程中执行oneror处理程序。绕过了安全规则。
但这还不能直接允许攻击者执行任意 JavaScript。Proton Mail 还有多重防线,这只是绕过了第一重HTML Sanitizer的安全规则防线。
第二道防线:Iframe 沙箱
<iframe>
具有sandbox
属性的元素。绕过电子邮件的 HTML 审核后,结果不会直接插入到 Proton Mail 页面本身的 DOM 中,而是插入到 iframe 的 DOM 中。目的是邮件中的 CSS 样式等内容不会影响 Proton Mail 的 UI。这使得 iframe 的内容与页面的其余部分隔离。另一个好处是能够通过在属性中提供白名单来限制 iframe 内的页面可以执行的操作sandbox
。
然而,Proton Mail 在 Safari 浏览器中打开时allow-scripts
命令被添加到白名单中,这意味着攻击者根本不需要绕过沙箱,因为他们只需执行 JavaScript 访问顶部框架即可。
对于所有其他浏览器,攻击者必须利用社会工程学让受害者单击在新选项卡中打开的链接,从而逃离沙箱并能够访问打开器的父框架:
第三道防线:内容安全政策CSP
最终的防御机制是 Proton Mail 的内容安全策略(CSP)。它限制了可以加载各种资源的来源,包括脚本、图像和样式。
但 Proton Mail 允许内嵌附件的任意内容类型和内容。这将允许攻击者发送 JavaScript 附件而不是图像,并将其引用为<img>
元素的源,从而触发包含 JavaScript 并具有内容类型的 Blob URL 的创建application/javascript
。
攻击者可以利用这种内联图像加载机制来制作任意 blob URL 并将其作为脚本加载以绕过 CSP。剩下的唯一挑战是如何从图像标签的src
属性中获取创建的 blob URL 并将其用作脚本标签的src
属性。
由于原点始终为https://mail.proton.me,因此 URL 的开头已知为blob:https://mail.proton.me/。这只留下由十六进制字符和破折号组成的 UUID,将每个字符的可能性减少到 17 个。方案是为所有可能的属性值创建选择器,但由于可能值的数量为 2 122 ,因此这是不可行的。
为此,我们必须将想要泄漏的值分割成较小块。在我们的例子中,我们不会立即泄漏整个 UUID,而是并行泄漏所有 3 字符子字符串。我们首先计算 UUID 的所有有效 3 字符子字符串,从 开始000
,到0-0
,直到fff
。然后,我们为每个子字符串创建一个 CSS 选择器,它会告诉我们该子字符串是否包含在我们想要泄漏的当前 UUID 中。当 CSS 选择器匹配时,我们会使用唯一的 URL 从攻击者服务器请求背景图像。
以下是如何将 blob URL 拆分为重叠的 3 字符块的示例:
这样,攻击者XSS服务器将知道 UUID 包含的所有不同块,但不知道它们的顺序。为了重建正确的 UUID,服务器必须通过从一个块开始并找到一个重叠的块来将其重新拼接在一起。
从该块开始8c2
,攻击者将查找以 开头的任何块c2
,从而找到该块c2a
。从那里他们会寻找以 , 开头的块2a
,依此类推。最后,应该重建完整的 blob UUID,除非有多个块以相同的两个字符开头。
现在我们有了泄漏 blob URL 的策略,我们需要在 CSS 中实现它。
解决方案是寻找一种方法,将任意数量的背景图像分配给单个元素,这样浏览器就可以获取所有背景图像。在阅读了CSS规范数小时后,我们发现了cross-fade()CSS函数。此函数以两个图像和一个百分比作为自变量,然后返回覆盖两个图像后得到的图像。图像参数可以指定为url(),但它们也可以由对cross-fade()函数的另一个调用产生!这意味着我们可以嵌套任意数量的cross-fade()调用,迫使浏览器请求嵌套树底部使用的所有url()。
下面的示例显示了这个嵌套树的样子。浏览器必须先加载图像.jpg和b.jpg,然后才能创建交叉褪色的图像。浏览器还必须加载c.jpg,然后才能将其与其他操作的结果交叉淡入淡出。最终结果是可以指定为元素背景图像的单个图像:
解决了所有防御后,泄漏 Blob URL 的最终 CSS POC如下所示:
如下为运行POC后浏览器的的情况:
攻击者服务器收到块信息后,会重建 blob URL 并向受害者发送第二封电子邮件。这次,电子邮件包含一个<script>
使用 blob URL 作为其 的标签src
,以及一个在新选项卡中打开 blob URL 的链接。对于使用 Safari 的受害者来说,脚本标签就足够了,因为不需要绕过 iframe 沙箱。其他受害者将必须单击该链接,这将在新选项卡中打开该链接,从而根据该指令绕过 iframe 沙箱allow-popups-to-escape-sandbox
。
一旦 JavaScript 有效负载被执行,它就可以直接访问运行 Proton Mail 应用程序的顶部窗口。攻击者可以使用此访问权限窃取解密形式的所有电子邮件,以受害者的名义发送电子邮件,甚至可能窃取受害者的加密密钥。
整个利用流程再次总结如下:
原文始发于微信公众号(军机故阁):Proton Mail邮件系统漏洞