目录导航
RainLoop简介
RainLoop 是一个开源网络邮件客户端,成千上万的组织使用它通过电子邮件交换敏感消息和文件。在这篇博文中,我们警告 RainLoop 用户存在一个代码漏洞,该漏洞允许攻击者从受害者的收件箱中窃取电子邮件。在撰写本文时,还没有官方补丁可用。
攻击者可以通过向使用 RainLoop 作为邮件客户端的受害者发送恶意电子邮件来轻松利用这篇博文中描述的代码漏洞。当受害者查看电子邮件时,攻击者可以完全控制受害者的会话,并可以窃取他们的任何电子邮件,包括那些包含密码、文档和密码重置链接等高度敏感信息的电子邮件。让我们看看发生了什么,以及我们可以从中学到什么。
影响
发现的代码缺陷是一个存储跨站点脚本漏洞 (CVE-2022-29360),它会影响 RainLoop 的最新版本v1.16.0。在撰写本文时,还没有官方补丁可用。可以在使用默认配置运行的任何 RainLoop 安装中利用该漏洞。知道目标组织员工电子邮件地址的攻击者可以向受害者发送恶意制作的电子邮件。当在 webmail 界面中查看它时,它会在受害者的浏览器中执行隐藏的 JavaScript 有效负载。不需要进一步的用户交互。
技术细节
在以下部分中,我们将详细介绍存储的跨站点脚本漏洞,以及一旦受害者查看恶意电子邮件,小工具如何被滥用以使 JavaScript 自动运行。
存储在电子邮件正文中的 XSS (CVE-2022-29360)
RainLoop 的后端是一个 PHP 应用程序,它充当用户与其邮件服务器之间的代理。与邮件客户端(如 Thunderbird)类似,它使用户能够登录到邮件服务器、获取电子邮件、查看它们并发送电子邮件。
清理逻辑
由于 RainLoop 是一个 Web 应用程序,它需要将传入的电子邮件呈现为 HTML 代码。它还需要确保呈现的 HTML 代码已经过验证并且不包含恶意组件(例如,不安全的链接、JavaScript 标记)。
在高层次上,RainLoop 部署了以下流程来实现这一点:
- 从邮件服务器接收原始的、不受信任的 HTML 代码
- 在 PHP中创建内置DOMDocument类的实例。这会将 HTML 解析为 HTML 元素及其属性的树结构
- 根据配置,使用允许或拒绝列表删除树结构中的任何危险内容
- 将DOMDocument的净化树结构转换为 HTML 代码
直观地说,分析试图删除任何危险 HTML 代码的代码(上面列表中的第 3 步)并找到该代码内部的弱点以绕过清理程序是有意义的。但是,我们的经验表明,在执行清理步骤后经常会出现逻辑错误。从安全研究人员的角度来看,它们更容易发现并且经常被开发人员忽略:有关使用此模式的先前发现的良好示例,请参阅Zimbra Stored XSS和WordPress CSRF to RCE。
我们提到第 4 步将DOMDocument的树结构转换为 HTML 代码。通常,这一步很简单,因为DOMDocument类具有内置的saveHTML()方法,该方法完全符合要求。

伪造一个 HTML <body>
最后一个问题必须在经过净化的 HTML 代码呈现给用户之前解决:由于DOMDocument类执行的规范化,HTML 代码saveHTML()发出包含<html>和<body>标签。尽管这是完全有效且无害的,但呈现电子邮件的 RainLoop 前端页面已经包含<html>和<body>标签。
此外,<body>标签可能包含必须保留的重要属性,例如样式和类。RainLoop 通过解析电子邮件结构的<body>标记中的属性,然后将电子邮件的 HTML 代码包装在包含原始<body>属性的假正文中来解决这些问题。
在接下来的段落中,我们将描述这个过程在 RainLoop 中是如何工作的,展示相应的代码片段,最后描述这个过程中导致存储型 XSS 漏洞的逻辑缺陷。
在第一步中,RainLoop从树结构中获取对<html>和<body>节点的引用,然后在所有子节点上调用saveHTML()以获取没有<html>和<body>标记的净化 HTML 代码:
rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php
222 $oHtml = $oDom->getElementsByTagName('html')->item(0);
223 $oBody = $oDom->getElementsByTagName('body')->item(0);
224
225 foreach ($oBody->childNodes as $oChild)
226 {
227 $sResult .= $oDom->saveHTML($oChild);
228 }
在下一步中,获取<html>节点的属性并将其添加到新创建的<div>标签中以模拟<html>标签:
rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php
232 $aHtmlAttrs = HtmlUtils::GetElementAttributesAsArray($oHtml);
233 $aBodylAttrs = HtmlUtils::GetElementAttributesAsArray($oBody);
234
235 $oWrapHtml = $oDom->createElement('div');
236 $oWrapHtml->setAttribute('data-x-div-type', 'html');
237 foreach ($aHtmlAttrs as $sKey => $sValue)
238 {
239 $oWrapHtml->setAttribute($sKey, $sValue);
240 }
对于<body>标记重复此过程,但有一个重要区别:为保留<body>属性而创建的<div>标记是使用文本内容___xxx___创建的。然后这个假<body>被附加到假<html>节点并转储到 HTML 代码中:
rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php
242 $oWrapDom = $oDom->createElement('div', '___xxx___');
243 $oWrapDom->setAttribute('data-x-div-type', 'body');
244 foreach ($aBodylAttrs as $sKey => $sValue)
245 {
246 $oWrapDom->setAttribute($sKey, $sValue);
247 }
248
249 $oWrapHtml->appendChild($oWrapDom);
250
251 $sWrp = $oDom->saveHTML($oWrapHtml);
让我们用一个例子来看看这段代码。假设攻击者发送了以下电子邮件:
<html>
<body data-some-attr="abc">
<h1>Hello!</h1>
<p>wehope you are doing good!</p>
</body>
</html>
到目前为止,我们描述的过程将产生以下 HTML 代码,存储在$sWrp变量中:
<div data-x-div-type="html">
<div data-x-div-type="body" data-some-attr="abc">
___xxx___
</div>
</div>
在最后一步中,电子邮件的其余部分被插入到上面的包装代码中。这是通过用之前生成的 HTML 代码替换假包装体内部的____xxx___来完成的:
rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php
252 $sResult = \str_replace('___xxx___', $sResult, $sWrp);
这最终会产生以下 HTML 代码:
<div data-x-div-type="html">
<div data-x-div-type="body" data-some-attr="abc">
<h1>Hello!</h1>
<p>I hope you are doing good!</p>
</div>
</div>
逻辑错误
由于攻击者可以控制<body>标签的属性及其值,他们可以创建一个属性值为___xxx___的<body>标签。
例如,这可能会导致以下 HTML 标记:
<div data-x-div-type="html">
<div data-x-div-type="body" data-some-attr="___xxx___">
___xxx___
</div>
</div>
由于str_replace()尽可能多地替换___xxx___字符串,攻击者可以将受控用户输入插入到data-some-attr的引用值中。让我们假设攻击者制作了一封电子邮件,如下所示:
<body data-some-attr="___xxx___">
<div title="x onclick='alert(document.cookie);//' y">
XSS PoC
</div>
</body>
在这种情况下,将___xxx___替换为 HTML 代码的其余部分后,HTML 标记将产生以下结果:
<body data-some-attr="<div title="x onclick='alert(document.cookie);//' y">
XSS PoC
</div>">
修补
在撰写本文时,还没有官方补丁可用。我们推荐 RainLoop fork SnappyMail。它具有很大的安全性改进并得到积极维护。我们要感谢这个分支的维护者对这个问题的快速响应和分析。他们向我们证实他们没有受到影响。因此,我们建议 RainLoop 的用户长期迁移到 SnappyMail。
为了在短期内提供帮助,我们鼓励用户应用我们开发的以下非官方补丁(请谨慎使用,风险自负):
--- /tmp/HtmlUtils.php 2022-04-11 09:34:35.000000000 +0200
+++ rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php 2022-04-11 09:35:12.000000000 +0200
@@ -239,7 +239,8 @@
$oWrapHtml->setAttribute($sKey, $sValue);
}
- $oWrapDom = $oDom->createElement('div', '___xxx___');
+ $rand_str = base64_encode(random_bytes(32));
+ $oWrapDom = $oDom->createElement('div', $rand_str);
$oWrapDom->setAttribute('data-x-div-type', 'body');
foreach ($aBodylAttrs as $sKey => $sValue)
{
@@ -250,7 +251,7 @@
$sWrp = $oDom->saveHTML($oWrapHtml);
- $sResult = \str_replace('___xxx___', $sResult, $sWrp);
+ $sResult = \str_replace($rand_str, $sResult, $sWrp);
}
$sResult = \str_replace(\MailSo\Base\HtmlUtils::$KOS, ':', $sResult);
为了使用这个补丁:
- 创建 RainLoop 文件的备份!
- 将上面的补丁文件内容上传到名为rainloop_xss.patch的文件中,并将其存储在 RainLoop 安装的根目录中
- 运行以下命令:
patch rainloop/v/1.13.0/app/libraries/MailSo/Base/HtmlUtils.php < rainloop_xss.patch
请注意,您的路径可能会有所不同,具体取决于您使用的 RainLoop 版本。在上面的示例中,使用了 1.13.0 版本。确保在您的路径中使用正确的版本。
时间线
日期 | 行动 |
---|---|
2021-11-30 | 我们通过联系 [email protected] 请求安全联系人。没有反应 |
2021-12-06 | 我们通过创建 GitHub 问题请求安全联系人。没有反应 |
2022-01-03 | 我们通过电子邮件和 GitHub 问题联系供应商,并告知他们我们的 90 天披露政策。没有反应 |
概括
在这篇博文中,我们分析了 RainLoop 中的一个持久性跨站点脚本漏洞,该漏洞在受害者查看恶意制作的电子邮件时触发。该漏洞是由于清理过程之后的一个逻辑错误而发生的,而该漏洞经常被安全审计所忽视。我们在Zimbra和WordPress等备受瞩目的目标中发现了类似的错误。一般来说,我们建议开发人员在清理后不要修改任何数据,因为任何修改都可能逆转清理步骤。此外,建议使用 DOM 树对象,而不是对 HTML 文本进行操作,因为这样会留下更多的错误空间。
后续
SonarSource 在其披露时间表中表示,它已于 2021 年 11 月 30 日通知了 RainLoop 的维护者该漏洞,并且该软件制造商已经四个多月没有发布修复程序。
这家瑞士代码质量和安全公司于 2021 年 12 月 6 日在 GitHub 上提出的一个问题至今仍未解决。我们已经联系 RainLoop 征求意见,如果我们收到回复,我们会更新故事。
在没有补丁的情况下,SonarSource 建议用户迁移到名为SnappyMail的 RainLoop 分支,该分支得到积极维护并且不受安全问题的影响。
转载请注明出处及链接