Citrix虚拟应用和桌面的登录前RCE通杀漏洞(CVE未公开)
- 参考Github地址(EXP):github.com/XiaomingX/e…
我们又来了,这次是一个新的漏洞链,涉及到Citrix的“虚拟应用和桌面”产品。这是一种技术架构,允许用户(包括黑客)几乎可以从任何地方访问他们的完整桌面环境,无论是笔记本电脑、平板还是手机。
这本质上是30年前备受热议的“瘦客户机”体验。不同于每个设备上存储软件和文件,应用程序(或整个桌面)运行在一个安全的数据中心的大服务器上,通过网络流式传输给用户。它和远程桌面有些相似,但显得更具企业级特点。
企业青睐这种技术的原因:
-
完整桌面体验:用户感觉就像是在使用自己的桌面,所有常用的应用、文件和设置都在。用户很难察觉到应用放在网络的另一头,随时可以在不同设备上继续工作。
-
管理方便:虚拟桌面对IT团队而言是一种理想的管理方式。管理员可以集中管理、更新和排查问题,而无需在每个员工的设备上操作。
-
增强安全性:数据存储在中心位置,更容易管理安全。如果设备丢失,数据依然安全,因为所有数据都在云端,无需远程擦除。
-
远程工作友好:用户可以从家里、办公室或其他任何网络可达的地方登录桌面,适合灵活的工作安排。
虽然Citrix的虚拟桌面使得用户能够无缝使用桌面体验,但攻击者同样喜欢这种技术,因为一旦企业应用开放,就意味着攻击者也只需要一步认证即可访问。
最近一个漏洞(CVE-2024-6151)可以让任何虚拟桌面用户获得系统权限,这实际上比听上去要更严重,因为这意味着攻击者可以伪装成任何用户,包括管理员,并监控其行为。
由于系统是无缝和便携的,攻击者可以更容易地伪装成用户,甚至“影子监控”他们的一举一动。这种集中管理的系统可能变成一个“全景监控”。
然而,目前很少看到真正未经认证的远程代码执行(RCE)漏洞的报告,可能是因为瘦客户机架构内特权升级攻击十分强大,或者这些解决方案在设计时就考虑到了企业远程访问工具所需的安全防范。
Citrix的会话录制功能 - “万恶之源”
“瘦客户端”解决方案特别适合监控用户活动,Citrix的“会话录制”功能就是一个例子。它可以记录用户的键盘和鼠标操作,连同桌面反应的视屏流。管理员可以实时查看用户的活动,对维护、合规和问题排查极为重要。
会话流的整体过程为:
- 用户登录
- 进入虚拟应用和桌面环境
- 开始会话
- 会话录制服务监控会话并在符合条件时开始录制
- 记录用户操作
- 保存会话录制以供管理员审核
我们的目的是深入了解这个功能的架构,包括Citrix工程师如何安全地录制用户的会话、处理数据及在环境内传输。这不仅仅是开启一个录屏工具那么简单。
要可靠地、按规模进行录制是极具挑战性的,这不像在笔记本电脑上简单按下“录制”按钮。而是需要在一个企业级产品中,流式传输和录制多个远程桌面会话。 由于技术要求高,加上实时数据流处理、存储和安全传输的复杂协调,系统中包含了无数复杂的环节。复杂性往往带来出错的机会,这些错误有时会导致严重的安全漏洞。
我们希望深入了解这个复杂的功能,帮助大家意识到Citrix工程师所面临的挑战,以及为什么对这些功能进行仔细审查非常重要。理解这些细节不仅强调了Citrix技术的广泛性,还揭示了这样一个强大但复杂的系统中可能存在的安全隐患。
我们在审查相关流程和文档后,绘制了以下的图示:
Citrix会话录制概念架构
+-------------+ +---------+ +----------------------------+
| 用户 | -----> | Citrix | -----> | 虚拟桌面 |
+-------------+ +---------+ | 和服务器 |
| |
| +------------------------+ |
| | Windows 10/11 VDA | |
| +------------------------+ |
+----------------------------+
+----------------------------------+
| 会话录制服务器 |
+----------------------------------+
| | |
v v v
+------------------+ +----------------+ +-----------+
| 录制策略控制台 | | 录制播放器 | | 数据库 |
+------------------+ +----------------+
如图所示,系统中有一个“会话录制服务器”,它可以与Citrix虚拟应用程序和桌面安装在同一台机器上,也可以在另一台机器上。当Citrix组件录制会话后,它会将录制内容传递给“会话录制服务器”,后者会将录制文件和相关的元数据(如提交用户、日期等)存储在数据库中。
之后,授权的管理员可以使用播放器组件查看会话录像,播放器会查询与会话录制服务器连接的数据库,检索相关信息。最后,“录制策略控制台”允许管理员设置更精细的触发条件,以决定何时开始录制会话,例如每当用户访问特定的高价值文件共享时即可开始录制。
我们已经理解了这些组件的作用,但还有一个问题:这些组件是如何彼此通信的?是通过网络套接字,也许是命名管道,或是某种共享内存?
为了答复这些问题,我们开始查看文件系统,寻找相关的可执行程序。幸运的是,Citrix将文件夹结构整理得很有条理。我们找到了一个名为“SessionRecording”的目录,里面肯定包含与会话录制相关的组件。
在这个目录下,我们发现了几个组件的文件夹,例如数据库组件和播放器组件,最重要的是“服务器”组件,里面包含了我们关注的逻辑。
我们仔细检查这些组件,寻找与其它组件交流的迹象。在过程中,我们发现了一个名为“SsRecStorageManager.exe”的可执行文件,令人好奇的是,它默认作为Windows服务运行。
这个组件负责处理会话录制的存储,图标是摄像机,这让人忍不住想要一探究竟——作为黑客,谁不想像摄像机一样监视会话呢?
当然,我们这些聪明的黑客,第一步是查看该组件的文档,而不是直接使用反编译工具。我们快速搜索后发现以下信息:
Citrix会话录制存储管理器是一个Windows服务,它管理来自每台启用会话录制的XenApp和XenDesktop计算机的录制文件。存储管理器通过Microsoft消息队列(MSMQ)服务接收会话录制的消息字节。为了保持录制的完整性,存储管理器必须快速处理接收到的消息。
现在我们对这个服务有了一般的理解。它通过MSMQ接收录制的会话文件。MSMQ允许两个独立的进程通过“队列”进行通信,一个进程可能会将“列出数据库中所有录制内容”的消息入队,另一个进程则可以从队列中获取这个消息,并将录制数据的列表放回队列。
不过,这个过程有一个重要的细节。
因为队列处理的是在进程之间(甚至机器之间)传递的数据,所以需要某种形式的转换。我们不能直接将内存块放入队列,因为接收方可能无法理解,必须经过序列化过程,将数据转换为可被其他一方理解的格式(对于.NET爱好者来说,通常称为“Marshalling”)。
复杂性是错误藏匿的温床,而历史上,序列化接口常常是攻击者悄悄插入恶意数据的渠道,这些数据会被信任的应用程序反序列化并处理,仿佛它们来自可信方。值得注意的是,MSMQ组件并未通过TCP暴露在网络上,不过我们稍后会解决这个问题。
回到文档上提到的会话录制数据传输为“消息字节”,这引起了我们的好奇,想深入了解这些“消息字节”是如何传输的,以及是否存在滥用反序列化过程的可能。因此,我们开始使用反编译工具,剖析该服务的代码。在此过程中,我们分析的是版本“Citrix_Virtual_Apps_and_Desktops_7_2402_LTSR”的代码。
鉴于代码库庞大,审核代码是个耗时的任务。但最终,我们找到一个名为“SmAudStorageManager.EventMetadataWithTime”的类,引起了我们的注意,代码如下:
/* 1 */ using System;
/* 2 */ using SmAudCommon;
/* 3 */
/* 4 */ namespace SmAudStorageManager
/* 5 */ {
...
/* 28 */ public Guid m_ctxSessionID;
/* 29 */ }
/* 30 */ }
这个类被标记为[Serializable],引发我们对其进行序列化或反序列化的怀疑。根据.NET文档,这个属性表示该类可以使用二进制或XML序列化。看起来,这个可执行文件很可能是在为MSMQ队列序列化数据。
然后,我们跟踪代码库中序列化的使用,反编译更多的库,试图找到所使用的序列化API,并检查序列化是否存在不安全的使用情况。经过多次方法的检查,我们遇到了SmAudStorageManager.ProjectInstaller.Install(IDictionary)方法。
接下来我们关注一下这个方法的实现,下面的代码片段值得仔细查看。 以下是对上述内容的简单表达方式:
这是关于 MSMQ 类的一部分,主要是在处理消息队列实例的权限限制。在第 45 行到第 48 行,通过调用 SetPermissions 方法,设置了这个队列实例的权限。
这是代码的一个部分:
public override void Install(IDictionary stateSaver)
{
try
{
Trace.WriteLine($"开始安装会话录制存储管理器 @ {DateTime.Now} ...");
Trace.WriteLine("确定服务依赖...");
ArrayList arrayList = new ArrayList();
arrayList.Add("Eventlog");
arrayList.Add("MSMQ");
this.AddServiceNameIfExists(arrayList, "COMSysApp");
this.AddServiceNameIfExists(arrayList, "EventSystem");
// 设置服务依赖
this.serviceInstaller.ServicesDependedOn = (string[])arrayList.ToArray(typeof(string));
Trace.WriteLine("服务依赖于:");
foreach (string text in this.serviceInstaller.ServicesDependedOn)
{
Trace.WriteLine($" {text}");
}
// 安装
try
{
Trace.WriteLine("开始基础安装...");
this.UninstallIfExists("CitrixSsRecStorageManager");
base.Install(stateSaver);
Trace.WriteLine("基础安装结束");
}
catch (Exception ex)
{
ExceptionHelper.TraceException(ex);
throw;
}
// 设置服务的 SID
try
{
ServiceSidController.AddServiceSid("CitrixSsRecStorageManager", ServiceSidController.SERVICE_SID_TYPE.SERVICE_SID_TYPE_UNRESTRICTED);
}
catch (Exception ex2)
{
ExceptionHelper.TraceException(ex2);
throw;
}
// 设置 MSMQ 权限
try
{
Trace.WriteLine("开始设置 MSMQ 权限...");
Trace.WriteLine($" 开始打开队列 {this.messageQueueInstaller.Path}...");
MessageQueue messageQueue = new MessageQueue(this.messageQueueInstaller.Path);
Trace.WriteLine("结束打开队列");
// 设置权限
try
{
messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.BuiltinAdministratorsSid), MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.LocalSystemSid), MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.NetworkServiceSid), MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.AnonymousSid), MessageQueueAccessRights.GenericWrite, AccessControlEntryType.Allow);
Trace.WriteLine("结束设置 MSMQ 权限");
}
catch (Exception ex3)
{
throw ex3;
}
finally
{
messageQueue.Close();
}
}
catch (Exception ex4)
{
ExceptionHelper.TraceException(ex4);
}
// 处理证书权限和文件夹权限等操作
// ...
Trace.WriteLine("会话录制存储管理器安装结束");
}
}
通过以上代码,可以看到对队列的权限设置非常宽松,几乎每个人都被允许“完全控制”,而且 AnonymousSid 还可以让任何人都能往队列添加消息,这可能会带来安全隐患。
现在我们对这整个过程的理解变得更加清晰了。
这个方法负责设置对MSMQ组件的初始访问,创建一个队列并设置权限。然而,它所设置的权限并不够严格。进一步分析发现,这个队列后来用于发送和接收类型为EventMetadataWithTime的事件。你还记得我们之前提到过这个类吗?现在我们知道之前发现的[Serializable]正是在这里使用的。
不过,我们还有一些问题没有解答:
- 这个队列在哪里被使用?
- 数据是如何从这个队列接收和处理的?
我们查看了其他方法,发现了OpenQueue()方法,它回答了我们大部分的问题。让我们来看一下这个方法的代码:
public bool OpenQueue()
{
bool flag = false;
try
{
string receiveQueueName = Globals.ReceiveQueueName; // 获取队列名
if (!MessageQueue.Exists(receiveQueueName)) // 检查队列是否存在
{
throw new Exception(string.Format(Strings.Error_QueueDoesNotExist, receiveQueueName));
}
MessageQueue.EnableConnectionCache = true; // 启用连接缓存
this.m_DataQueue = new MessageQueue(receiveQueueName); // 创建消息队列实例
if (!this.m_DataQueue.CanRead) // 检查队列是否可读
{
throw new Exception(string.Format(Strings.Error_QueueReadAccessDenied, receiveQueueName));
}
this.m_DataQueue.Formatter = new BinaryMessageFormatter(FormatterAssemblyStyle.Simple, FormatterTypeStyle.TypesWhenNeeded); // 设置消息格式化器
Trace.WriteLine(string.Format("Open queue: {0}", receiveQueueName));
flag = true;
}
catch (Exception ex)
{
// 日志记录异常
string errorMethod_OpeningQueue = Strings.ErrorMethod_OpeningQueue;
ExceptionHelper.LogException(ex, 2078, errorMethod_OpeningQueue, ExceptionHelper.LogAction.Error);
}
return flag; // 返回是否成功
}
在第4行,获取了一个名为Globals.ReceiveQueueName的全局变量,猜测这是消息队列的名称。在第5行,通过MessageQueue.Exists方法检查这个队列是否可用。在第10行,使用这个队列名实例化了一个MessageQueue类,并将其赋值给m_DataQueue属性。
有趣的是,在第15行,这个队列的m_DataQueue.Formatter属性被设置为BinaryMessageFormatter。根据经验,使用BinaryFormatter进行反序列化几乎总是危险的,它会暴露许多功能给任何能够发送消息的人。
微软自己也说,BinaryFormatter是不安全的,建议应用程序尽快停止使用它,即使他们认为处理的数据是可信的。其实,这是相当强烈的说法!
结合这些信息,我们可以看到之前提到的Serializable类型(EventMetadataWithTime)是通过分配给m_DataQueue的BinaryFormatter进行反序列化的。而且,由于在队列初始化时权限设置不安全,任何人都可以与这个队列进行通信。
在深入Citrix之前,让我们快速回顾一下关于BinaryFormatter的理论。
.NET的BinaryFormatter反序列化基础知识
我们可以讨论关于利用.NET的BinaryFormatter的问题很多,比如有许多“工具”和背景知识,但时间有限,这里只做简单回顾。
如果你错过了上面的微软引用,我们在这里再说一次:BinaryFormatter是不安全的,无法做到安全。
那么,使用BinaryFormatter时可能会发生什么问题呢?
简单说一下:如果能够说服目标对象反序列化我们提供的消息,下一步就是找到一个被称为“gadget”的类。“gadget”是指在反序列化过程中会调用一个或多个方法的类。在特定条件下,这些方法会做一些对攻击者有用的事情。
这是一个简单的示例代码,展示了一种危险类型的gadget:
using System;
using System.IO;
[Serializable]
public class LogFile
{
private string filePath;
public LogFile(string path)
{
filePath = path;
}
~LogFile()
{
if (File.Exists(filePath))
{
File.Delete(filePath);
Console.WriteLine($"[警告] 删除文件: {filePath}");
}
}
}
这个类包含一个接收字符串参数并赋值给属性filePath的构造函数,还包含一个析构函数,在对象被销毁时删除该文件。看起来似乎很无辜,但实际上可能会带来安全隐患。
好的,但这在利用中的具体意义是什么呢?如果攻击者能发送一个这个 LogFile 类的序列化实例,反序列化器会正常地实例化这个类。当垃圾回收器在对象生命周期结束时启动,刚刚反序列化的类的析构函数会被调用,从而删除文件。由于“filePath”字段是由反序列化器设置的,我们可以控制它,这样我们就可以删除任何文件——这就是我们发现的一个“工具”,它允许任意文件删除。
当然,这只是一个例子,实际上用于远程代码执行(RCE)的“工具”通常更加复杂,感兴趣的读者可以看看一些著名的安全研究者(如James Forshaw、Alvaro Muñoz、Oleksandr Mirosh、Soroush Dalili、Piotr Bazydło等)的研究成果。他们开发了可以针对特定.NET框架版本的“通用”工具,达到完全的远程代码执行。
关于这些“工具”的现实参考,可以查看 YSoSerial.NET 项目。
利用 MSMQ 反序列化
那么,这些知识如何帮助我们利用 MSMQ 及其目标呢?通过这些信息,我们知道我们要寻找的是那些在反序列化后暴露危险功能的类。
幸运的是,我们不需要自己找,我们将使用 TypeConfuseDelegate 工具,它适用于.NET框架目标。如果想了解这个由James Forshaw发现的出色工具,可以参考该文章。
不过,我们还有一个额外的问题。MSMQ通常通过TCP端口1801访问,但在Citrix环境中,这个端口默认是关闭的。
所以我们放弃了考虑别的办法——如何在没有访问底层服务的情况下利用我们的反序列化漏洞?经过一番思考,我们想起了 CVE-2023-21554,这是MSMQ本身的一个漏洞。
我们记得该漏洞的利用代码(可以在Metasploit中找到)似乎包含了HTTP负载。这是否意味着有办法从HTTP“跳转”到MSMQ数据呢?是否可以完全通过HTTP入队消息?
Citrix的解决方案默认将HTTP/HTTPS暴露给互联网,所以也许还有希望?
通过快速搜索,我们发现一个有希望的结果。自MSMQ v3(很久以前发布)以来,已经支持MSMQ通过HTTP访问。然而不幸的是,它并没有默认启用。为了安全,产品应该最少暴露功能,对吗?Citrix肯定不会仅仅因为这个功能存在就启用它吧?
然而,结果是,他们实际上启用了这个看似不必要的功能。尽管我们没有看到任何使用这个功能的功能,但在用户安装产品时,它在后台已被激活。也许他们有其他产品使用它,或者有开发者不小心启用了它,然后就这样提交了代码,并忘记了它。
现在,我们有了构建利用所需的所有部分。我们知道有一个权限配置错误的MSMQ实例,并且它使用臭名昭著的 BinaryFormatter 类进行反序列化。而且它不仅可以通过本地的MSMQ TCP端口访问,还可以通过HTTP从任何其他主机访问。这种组合允许我们进行未授权的远程代码执行。由于我们处理的是反序列化问题,这是一种已知相对稳定的漏洞类型,我们可以期待我们的利用(一旦制作完成)能可靠地工作——没有复杂的堆操作或其他不确定因素干扰。
构建利用包
为了构建利用包,我们必须深入研究数据包的结构,这变得相当具有挑战性,因为文档有限。经过大量的尝试和错误,我们测试了不同的配置并调整了各种字段,建立了一个本地测试环境来处理MSMQ消息,同时启用了HTTP支持。我们写的小应用程序让我们可以实验不同的数据类型和数值,逐渐理解每个部分是如何结合在一起的。
数据包结构解释
以下是MSMQ HTTP消息的主要部分的解析:
-
HTTP请求行:这是开始行,表明这是一个针对服务器特定队列端点的
POST请求(/msmq/queue_name)。这告诉服务器我们要向队列发送一条新消息。 -
HTTP头部:标准的HTTP头部提供有关消息的基本信息:
Host指定服务器的地址。Content-Type为multipart/related,表示消息有多个部分,如SOAP信封和序列化数据负载。它还包括一个分隔符值以区分这些部分,并指定该多部分消息的主要类型为text/xml。SOAPAction将操作类型定义为MSMQMessage,告诉服务器正在处理何种类型的消息。
-
第一个分界(SOAP信封):在初始头部后,我们到达第一个分界,开始实际消息内容。SOAP信封是一个包含两个关键部分的XML结构:
Header:包含路由信息(Action和MessageID),告诉服务器这条消息要去哪里,并为其分配一个唯一ID。Properties:像ExpiresAt和SentAt这样的元数据字段,帮助管理消息的生命周期和时机。
-
Body:在正文内部,特定字段定义消息的细节,如
Priority(重要性级别)和BodyType(指定数据是二进制还是文本)。 -
第二个分界(序列化数据负载):下一个部分由另一个分隔符分开,包含消息的实际内容,具有:
Content-Type为application/octet-stream,表示二进制数据。Content-Id将此数据链接回SOAP信封。
序列化数据
这是消息的主要数据载荷,推送到代理队列中,在我们的例子中是一个序列化的 .NET 对象。这些要素共同构建了一个结构化的消息,使得 MSMQ 能够在网络中进行路由、优先级排序和传送。了解这个布局后,我们可以组装 MSMQ 服务器接受的完整数据包。
HTTP 请求示例
+--------------------------------------------------+
| HTTP Request Line |
| POST /msmq/queue_name HTTP/1.1 |
+--------------------------------------------------+
| Host: example.com |
| Content-Type: multipart/related; |
| boundary="MSMQ_SOAP_boundary_12345"; |
| type="text/xml" |
| Content-Length: 2100 |
| SOAPAction: "MSMQMessage" |
| Proxy-Accept: NonInteractiveClient |
+--------------------------------------------------+
这段代码描述了 HTTP 请求的结构,其中包括请求的主机、内容类型、内容长度等信息。
消息内容示例
--MSMQ_SOAP_boundary_12345
+----------------------------------------------------+
| Content-Type: text/xml; charset=UTF-8 |
| Content-Length: 800 |
+----------------------------------------------------+
| SOAP Envelope |
| <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://... |
| <SOAP-ENV:Header> |
| <m:Routing xmlns:m="http://schemas"> |
| <Action>SendMessage</Action> |
| <To>http://example.com/msmq/queue_name</To> |
| <MessageID>uuid:123456789</MessageID> |
| </m:Routing> |
| <m:Properties> |
| <ExpiresAt>20250101T123000</ExpiresAt> |
| <SentAt>20241107T100000</SentAt> |
| </m:Properties> |
| </SOAP-ENV:Header> |
| <SOAP-ENV:Body> |
| <m:MessageBody> |
| <Priority>5</Priority> |
| <BodyType>Binary</BodyType> |
| </m:MessageBody> |
| </SOAP-ENV:Body> |
| </SOAP-ENV:Envelope> |
--MSMQ_SOAP_boundary_12345
这部分显示了 SOAP 消息的头和体,具体包括路由信息、属性和消息主体。
结论
今天我们看到的是什么呢?又一次,我们对互联网失去了一些信心。我们了解到,一个不小心暴露的 MSMQ 实例可以被利用,通过 HTTP 实现对 Citrix 虚拟应用和桌面的未经认证的远程代码执行。我们展示了如何利用现成的工具制造一个稳定且可靠的攻击链,这使得攻击变得更加简单。
这并不算是 BinaryFormatter 本身的缺陷,也不是 MSMQ 的问题,而是 Citrix 依赖于被证实不安全的 BinaryFormatter 来维持安全边界。这是设计阶段的“错”——在选择序列化库时出现了问题。
有趣的是,尽管 MSMQ 端口无法访问,仍然可以进行攻击。事实上,这是一条复杂的路径,大多数(我们希望是“所有”)攻击者可能在这个过程中放弃了,或根本忽视了这一攻击面。
我们已经向 Citrix 报告了这个序列化问题,及其 HTTP 暴露的 MSMQ 队列。现在是否应将其视为两个独立问题还是一个实际的问题还存在争议。虽然 Citrix 使用不受信数据的 BinaryFormatter 可以视为一个缺陷,但尚不清楚暴露 MSMQ 队列是否真是一个疏忽造成的缺陷。
在经过一些沟通后,Citrix 复现了这一问题,并已达成共同协议,公开披露的日期定为 11 月 12 日。Citrix 在沟通上态度友好,并认真对待我们的报告,但目前我们并不知道针对上述弱点的补丁版本号或 CVE 编号。
对此的补救建议是“更新到已修补的版本”。由于 MSMQ 接口是应用核心部分,限制其访问可能会导致环境的潜在故障。我们可以确定的是,该漏洞存在于我们分析的版本中: Citrix_Virtual_Apps_and_Desktops_7_2402_LTSR 。我们会在得到更多关于修复版本的信息时更新这篇文章。