拆分电子邮件原子:利用解析器绕过访问控制
引言
一些网站解析电子邮件地址以提取域名并推断所有者所属的组织。这种模式使得电子邮件地址解析差异变得至关重要。预测电子邮件将路由到哪个域名应该很简单,但实际上非常困难——即使是“有效”且符合RFC的地址。
在本文中,我将向您展示如何将电子邮件解析差异转化为访问控制绕过甚至远程代码执行(RCE)。
本文附带一个免费的在线CTF,因此您可以立即尝试新技能。
您还可以将此论文作为打印/下载友好的PDF获取。您还可以从Black Hat获取幻灯片。
我在Black Hat和DEF CON上发表了此演讲。您可以在此处观看:
大纲
- 引言
- 创建电子邮件域名混淆
- 解析器差异
- Unicode溢出
- 编码字
- 编码字案例研究
- GitHub
- Zendesk
- GitLab
- PHPMailer
- Punycode
- 什么是Punycode?
- 畸形Punycode
- 尝试利用Joomla
- 利用Joomla
- 方法论/工具
- 生成电子邮件拆分攻击
- 自动化利用编码字
- 模糊测试畸形Punycode
- 奖励材料
- 防御
- 材料
- CTF
- 时间线
- 参考文献
- 要点
引言
一些规定电子邮件地址格式的RFC已经存在了50多年,它们被混杂在一起形成了一个过于宽松的电子邮件地址标准。电子邮件可以包含引号值、注释、转义和各种编码。如果您面临编写电子邮件解析器的任务,技术上您应该遵循规范,但由于所有这些复杂性,这是一项困难的工作。Web应用程序将这种复杂性外包给电子邮件解析库,因此它们实际上不知道电子邮件是如何解析的。当它们决定基于电子邮件域名做出安全决策时,这会导致问题。
如果您查看RFC2822的3.2.5和3.2.2节,它允许您使用引号值和转义。它们使您能够在电子邮件地址的本地部分使用通常不允许的字符。一些例子是:
|
|
在第一个例子中,由于本地部分被引用,at符号将用作目标邮箱,引号被移除。在第二个例子中,它展示了如何在引用的本地部分内使用转义来使用双引号作为目标邮箱。如果我们更深入地看同一RFC节3.2.3,我们可以看到它支持注释。注释使用括号构造,可以包含空格甚至嵌套。以下是一些使用注释的“有效”电子邮件示例:
|
|
您不仅限于字母数字值;您可以在注释中放置多种字符。这一切似乎都 ripe for abuse,通过在解析器、应用程序和邮件程序之间制造混淆。我的研究之旅始于尝试通过滥用转义和注释来制造这种混淆。
创建电子邮件域名混淆
我不为发现这个故事的方式感到自豪,但这是事实。我没有花几个小时用调试器查看Postfix和Sendmail源代码,肯定有随机性和运气的元素。
它始于我登录一个用于测试的盒子时,我安装了一个未命名的应用程序并开始测试电子邮件解析差异。我一无所获。我尝试的一切都失败了,我完全放弃了研究的想法。然后出于绝望,我取了应用程序使用的特殊字符并将其粘贴到我的电子邮件地址中。我知道它会有效,因为所有字符都是它们允许的,但我只是想看看邮件程序会发生什么。
我检查了盒子的系统日志,注意到我收到了一个带有无效主机的DSN(投递状态通知)。对此感到惊讶,我开始深入挖掘。我开始从电子邮件地址中移除字符,以缩小Sendmail认为它是无效主机的原因。最终,我将其缩小到感叹号,并想起了在进行这项研究时读到的UUCP协议。
UUCP是互联网和电子邮件之前存在的一种古老协议。它允许您在Unix系统之间发送消息,代表Unix到Unix复制。它使用感叹号作为域和用户部分之间的分隔符,但顺序与传统电子邮件地址相反。
这太疯狂了,纯粹靠运气,我粘贴的字符以反斜杠结尾,转义了at符号,然后感叹号将地址视为UUCP地址!以下是我的发现:
原始发现:
|
|
自然,我必须用不同的Collaborator域名跟进以确保它实际上会转到不同的服务器:
|
|
前面的例子在使用Sendmail 8.15.2时转到Collaborator域名“oastify.com”,而不是example.com。这对我来说非常令人兴奋,因为我证明这项研究实际上有所进展。下一步是找到导致这种行为其他字符,所以我很快写了一个SMTP模糊测试器。我发现Postfix没有这种行为,因为它更安全,对吧?嗯,那是我在想,直到我通过模糊测试器在Postfix 3.6.4中找到了一个变体:
|
|
这实际上转到psres.net而不是example.com,并使用了另一种古老的协议称为源路由。源路由允许您使用服务器链发送邮件。想法是您用逗号分隔每个主机,然后在末尾包括最终目的地。还有所谓的“百分比黑客”,即邮件程序将%或不同选择的字符转换为at符号,然后将电子邮件转发到服务器。这个例子说明了这一点:
|
|
在这个过程中,电子邮件最初发送到example.com,之后百分比符号转换为at符号,电子邮件发送到foo@psres.net。这正是向量发生的情况,括号注释了电子邮件地址的域部分,然后Postfix使用本地部分作为源路由,将电子邮件发送到意外目的地。Postfix实际上也支持UUCP。我后来发现如果您使用单括号技巧。
这些发现给了我信心,认为那里有大量错误,所以我开始寻找更多。
解析器差异
Unicode溢出
我必须用这项研究解决的主要问题之一是生成被阻止的字符。由于许多Web应用程序会阻止多个at符号。这就是为什么我开始研究unicode溢出。
我正在测试一个未命名的目标,并注意到当使用更高的unicode字符时,它们会生成其他ASCII字符。这种模式起初看起来随机,但后来我明白了发生了什么。最好从PHP中chr()算法的工作原理图像中说明。chr()函数返回由整数代码点指定的字符:
在例子中,PHP循环遍历字节并检查是否小于零,如果是,则添加256直到为正。然后它执行模运算以使值适合0-255范围。这意味着如果您传递大于255的字节值,它将溢出并由于模运算被迫进入0-255范围。这正是unicode溢出的工作方式;我们只需要提供一个代码点大于255的字符来生成其他字符。最好用一个简单例子说明:
|
|
在前面的例子中,我使用fromCodePoint函数生成一个字符,我传递一个十六进制值0x100,转换为256十进制,然后我添加0x40,即at符号的十六进制数。然后当系统执行像PHP中chr()函数的操作时,unicode代码点将溢出并适合0-255,然后生成at符号。
发现这个后,我开始用Turbo Intruder模糊测试未命名的目标,并注意到其他字符表现出这种行为。起初看起来随机,但后来我意识到发生了什么,0x100只是您可以用来执行溢出的数字之一。如果您使用更高的字符,您可以使用之间的任何字符。
|
|
每个十六进制值都创建溢出,因为模运算将导致零,这可以继续直到当前最大unicode代码点0x10ffff。这个目标允许所有 sort of unicode字符创建其他字符:
|
|
如果您对每个字符执行256模运算,它将导致生成的字符:
|
|
虽然我能够欺骗广泛字符,但我无法用此技术在此未命名目标上拆分电子邮件。但这只是开始,我证明了生成被阻止字符是可能的。这给了我寻找更多的信心。
编码字
我越开始看,电子邮件RFC越想给。我在这项研究之前假设电子邮件通常是字母数字,本地部分中有点。我从未想象存在一个完整的复杂编码系统,允许您执行层编码。然而这是我发现的。浏览RFC时,我注意到rfc2047和编码字,此编码系统允许您使用十六进制和base64表示字符。
如果我们使用编码电子邮件作为例子说明:
|
|
“=?”表示编码字的开始,然后您指定字符集,在这种情况下UTF-8。然后问号分隔下一个命令,即“q”,表示“Q-Encoding”,之后有另一个问号表示编码格式结束和编码数据开始。Q-Encoding只是带有等号前缀的十六进制。在这个例子中,我使用=41=42=43,即大写“ABC”。最后,?=表示编码结束。当由电子邮件库解析时,电子邮件目的地将是ABCUSER@psres.net!
有了这些信息,我开始寻找使用此编码解析电子邮件的真实系统。为了帮助这一点,我想出了两个探针,在大多数具有此行为的网站上工作:
|
|
最初我使用字符集“x”来减少探针大小,然而一些系统拒绝未知字符集并会失败。最好使用这两个探针,因为我在测试许多网站后发现它们是最常见的允许字符集。使用Collaborator生成有效负载并将上面的“collab”替换为生成的。然后如果您在SMTP对话的RCPT TO命令中获得SMTP交互:
|
|
这然后证明电子邮件解析器正在用“编码字”解码电子邮件。
我找到了一堆具有此行为的网站,它们都有一个共同点。Ruby。似乎它们都使用相同的Ruby Gem称为“Mail”,它有超过5.08亿次下载。我开始查看源代码,我发现库正在解码UTF-7!在我的测试床中,我尝试重现这一点:
|
|
这太疯狂了!电子邮件现在可以有UTF-7了!然后一个想法涌入我的脑海:如果有Q-Encoding和字符集,你能两者都有吗?这个问题的惊人答案是响亮的 yes。您可以混合UTF-7与Q-Encoding!
|
|
之后我开始玩base64编码,因为当然“编码字”在电子邮件中支持那个!您只需在编码类型中使用“b”而不是“q”,您就可以使用它。
|
|
前面的例子使用base64编码字符串“foobar”,由解析器解码。我知道您在想什么或也许只是我,但是的您可以使用UTF-7和base64编码数据:
|
|
在这个例子中,有一个base64编码地址与UTF-7字符集。首先电子邮件解析器将解码base64。然后电子邮件解析器将解码UTF-7字符集。最后电子邮件将解码为foobar@psres.net。此时您可能对逐字遵循RFC有一些怀疑。尤其当我告诉您这在我测试Mail库时在域部分也工作时。注意我在这里使用字母数字值,但您当然也可以编码任何特殊字符。
编码字案例研究
GitHub:访问受Cloudflare“Zero Trust”保护的内部网络
到目前为止,我们已经看到如何创建电子邮件域名混淆和令人惊讶的编码,但现在是时候使用这些知识来利用真实系统了。我测试的第一个目标之一是GitHub。我特别针对GitHub,因为我知道它是用Ruby编写的。
我使用前面提到的两个探针来确认GitHub支持“编码字”。电子邮件在Collaborator SMTP对话中被解码!所以我开始进一步测试。我需要做的是使用“编码字”来产生另一个at符号。起初我开始玩引用的本地部分值,我成功地在引用值中嵌入原始at符号。也许我可以在引用的本地部分内使用“编码字”来突破引用值并产生两个不同地址?我试验了=22(双引号)和=40(at符号),但没有成功。
这项研究的麻烦是您有时得不到任何反馈,因为它通过电子邮件验证但在到达邮件程序之前失败。您可以使用DNS交互作为线索,但它们通常几乎无用,因为您无法识别失败到达邮件程序的原因。
经过多次尝试,我开始考虑SMTP对话,我尝试放置大于字符。这里的想法是我可以用它来结束SMTP对话中的RCPT TO命令:
|
|
前面的例子显示了一个引用的本地部分与原始at符号和大于。您可以看到攻击如何形成。您有两个地址,使用大于的想法然后使您能够忽略SMTP对话中的第二个地址。有了这个想法固定在我的脑海中,我开始使用编码向量来构建攻击。
我很快发现双引号对GitHub没有任何用处,原因是它总是留下一个开放双引号,这将失败验证。我当然尝试编码和转义,但没有成功。我移除引号并使用“编码字”来生成at符号和大于,它通过验证,但我没有收到电子邮件。没有SMTP对话。什么都没有。考虑这一点,我想也许电子邮件末尾的尾随垃圾导致邮件程序失败,要么异常要么验证。如果我能引入一些字符来避免异常或验证呢?我尝试编码空白,但失败了,然后我尝试编码null,并bingo!我有以下电子邮件的交互:
|
|
对于GitHub,字符集无关紧要,所以我使用“x”,编码的at符号(=40)转换为at,大于(=3e)完成RCPT TO命令,最后null(=00)使邮件程序忽略之后的一切,您需要在编码后放置一个有效的本地部分,所以我使用“foo”,这成功通过验证并拆分电子邮件。然后我可以验证任何我喜欢的电子邮件域。我已经用microsoft.com、mozilla.com和github.com验证了我的测试帐户上的地址:
|
|
这已经是一个错误,因为您不应该能够验证您不拥有的地址。然后我的同事James Kettle建议我查看Cloudflare“Zero Trust”,看看它是否可以配置为信任某些电子邮件域。我创建了一个测试帐户并深入研究配置,发现您可以使用GitHub作为IdP,并使用电子邮件域来确定您是否有权访问站点。这可能是内部网络或任何其他受Zero Trust保护的域,只要它们使用GitHub作为IdP。
Zendesk:访问电子邮件域保护的支持中心
在GitHub成功后,我开始寻找使用Ruby并具有某种电子邮件域验证的应用程序。一个突出的是Zendesk,因为也许您可以访问受保护的支持台?在尝试拆分电子邮件地址之前,我搜索了他们的文档,发现您需要打开支持中心,允许注册,然后选择允许注册的域。
支持中心配置好后,我开始测试。我尝试了所有在GitHub上使用的攻击,但没有成功。也许他们使用不同的邮件程序或验证?我尝试了一些新想法,使用电子邮件的引用本地部分,并且我在Collaborator中得到的交互似乎比测试GitHub时更有希望。
我发现有用的是使用两个重复的Collaborator域,所以我总是得到交互,并通过检查SMTP对话,您可以看到什么被转换。我发送了以下内容:
|
|
这个交互告诉了我一堆事情,首先是它们允许大写。接下来是它们允许转换空格,第三是它们在解码时似乎引用通常不允许在本地部分中的值。也许我可以滥用这种行为?
经过更多尝试,我终于有所进展。我欺骗了解析/验证来转换被阻止的字符,双重编码引号,并生成将被它们的代码移除的字符,直到最终我构建了一个有效的电子邮件拆分攻击:
|
|
使用此“电子邮件”,我能够绕过支持中心设置的限制。此攻击的关键是嵌入的编码引号,由解析器解码。然后=3c22生成一个小于字符,被移除,然后完成引号,因此它通过它们的验证/异常。您会注意到“=3e=00”是我在GitHub上使用的相同序列,所以它们显然共享一些相同代码,但它们响应的方式大不相同,因此攻击更完成。
GitLab:获得对GitLab Enterprise服务器的未授权访问
寻找更多Ruby新鲜肉,我转向GitLab。它们是一个IdP并提供Enterprise产品,所以似乎是一个很好的测试目标。James有一个他之前测试的GitLab服务器,所以我首先开始看那个。您可以配置它允许具有特定域的注册。所以这立即引起了我的注意。我尝试了在GitHub和Zendesk上使用的向量,但它们没有工作。然后我记得“编码字”允许您使用下划线作为空格,这个向量是我迄今为止演示的最优雅的:
|
|
我使用Postfix作为配置的Enterprise实例的邮件程序。您可以使用=20做同样的事情,但下划线是1个