解析邮件原子:利用解析器绕过访问控制

本文探讨了电子邮件地址解析中的漏洞,展示了如何利用解析器差异绕过访问控制,甚至实现远程代码执行(RCE)。内容包括Unicode溢出、编码字(encoded-word)滥用、Punycode攻击,以及针对GitHub、Zendesk、GitLab和Joomla的实际案例研究。

解析邮件原子:利用解析器绕过访问控制

引言

一些网站解析电子邮件地址以提取域名并推断所有者所属的组织。这种模式使得电子邮件地址解析器差异变得至关重要。预测电子邮件将路由到哪个域名应该很简单,但实际上非常困难——即使是“有效”且符合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节,它允许您使用引号值和转义。它们使您能够在电子邮件地址的本地部分使用通常不允许的字符。一些例子是:

1
2
"@"@example.com
"\""@example.com

在第一个例子中,由于本地部分被引用,at符号将用作目标邮箱,引号被移除。在第二个例子中,它展示了如何在引用的本地部分内使用转义来将双引号用作目标邮箱。如果我们更深入地看同一RFC节的3.2.3,我们可以看到它支持注释。注释使用括号构造,可以包含空格甚至嵌套。以下是一些使用注释的“有效”电子邮件示例:

1
(foo)user@(bar)example.com

您不仅限于字母数字值;您可以在注释中放置多种字符。这一切似乎都 ripe for abuse,通过在解析器、应用程序和邮件程序之间制造混淆。我的研究之旅始于尝试通过滥用转义和注释来制造这种混淆。

创建电子邮件域名混淆

我并不自豪于这个故事是如何发现的,但这是事实。我没有花几个小时用调试器查看Postfix和Sendmail源代码,肯定有随机性和运气的元素。

它始于当我登录到一个用于测试的盒子时,我安装了一个未命名的应用程序并开始测试其电子邮件解析差异。我一无所获。我尝试的一切都失败了,我完全放弃了研究的想法。然后出于绝望,我取了应用程序使用的特殊字符并将其粘贴到我的电子邮件地址中。我知道它会有效,因为这是它们允许的所有字符,但我只是想看看邮件程序会发生什么。

我检查了盒子的系统日志,注意到我收到了一个带有无效主机的DSN(投递状态通知)。对此感到惊讶,我开始更深入地挖掘。我开始从电子邮件地址中移除字符,以缩小Sendmail认为它是无效主机的原因。最终,我将其缩小到感叹号,并想起了在进行这项研究时读到的UUCP协议。

UUCP是一种古老的协议,存在于互联网和电子邮件之前。它允许您在Unix系统之间发送消息,代表Unix到Unix复制。它使用感叹号作为域和用户部分之间的分隔符,但顺序与传统电子邮件地址相反。

这太疯狂了,纯粹靠运气,我粘贴的字符以反斜杠结尾,它转义了at符号,然后感叹号将地址视为UUCP地址!以下是我的发现:

原始发现:

1
!#$%&'*+\/=?^_`{|}~-collab\@psres.net

自然,我必须用不同的Collaborator域名跟进,以确保它实际上会转到不同的服务器:

1
oastify.com!collab\@example.com

前面的例子在使用Sendmail 8.15.2时转到Collaborator域名“oastify.com”,而不是example.com。这对我来说非常令人兴奋,因为我证明这项研究实际上有所进展。下一步是找到导致这种行为的其他字符,所以我很快写了一个SMTP模糊测试器。我发现Postfix没有这种行为,因为它更安全,对吧?嗯,这就是我所想的,直到我通过模糊测试器在Postfix 3.6.4中发现了一个变体:

1
collab%psres.net(@example.com

这实际上转到psres.net而不是example.com,并使用了另一个古老的协议称为源路由。源路由允许您使用一系列服务器链来发送邮件。想法是您用逗号分隔每个主机,然后在末尾包括最终目的地。还有所谓的“百分比黑客”,即邮件程序将%或不同的选定字符转换为at符号,然后将电子邮件转发到服务器。这个例子说明了这一点:

1
foo%psres.net@example.com        foo@psres.net

在这个过程中,电子邮件最初发送到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的字符来生成其他字符。最好用一个简单的例子说明:

1
String.fromCodePoint(0x100 + 0x40)

在前面的例子中,我使用fromCodePoint函数生成一个字符,我传递一个十六进制值0x100,转换为256十进制,然后我添加0x40,这是at符号的十六进制数。然后当系统执行像PHP中的chr()函数这样的操作时,unicode代码点将溢出并拟合到0-255内,然后生成at符号。

在我发现这个之后,我开始用Turbo Intruder模糊测试未命名的目标,并注意到其他字符表现出这种行为。起初看起来随机,但然后我意识到发生了什么,0x100只是您可以用来执行溢出的数字之一。如果您使用更高的字符,您可以使用之间的任何字符。

1
2
3
4
5
String.fromCodePoint(0x100 + 0x40) // ŀ → @
String.fromCodePoint(0x1000 + 0x40) // ၀ → @
String.fromCodePoint(0x10000 + 0x40) // 𐁀 → @
...
0x10ffff

每个十六进制值都创建溢出,因为模运算将导致零,并且这可以继续直到当前最大unicode代码点0x10ffff。这个目标允许所有类型的unicode字符创建其他字符:

1
2
3
4
5
6
7
'✨' === '('
'✩' === ')'
'✻' === ';'
'✼' === '<'
'✽' === '='
'✾' === '>'
'❀' === '@'

如果您对每个字符执行256模运算,它将导致生成的字符:

1
2
3
4
//Mod each code point by 256
'❀'.codePointAt(0) % 256 === 0x40
String.fromCodePoint(0x40)
// @

尽管我能够欺骗广泛的字符,但我无法在这个未命名的目标上用这种技术拆分电子邮件。但这只是开始,我证明了生成被阻止的字符是可能的。这给了我寻找更多的信心。

编码字

我开始看得越多,电子邮件RFC想要给的越多。在这项研究之前,我假设电子邮件通常是字母数字,本地部分中有点。我从未想象存在一个完整的复杂编码系统,允许您执行多层编码。然而这就是我发现的。浏览RFC时,我注意到rfc2047和编码字,这种编码系统允许您使用十六进制和base64表示字符。

如果我们使用编码电子邮件作为示例说明:

1
=?UTF-8?q?=41=42=43USER?=@psres.net

“=?”表示编码字的开始,然后您指定字符集,在这种情况下是UTF-8。然后问号分隔下一个命令,即“q”,表示“Q-Encoding”,之后有另一个问号表示编码格式的结束和编码数据的开始。Q-Encoding只是带有等号前缀的十六进制。在这个例子中,我使用=41=42=43,这是大写的“ABC”。最后,?=表示编码的结束。当由电子邮件库解析时,电子邮件目的地将是ABCUSER@psres.net!

有了这些信息,我开始寻找使用这种编码解析电子邮件的真实系统。为了帮助这一点,我想出了两个探针,在大多数具有这种行为的网站上工作:

最初我使用字符集“x”来减少探针的大小,然而一些系统拒绝未知字符集并会失败。最好使用这两个探针,因为我在测试许多网站后发现它们是最常见的允许字符集。使用Collaborator生成有效载荷,并将上面的“collab”替换为生成的。然后如果您在SMTP对话的RCPT TO命令中获得SMTP交互:

1
abccollab@psres.net

这然后证明电子邮件解析器正在用“编码字”解码电子邮件。

我找到了一堆具有这种行为的网站,它们都有一个共同点。Ruby。似乎它们都使用相同的Ruby Gem称为“Mail”,它有超过5.08亿次下载。我开始查看源代码,我发现库正在解码UTF-7!在我的测试台中,我尝试重现这一点:

1
=?UTF-7?q?+AG8AbwBiAGEAcg-?=@psres.net

这太疯狂了!电子邮件现在可以有UTF-7了!然后一个想法涌入我的脑海:如果有Q-Encoding和字符集,你能两者都有吗?这个问题的惊人答案是响亮的 yes。您可以将UTF-7与Q-Encoding混合!

之后我开始玩base64编码,因为当然“编码字”在电子邮件中支持那个!您只需在编码类型中使用“b”而不是“q”,您就可以使用它。

1
=?UTF-8?b?Zm9vYmFy?=@psres.net

前面的例子使用base64编码字符串“foobar”,由解析器解码。我知道您在想什么,或者可能只是我,但是的,您可以使用UTF-7和base64编码数据:

1
=?UTF-7?b?ZgBvAG8AYgBhAHIA?=@psres.net

在这个例子中,有一个base64编码地址带有UTF-7字符集。首先电子邮件解析器将解码base64。然后电子邮件解析器将解码UTF-7字符集。最后电子邮件将解码为foobar@psres.net。此时您可能对逐字遵循RFC有一些怀疑。尤其当我告诉您这在我测试Mail库时在域部分也工作时。注意我在这里使用字母数字值,但您当然也可以编码任何特殊字符。

编码字案例研究

GitHub:访问受Cloudflare“零信任”保护的内部网络

到目前为止,我们已经看到如何创建电子邮件域名混淆和令人惊讶的编码,但现在是时候使用这些知识来利用真实系统了。我测试的第一个目标之一是GitHub。我特别针对GitHub,因为我知道它是用Ruby编写的。

我使用前面提到的两个探针确认GitHub支持“编码字”。电子邮件在Collaborator SMTP对话中被解码!所以我开始进一步测试。我需要做的是使用“编码字”来产生另一个at符号。起初我开始玩引用的本地部分值,我成功地在引用的值中嵌入原始at符号。也许我可以在引用的本地部分内使用“编码字”来突破引用的值并产生两个不同的地址?我尝试了=22(双引号)和=40(at符号),但没有成功。

这项研究的麻烦是您有时得不到任何反馈,因为它通过电子邮件验证但在到达邮件程序之前失败。您可以使用DNS交互作为线索,但它们通常几乎无用,因为您无法识别失败到达邮件程序的原因。

经过多次尝试,我开始考虑SMTP对话,我尝试放置大于字符。这里的想法是我可以使用它来结束SMTP对话中的RCPT TO命令:

1
RCPT TO:<"collab@psres.net>collab"@psres.net>

前面的例子显示了一个引用的本地部分带有原始at符号和大于。您可以看到攻击如何形成。您有两个地址,使用大于的想法然后使您能够忽略SMTP对话中的第二个地址。有了这个想法固定在我的脑海中,我开始使用编码向量来构建攻击。

我很快发现双引号对GitHub没有任何用处,原因是它总是留下一个开放的双引号,这将失败验证。我当然尝试编码和转义,但没有成功。我移除引号并使用“编码字”来生成at符号和大于,它通过验证,但我没有收到电子邮件。没有SMTP对话。什么都没有。考虑这一点,我想也许电子邮件末尾的尾随垃圾导致邮件程序失败,要么有异常要么验证。如果我能引入一些字符来避免异常或验证呢?我尝试编码空格,但失败了,然后我尝试编码null,然后bingo!我有了以下电子邮件的交互:

1
=?x?q?collab=40psres.net=3e=00foo?=@psres.net

对于GitHub,字符集无关紧要,所以我使用“x”,编码的at符号(=40)转换为at,大于(=3e)结束RCPT TO命令,最后null(=00)使邮件程序忽略之后的一切,您需要在编码后放置一个有效的本地部分,所以我使用“foo”,这成功通过验证并拆分电子邮件。然后我可以验证任何我喜欢的电子邮件域。我已经用microsoft.com、mozilla.com和github.com验证了我的测试账户上的地址:

1
=?x?q?collab=40microsoft.com=3e=00foo?=@psres.net

这已经是一个错误,因为您不应该能够验证您不拥有的地址。然后我的同事James Kettle建议我查看Cloudflare“零信任”,看看它是否可以配置为信任某些电子邮件域。我创建了一个测试账户并深入配置,发现您可以使用GitHub作为IdP,并使用电子邮件域来确定您是否有权访问站点。这可能是内部网络或任何其他受零信任保护的域,只要它们使用GitHub作为IdP。

Zendesk:访问电子邮件域保护的支持中心

在GitHub成功后,我开始寻找使用Ruby并具有某种电子邮件域验证的应用程序。一个突出的是Zendesk,因为也许您可以获得受保护支持台的访问权限?在我尝试拆分电子邮件地址之前,我搜索了他们的文档,发现您需要打开支持中心,允许注册,然后选择允许注册的域。

支持中心配置好后,我开始测试。我尝试了所有在GitHub上使用的攻击,但没有成功。也许他们使用不同的邮件程序或验证?我尝试了一些新想法,使用电子邮件的引用本地部分,并且我在Collaborator中得到的交互似乎比测试GitHub时更有希望。

我发现有用的是使用两个重复的Collaborator域,所以我总是得到交互,并通过检查SMTP对话,您可以看到什么被转换。我发送了以下内容:

1
2
Input:
=?x?q?=41=42=43collab=40psres.net=3e=20?=@psres.net

并得到以下返回:

1
2
Output:
RCPT TO:<"ABCcollab@psres.net> "@psres.net>

这个交互告诉了我一堆事情,首先是它们允许大写。接下来是它们允许转换空格,第三是它们似乎在解码时引用通常不允许在本地部分中的值。也许我可以滥用这种行为?

经过更多尝试,我终于有所进展。我欺骗了解析/验证来转换被阻止的字符,双重编码引号,并生成将被其代码移除的字符,直到最终我构建了一个有效的电子邮件拆分攻击:

1
=?x?q?collab=40psres.net=3c22=3e=00foo?=@psres.net

使用这个“电子邮件”,我能够绕过支持中心设置的限制。这个攻击的关键是嵌入的编码引号,由解析器解码。然后=3c22生成一个小于字符,被移除,然后完成引号,因此它通过它们的验证/异常。您会注意到“=3e=00”是我在GitHub上使用的相同序列,所以它们显然共享一些相同的代码,但它们响应的方式非常不同,因此攻击更完整。

GitLab:获得对GitLab Enterprise服务器的未授权访问

寻找更多Ruby新鲜肉,我转向GitLab。它们是一个IdP并提供Enterprise产品,所以似乎是一个很好的测试目标。James有一个他之前测试过的GitLab服务器,所以我首先开始查看那个。您可以配置它允许具有特定域的注册。所以这立即引起了我的注意。我尝试了在GitHub和Zendesk上使用的向量,但它们没有工作。然后我记得“编码字”允许您使用下划线作为空格,这个向量是我迄今为止展示的最优雅的:

1
=?x?q?collab=40psres.net=5f?=@psres.net

我使用Postfix作为配置的Enterprise实例的邮件程序。您可以使用=20做同样的事情,但下划线是1个字符,我喜欢优雅的向量!

这意味着我可以获得对使用基于域注册限制的GitLab Enterprise服务器的访问权限。正如我提到的,GitLab也是一个IdP,所以我也开始测试Web应用程序。Enterprise黑客在这里没有工作。我认为那是因为它们使用不同的邮件程序。然而,我没花多长时间就找到了另一个向量。到现在我已经收集了一堆向量,所以我有一个Turbo Intruder脚本,

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计