哈克k1
发布于 IP属地北京

Citrix 虚拟应用程序和桌面BinaryFormatter反序列化漏洞

本文转载自watchtowrlabs:https://labs.watchtowr.com/visionaries-at-citrix-have-democratised-remote-network-access-citrix-virtual-apps-and-desktops-cve-unknown/

我们带着另一个刚刚发布的错误链回来了。这一次,它出现在 Citrix 的“虚拟应用程序和桌面”产品中。

这是一个技术堆栈,使最终用户(很可能是勒索软件团伙)能够从任何地方访问他们的完整桌面环境,无论他们使用的是笔记本电脑、平板电脑还是手机。

这本质上就是 30 年前人们非常热衷的“瘦客户端”体验 - 应用程序(或整个桌面)不再将软件和文件存储在每个单独的设备上,而是在数据中心安全隐藏的大型服务器上运行,并通过网络流式传输给最终用户。这有点像远程桌面,但更适合企业使用。

当然,它比远程桌面更精致一些,完全能够被最终用户使用。以下是用户通过其组织的设置运行 Chrome 的示例(由 Citrix 提供)。

总体而言,企业喜欢这种技术。原因如下:

  • 完整的桌面体验:如果操作正确,用户会感觉就像在使用自己的桌面,拥有所有常用的应用程序、文件和设置。通常,用户甚至无法分辨出他们和应用程序之间有整个网络,但实际上,它们都托管在其他地方。这意味着无论用户使用什么设备,都可以从上次中断的地方继续操作。
  • 轻松管理:虚拟桌面是 IT 团队管理的梦想(同样,前提是“正确完成”。管理员可以从一个中心位置而不是每个员工的设备上更新、保护和排除故障。想象一下,在服务器机房外,您唯一需要排除故障的就是键盘、鼠标和显示器。这就是承诺。
  • 增强安全性:由于数据存储在中央位置,因此更容易保证安全,至少理论上是这样的。如果有人丢失了设备,设备上不会有任何数据;所有数据都安全地存储在云端。您无需撤销任何操作或远程擦除任何设备。
  • 远程工作就绪:用户可以从家里、办公室或任何可以访问网络的地方登录他们的桌面,这对于灵活的工作安排非常理想,并能保持一切的一致性。

我们认为虚拟桌面可以为用户提供无缝的桌面体验,无论他们身在何处,并且公司可以获得更轻松的管理并获得一些安全优势。

当然,攻击者也喜欢这项技术,但原因却截然不同。

以交互方式发布企业应用程序并让其为用户做好准备也意味着让它们为攻击者做好准备,只需一个身份验证步骤即可 - 我们当然看到攻击者已经取得了一些巨大的成功,CVE-2024-6151就是最近的例子。

这是一个特权漏洞,它会让任何 VDI 用户获得 SYSTEM 权限,实际上,这比听起来要糟糕得多,因为这是托管所有应用程序的服务器上的 SYSTEM 权限,并且访问是“设计使然”的 - 允许攻击者冒充任何​​用户(包括管理员)并监控行为和连接性。

由于一切都如此无缝和便携,因此很容易从那里跳到模仿用户或“跟踪”他们,观察他们的每一个动作。集中管理系统很容易变成一个全景监狱。

然而,我们还没有看到很多真正的未经身份验证的 RCE 漏洞报告。

我们提出了 6 个可能的原因:

  1. 这可能是由于瘦客户端解决方案的架构所致,其中权限提升攻击非常强大。
  2. 也许这些解决方案的构建方式反映了企业远程桌面访问工具所需的安全态势?
  3. 也许这些解决方案的构建方式反映了企业远程桌面访问工具所需的安全态势?
  4. 也许这些解决方案的构建方式反映了企业远程桌面访问工具所需的安全态势?
  5. 也许这些解决方案的构建方式反映了企业远程桌面访问工具所需的安全态势?
  6. 也许这些解决方案的构建方式反映了企业远程桌面访问工具所需的安全态势?

无论如何,Citrix 的虚拟应用程序和桌面解决方案是一个庞大而复杂的系统,具有许多活动部件(我们将放大其中一个并很快将其拆开)。

大家都知道,事情越复杂,出现漏洞的几率就越大。尽管供应商投入了大量精力来确保其安全,但肯定存在一些RCE漏洞,不是吗?

鉴于历史上缺乏弱点,表明明显缺乏毁灭世界的破坏,我们决定仔细研究该产品。

Citrix 是否实现了远程网络访问的民主化?是否从授权者中移除了此特权?

Citrix 会话记录——又称“万恶之源”

“瘦客户端”式解决方案非常适合的一件事是监控 - 也称为“跟踪”,“审计功能”或只是“管理员查看登录用户正在做什么的能力”。

在 VDI 解决方案中,发送到客户端的会话数据实际上是视频流。因此,授权管理员可以“轻松”地发送视频并自行观看。

Citrix 在其“会话记录”功能上更进一步。此功能可捕获用户活动,记录键盘和鼠标输入,以及桌面反应的视频流。这类似于虚拟机会话的记录(或者,您的 APT 朋友使用“屏幕截图”命令向他们的 Cobalt Strike 信标发送垃圾邮件)。

Citrix 宣传该功能不仅对监控非常有用(这一点很明显),而且对合规性和故障排除也非常有用。甚至可以设置某些操作(如识别敏感数据)将触发记录,这有助于满足监管需求并标记可疑活动。

它对于故障排除也非常有用,因为最终用户可以“记录”问题的出现,向技术团队准确展示发生的情况(而不仅仅是开具一张“它不起作用”的主题的票据)。

以下是记录会话时用户所看到内容的示例:

总体而言,它提供了用户活动的安全记录,有助于审计、检测异常行为和诊断问题。现在,从用户的角度来看,会话记录流程如下:

                     User Login
                         |
                         v
                +-------------------+
                | Citrix Login      |
                +-------------------+
                         |
                         v
              +------------------------+
              | Virtual Apps and       |
              | Desktops Environment   |
              +------------------------+
                         |
                         v
              +------------------------+
              | Start Session          |
              +------------------------+
                         |
                         v
          +------------------------------------+
          | Session Recording Service          |
          | - Monitors the session             |
          | - Starts recording if conditions   |
          |   are met (e.g., policy triggered) |
          +------------------------------------+
                         |
                         v
          +------------------------------------+
          | Record User Actions                |
          | - Keystrokes, application access,  |
          |   and screen activity              |
          +------------------------------------+
                         |
                         v
            +------------------------------------+
            | Store Session Recording            |
            | - Saved in a secure repository     |
            | - Available for review by admins   |
            +------------------------------------+

我们的主要动机之一是发现此功能背后的架构 — Citrix 工程师如何记录用户会话、安全地处理数据并在环境中传输数据。这不仅仅是“启动屏幕录像机”的问题。

我想到了一些问题:

  • 哪个进程捕获了会话?
  • 录音是如何存储的?
  • 是否涉及多个组件,还是由单一流程监督整个操作?

这些问题的答案描绘出了一个关键的攻击面,这正是从安全角度来看该功能如此有趣的原因。

让我们明确一点:像 Citrix 那样可靠且大规模地记录用户会话极具挑战性。

这不像在笔记本电脑上打开 QuickTime 并按下“录制”按钮;这是一个 Web 应用程序,可以在安全的企业级产品中同时流式传输和录制多个远程桌面会话。

纯粹的技术需求,加上协调处理实时流、存储和安全数据传输的流程的复杂性,意味着有无数错综复杂的活动部件。众所周知,复杂性往往会滋生错误的机会——这些错误有时会导致严重的漏洞。

我们希望揭开这一复杂功能的层层面纱,让大家意识到 Citrix 工程师所面对的复杂性以及这些功能为何值得严格审查。了解这些细节不仅凸显了 Citrix 技术的惊人范围,还揭示了强大而复杂的系统中可能存在的安全漏洞。

最终,在审查流程和检查文档后,我们得出了下图。


可以看到,有一个“Session Recording Server”,它可以位于安装 Citrix Virtual Apps and Desktops 的同一台机器上,也可以位于单独的机器上。

当 Citrix 主要组件记录会话时,它会将其传递给此“会话记录服务器”,然后将记录连同您期望的元数据(提交记录的用户、日期等)一起存储在数据库中。

稍后,授权管理员可以使用播放器组件检查会话录像,该组件查询连接到 Session Recording Server 的数据库并检索相关信息。


最后,“记录策略控制台”是向管理员提供更细粒度控制的组件,允许设置会话开始记录的具体触发器。例如,每次用户访问特别高价值的文件共享时,您都可以启动记录会话。


好了,现在我们了解了这些不同组件的作用,但还有一个问题:这些组件如何相互通信?是通过网络套接字进行通信吗?也许是命名管道,或者某种共享内存?

要回答这些问题,就得在文件系统中挖掘并找到相关的可执行文件。幸运的是,这很简单,因为 Citrix 很有帮助地保持了有序的文件夹结构。我们很快就找到了一个名为“SessionRecording”的目录 - 除了与会话记录相关的组件外,它还能包含什么呢?


完美。我们这里有一堆文件夹,用于存放会话记录的各个组件 - 例如数据库组件、播放器,以及(最重要的!)“服务器”组件,所有有用的逻辑可能都藏在这里等着我们。

我们开始仔细研究所涉及的一些组件,寻找与其他组件通信的任何迹象。在此过程中,我们偶然发现了一个名为“SsRecStorageManager.exe”的可执行文件 - 有趣的是,它默认作为 Windows 服务运行。


有了这样的文件名,这肯定是一个处理会话记录存储的组件。而摄像机的图标实在太诱人了,让人无法忽视——哪个黑客不想像摄像机一样监视会话呢?!

当然,作为聪明的黑客,我们首先要做的就是阅读此组件的文档(而不是直接使用反编译器)。我们进行了快速搜索,找到了以下文档:

Citrix 会话记录存储管理器是一项 Windows 服务,用于管理从运行 XenApp 和 XenDesktop 的每台启用了会话记录的计算机接收的会话记录文件。存储管理器通过 Microsoft 消息队列 (MSMQ) 服务以消息字节的形式接收会话记录。为了始终保持记录的完整性,存储管理器应能够以会话记录代理发送消息的速度管理接收到的消息。

好的,现在我们对这项服务的功能有了大致的了解。它获取录制的会话文件,并通过 MSMQ 接收它们。此组件 MSMQ 或“Microsoft 消息队列”仅允许两个单独的进程通过“队列”进行通信 - 例如,一方可能会将一条消息排入队列,内容是“列出数据库中的所有记录”,然后另一个应用程序可能会从队列中获取此消息,并通过将记录数据列表放回队列来做出响应。

然而,这个过程隐含着一个重要的细节。

因为队列处理在进程之间(甚至在整台机器之间)传输的数据,所以需要对队列中的对象进行某种转换。我们不能简单地将内存块转储到那里,因为接收端可能无法理解它们 - 我们需要某种序列化过程来将数据转换为另一端可以解释的形式。(对于那些 .NET 爱好者来说,.NET 通常将此称为“编组”)。

当然,复杂性是隐藏错误的好地方,从历史上看,序列化接口已被证明是攻击者秘密插入自己的数据的好方法,然后信任应用程序将反序列化并处理这些数据,就好像它来自受信任方一样。这里有一个小细节,MSMQ 组件实际上并没有通过 TCP 暴露给网络,但不要担心 - 我们稍后会处理这部分。

回顾文档,文档中指出会话记录数据只是作为“消息字节”传输的。阅读此内容激发了我们的“蜘蛛感应”,让我们想要进一步了解这些“消息字节”是如何传输的 - 它们是如何序列化的,我们是否能够滥用反序列化过程?因此,我们启动了信任反编译器并开始剖析该服务。对于那些跟随的人,我们对版本进行了此分析Citrix_Virtual_Apps_and_Desktops_7_2402_LTSR

审查如此规模的代码库始终是一项耗时的任务,而且我们需要耐心,因为我们会详尽地审核代码。不过,最终,我们遇到了一个名为的类SmAudStorageManager.EventMetadataWithTime,它引起了我们的注意。请看一看:

/*  1 */  using System;
/*  2 */  using SmAudCommon;
/*  3 */  
/*  4 */  namespace SmAudStorageManager
/*  5 */  {
/*  6 */  	// Token: 0x0200000A RID: 10
/*  7 */  	[Serializable]
/*  8 */  	internal class EventMetadataWithTime
/*  9 */  	{
/* 10 */  		// Token: 0x04000048 RID: 72
/* 11 */  		public EventMetadata m_eventMetadata;
/* 12 */  
/* 13 */  		// Token: 0x04000049 RID: 73
/* 14 */  		public DateTime m_eventTime;
/* 15 */  
/* 16 */  		// Token: 0x0400004A RID: 74
/* 17 */  		public DateTime m_eventUtcTime;
/* 18 */  
/* 19 */  		// Token: 0x0400004B RID: 75
/* 20 */  		public string m_user;
/* 21 */  
/* 22 */  		// Token: 0x0400004C RID: 76
/* 23 */  		public string m_domain;
/* 24 */  
/* 25 */  		// Token: 0x0400004D RID: 77
/* 26 */  		public string m_server;
/* 27 */  
/* 28 */  		// Token: 0x0400004E RID: 78
/* 29 */  		public Guid m_ctxSessionID;
/* 30 */  	}
/* 31 */  }

我们立即注意到,该类已被标记为该[Serializable]属性,这让我们怀疑该可执行文件可能正在执行某种反序列化/序列化。

事实上,.NET文档只是简单地指出此属性“表示可以使用二进制或 XML 序列化来序列化类”。这个可执行文件很可能正在序列化可用于 MSMQ 队列的数据。

从这里开始,我们跟踪了整个代码库中序列化使用的足迹,反编译了更多的库来查找正在使用的序列化 API,最终目的是检查序列化是否以不安全的方式使用。

在研究了无数不同的方法之后,我们找到了这个SmAudStorageManager.ProjectInstaller.Install(IDictionary)方法。

让我们仔细检查它的实现——在跟随的过程中请注意下面的代码片段。

您可以注意到,行 (41) 实例化了MessageQueueMSMQ 类的一部分System.Messaging.MessageQueue.MessageQueue

然后从第(45)行到第(48)行,通过调用方法对此队列实例进行权限限制SetPermissions

  public override void Install(IDictionary stateSaver)
{
/*  1 */	try
/*  2 */	{
/*  3 */		Trace.WriteLine(string.Format("\\nBegin Session Recording Storage Manager install @ {0} ...", DateTime.Now));
/*  4 */		Trace.WriteLine("Determining service dependencies...");
/*  5 */		ArrayList arrayList = new ArrayList();
/*  6 */		arrayList.Add("Eventlog");
/*  7 */		arrayList.Add("MSMQ");
/*  8 */		this.AddServiceNameIfExists(arrayList, "COMSysApp");
/*  9 */		this.AddServiceNameIfExists(arrayList, "EventSystem");
/* 10 */		this.serviceInstaller.ServicesDependedOn = (string[])arrayList.ToArray(typeof(string));
/* 11 */		Trace.WriteLine("  Service depends on:");
/* 12 */		foreach (string text in this.serviceInstaller.ServicesDependedOn)
/* 13 */		{
/* 14 */			Trace.WriteLine(string.Format("     {0}", text));
/* 15 */		}
/* 16 */		try
/* 17 */		{
/* 18 */			Trace.WriteLine("CitrixSsRecStorageManager: base.Install begin");
/* 19 */			this.UninstallIfExists("CitrixSsRecStorageManager");
/* 20 */			base.Install(stateSaver);
/* 21 */			Trace.WriteLine("CitrixSsRecStorageManager: base.Install end");
/* 22 */		}
/* 23 */		catch (Exception ex)
/* 24 */		{
/* 25 */			ExceptionHelper.TraceException(ex);
/* 26 */			throw;
/* 27 */		}
/* 28 */		try
/* 29 */		{
/* 30 */			ServiceSidController.AddServiceSid("CitrixSsRecStorageManager", ServiceSidController.SERVICE_SID_TYPE.SERVICE_SID_TYPE_UNRESTRICTED);
/* 31 */		}
/* 32 */		catch (Exception ex2)
/* 33 */		{
/* 34 */			ExceptionHelper.TraceException(ex2);
/* 35 */			throw;
/* 36 */		}
/* 37 */		try
/* 38 */		{
/* 39 */			Trace.WriteLine("Begin set MSMQ permissions...");
/* 40 */			Trace.WriteLine(string.Format("  Begin open queue {0}...", this.messageQueueInstaller.Path));
/* 41 */			MessageQueue messageQueue = new MessageQueue(this.messageQueueInstaller.Path);
/* 42 */			Trace.WriteLine("  End open queue");
/* 43 */			try
/* 44 */			{
/* 45 */				messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.BuiltinAdministratorsSid), MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
/* 46 */				messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.LocalSystemSid), MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
/* 47 */				messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.NetworkServiceSid), MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
/* 48 */				messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.AnonymousSid), MessageQueueAccessRights.GenericWrite, AccessControlEntryType.Allow);
/* 49 */				Trace.WriteLine("End set MSMQ permissions");
/* 50 */			}
/* 51 */			catch (Exception ex3)
/* 52 */			{
/* 53 */				throw ex3;
/* 54 */			}
/* 55 */			finally
/* 56 */			{
/* 57 */				messageQueue.Close();
/* 58 */			}
/* 59 */		}
/* 60 */		catch (Exception ex4)
/* 61 */		{
/* 62 */			ExceptionHelper.TraceException(ex4);
/* 63 */		}
/* 64 */		try
/* 65 */		{
/* 66 */			string signingCertificateThumbprint = RegistryConfiguration.Server.SigningCertificateThumbprint;
/* 67 */			if (signingCertificateThumbprint != null && signingCertificateThumbprint != string.Empty)
/* 68 */			{
/* 69 */				CertSecurityHelper.AddCertAccessRuleThroughThumbprint(signingCertificateThumbprint, CertSecurityHelper.StorageManagerServiceUser);
/* 70 */				Trace.WriteLine(string.Format("Install AddCertAccessRule Finished.", Array.Empty<object>()));
/* 71 */			}
/* 72 */		}
/* 73 */		catch (Exception ex5)
/* 74 */		{
/* 75 */			this.SendToMsiLog(string.Format("Install AddCertAccessRule Exception: {0}.", ex5.Message));
/* 76 */		}
/* 77 */		try
/* 78 */		{
/* 79 */			DirFileSecurityHelper.UpdateRecordingDirAndFilePermission();
/* 80 */		}
/* 81 */		catch (Exception ex6)
/* 82 */		{
/* 83 */			this.SendToMsiLog(string.Format("Install UpdateRecordingDirAndFilePermission Exception: {0}.", ex6.Message));
/* 84 */		}
/* 85 */		try
/* 86 */		{
/* 87 */			ProjectInstaller.CheckDatabaseConnection();
/* 88 */			DirFileSecurityHelper.UpdateLiveRecordingFilePermission(DatabaseProxy.DatabaseConnection);
/* 89 */		}
/* 90 */		catch (Exception ex7)
/* 91 */		{
/* 92 */			this.SendToMsiLog(string.Format("Install UpdateLiveRecordingFilePermission Exception: {0}.", ex7.Message));
/* 93 */		}
/* 94 */		Trace.WriteLine("End Session Recording Storage Manager Install\\n");
/* 95 */		try
/* 96 */		{
/* 97 */			string text2 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log");
/* 98 */			if (!Directory.Exists(text2))
/* 99 */			{
/* 100 */				DirectorySecurity directorySecurity = new DirectorySecurity();
/* 101 */				directorySecurity.AddAccessRule(new FileSystemAccessRule("NETWORK SERVICE", FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
/* 102 */				directorySecurity.AddAccessRule(new FileSystemAccessRule("Administrators", FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
/* 103 */				Directory.CreateDirectory(text2, directorySecurity);
/* 104 */			}

[..SNIP..]

我们很快就能注意到这些权限有多么可怕,几乎每个人都拥有宝贵的“完全控制”访问权限,并且GenericWrite在AnonymousSid第 (48) 行,任何人都可以将消息放入队列,然后对其进行处理和采取行动。

  messageQueue.SetPermissions(ProjectInstaller.GetLocalizedTrusteeName(WellKnownSidType.AnonymousSid), MessageQueueAccessRights.GenericWrite, AccessControlEntryType.Allow);

现在,我们对这一切如何运作的认识变得越来越清晰了。

此方法负责设置对 MSMQ 组件的初始访问、创建队列并设置其权限。但是,它设置的权限不够严格。进一步分析表明,此队列稍后用于发送和接收 类型的事件EventMetadataWithTime

你还记得我们之前讨论过这个类吗?现在我们知道[Serializable]之前找到的在哪里使用了 - 就在这里。

然而,我们仍然有一些问题尚未解答:

  • 这个队列在哪里使用?
  • 从这个队列接收到的数据是如何处理?

我们研究了其余的方法,并找到了OpenQueue()可以回答我们大部分问题的方法。让我们来看看:

  /*    */  public bool OpenQueue()
/*    */  {
/*  1 */    	bool flag = false;
/*  2 */    	try
/*  3 */    	{
/*  4 */          string receiveQueueName = Globals.ReceiveQueueName;
/*  5 */          if (!MessageQueue.Exists(receiveQueueName))
/*  6 */    		{
/*  7 */    			throw new Exception(string.Format(Strings.Error_QueueDoesNotExist, receiveQueueName));
/*  8 */    		}
/*  9 */    		MessageQueue.EnableConnectionCache = true;
/* 10 */    		this.m_DataQueue = new MessageQueue(receiveQueueName);
/* 11 */    		if (!this.m_DataQueue.CanRead)
/* 12 */    		{
/* 13 */    			throw new Exception(string.Format(Strings.Error_QueueReadAccessDenied, receiveQueueName));
/* 14 */    		}
/* 15 */    		this.m_DataQueue.Formatter = new \
/*    */            BinaryMessageFormatter(FormatterAssemblyStyle.Simple, \
/*    */            FormatterTypeStyle.TypesWhenNeeded);
/* 16 */    		Trace.WriteLine(string.Format("Open queue: {0}", receiveQueueName));
/* 17 */    		flag = true;
/* 18 */    	}
/* 19 */    	catch (Exception ex)
/* 20 */    	{
/* 21 */    		string errorMethod_OpeningQueue = Strings.ErrorMethod_OpeningQueue;
/* 22 */    		ExceptionHelper.LogException(ex, 2078, errorMethod_OpeningQueue, ExceptionHelper.LogAction.Error);
/* 23 */    	}
/* 24 */    	return flag;
/* 25 */  }

在行 (4) 中,Globals.ReceiveQueueName检索了一个名为的全局变量。人们可以很快猜出这是消息队列名称。然后,在行 (5) 中,通过该MessageQueue.Exists方法检查此队列名称的可访问性。此后,在行 (10) 中,使用此队列名称实例化类的实例MessageQueue,然后将其分配给m_DataQueue属性。

不过,这里有趣的是,m_DataQueue.Formatter这个队列的属性在第 (15) 行被设置为BinaryMessageFormatter

时间告诉我们,使用 BinaryFormatter 进行反序列化几乎总是危险的 - 它向任何可以向其提供消息的人公开了很多功能,虽然它可以安全地使用,但它提供了足够多的“脚枪”,甚至微软自己也说不应该使用它:

BinaryFormatter类型 很危险, 不 建议用于数据处理。应用程序应尽快停止使用 BinaryFormatterBinaryFormatter,即使它们认为所处理的数据值得信赖。 不安全,无法保证安全。
这是来自该图书馆原创者的相当强烈的言语!

将此处的各个部分连接起来,我们可以看到,Serializable我们之前看到的类型 ( EventMetadataWithTime) 正在使用分配给 MSMQ 实例的 BinaryFormatter 进行反序列化m_DataQueue。我们不要忘记这样一个事实,即由于在队列初始化例程中设置的不安全权限,任何人都可以与此队列通信。

在深入研究 Citrix 之前,让我们先快速回顾一下并回顾一些 BinaryFormatter 理论。

.NET BinaryFormatter 反序列化 101

我们可以花几个小时讨论如何利用 .NET 的 BinaryFormatter。有这么多小工具和如此多的背景知识,概述和分享总是很有趣。但不幸的是,时间总是很短,所以我们将坚持快速复习一下。

如果你错过了上面微软的引言,我们在这里重申一下:

BinaryFormatter 不安全且无法保证安全

在我们继续前进时请记住这一点(为 BinaryFormatter 查找你自己的代码库并利用结果留给读者作为练习)。

那么,使用 BinaryFormatter 时会出现什么问题?

此时需要快速说明一下 - 请记住,以下只是一个示例,旨在帮助读者快速了解简单的“小工具”的外观,并且这不是我们用于检测工件生成器利用的小工具。

既然能够诱使目标反序列化我们发送的消息,下一步就是找到所谓的“小工具”。 “小工具”是一个类,其中包含一个或多个在反序列化过程中调用的方法。 在受控的情况下,这些方法会执行对攻击者有用的操作。

演示起来比解释起来容易,因此请看一下下面的代码,它属于危险类型:

   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($"[Warning] Deleted file: {filePath}");
      }
    }
  }
} 

此类包含一个构造函数,该构造函数接受一个字符串并将其分配给名为“filePath”的属性。它还包含一个析构函数,该析构函数在对象被销毁时删除该文件。够无辜了吧?

好的,但是这在漏洞利用的背景下到底意味着什么呢?如果攻击者可以提供此类的序列化实例LogFile,那么反序列化器就会像人们所期望的那样及时实例化该类。一旦垃圾收集器在对象生命周期结束时启动,就会调用新反序列化类的析构函数,并删除该文件。由于“filePath”字段是由反序列化器设置的,因此它在我们的控制之下,我们可以删除任何我们想要的文件 - 哇,我们刚刚发现了一个允许任意文件删除的“小工具”。

当然,这只是一个例子,而且有些牵强。能够实现 RCE 的“真实”小工具通常更为复杂,有兴趣的读者可以探索伟大的安全研究人员的工作(例如 James Forshaw、Alvaro Muñoz、Oleksandr Mirosh、Soroush Dalili、Piotr Bazydło 等)。这些人开发了可以针对特定 .NET 框架版本实现完整远程代码执行的“通用”小工具。

您可以通过查看YSoSerial.NET项目来参考上面的真实世界小工具参考。

利用 MSMQ 反序列化

好的,这些背景知识很有趣,但是它如何帮助我们利用 MSMQ 以及我们的目标本身呢?有了这些知识,我们就知道要寻找什么了——某种在反序列化后会暴露危险功能的类。幸运的是,我们不需要自己去寻找它——我们将使用TypeConfuseDelegate针对 .NET 框架目标的小工具。
然而,正如我们之前提到的,这里还有一个额外的问题。MSMQ 通常通过 TCP 端口 1801 访问,但该端口在 Citrix 环境中默认不打开。此时,我们放弃了,在无法访问底层服务的情况下,我们如何利用我们巧妙的反序列化漏洞?在跳到一些直接命令注入漏洞并进行深刻反省之后,我们想起了CVE-2023-21554,这是 MSMQ 本身的一个错误。
由于我们是那种为了好玩而阅读漏洞代码的人,我们记得漏洞代码(在 Metasploit 中很容易找到)似乎包含 HTTP 负载。

  • 这是否意味着有一种方法可以从 HTTP '跳转'到 MSMQ 数据?
  • 是否可能完全通过 HTTP 将消息放入队列?
  • Citrix 的解决方案默认将 HTTP/HTTPS 公开到互联网,所以也许有一些亮点?

https://github.com/rapid7/metasploit-framework/blob/96f6f66429cf82729c13c5f615ef6046fe3dbc4b/modules/auxiliary/scanner/msmq/cve_2023_21554_queuejumper.rb#L292

快速搜索得到了一个有希望的结果:

似乎自 MSMQ v3(很久以前发布!)以来,就一直支持 MSMQ over HTTP。但不幸的是,默认情况下它并未启用。

大家都知道,对于安全的产品来说,应该公开最低限度的功能,对吧?Citrix 肯定不会仅仅因为有这个额外的功能就启用它吧?

我的意思是,他们为什么会这么做?

他们的代码使用 TCP 上的 MSMQ,他们肯定不会启用这个未使用的功能而只是随意暴露攻击面..?

通过 HTTP 利用 MSMQ!

事实证明,他们确实启用了这个看似不必要的功能。

尽管他们没有在我们所看到的任何功能中使用此功能,但当用户安装产品时,它仍然会在后台激活。

也许他们有进一步使用它的产品,该产品必须通过 HTTP 进行交互。

也许有些开发人员意外地启用了它,提交了代码,然后就忘记了它。

我们将把根本原因分析留给 Citrix 自己。


现在,我们已经拥有了构建漏洞利用所需的所有部分。

我们知道有一个权限配置错误的 MSMQ 实例,并且我们知道它使用臭名昭著的 BinaryFormatter 类来执行反序列化。

最棒的是,它不仅可以通过 MSMQ TCP 端口在本地访问,还可以通过 HTTP 从任何其他主机访问。

此组合允许进行老式的未经身份验证的 RCE。由于我们正在处理反序列化问题,这是一个相对稳定的错误类,因此我们可以高度确信我们的漏洞(一旦制作完成)将可靠地工作 - 没有棘手的堆操作或其他熵潜入。

Bob the Packet 建造者

为了构建漏洞包,我们必须深入研究包的结构,但由于文档有限,这变得非常具有挑战性。

通过大量的反复试验,我们测试了不同的配置,并通过设置一个可以处理 MSMQ 消息并启用 HTTP 支持的本地测试环境来调整各个字段,我们编写的小应用程序允许我们尝试不同的数据类型和值,直到我们逐渐开始了解每个部分是如何组合在一起的。

数据包结构说明

以下是此 MSMQ HTTP 消息主要部分的细分:

HTTP 请求行:这是起始行,表示这是POST针对服务器上特定队列端点的请求(/msmq/queue_name)。这告诉服务器我们正在向队列发送一条新消息。
HTTP 标头:标准 HTTP 标头提供有关消息的基本信息:
主机指定服务器的地址。
Content-Type为multipart/related,表示该消息包含多个部分,如 SOAP 信封和序列化数据负载。它还包含一个boundary值来分隔这些部分,并指定此多部分消息中的主要类型为text/xml。
SOAPAction将动作类型定义为"MSMQMessage",告知服务器它正在处理的消息类型。
第一个边界(SOAP 信封):在初始标头之后,我们到达第一个边界,它开始实际的消息内容。 SOAP 信封是一个 XML 结构,包含两个关键部分:
标头:包含路由信息(To、Action、MessageID),告知服务器此消息要发往何处,并为其分配唯一的 ID。
属性ExpiresAt:像和这样的元数据字段SentAt,有助于管理消息的生命周期和时间。
正文:正文内部的特定字段定义消息的详细信息,例如Priority(重要性级别)和BodyType(指定数据是二进制还是文本)。
第二边界(序列化数据有效负载):由另一个边界分隔的下一部分保存消息的实际内容。它具有:
Content-Type为application/octet-stream,表示二进制数据。
Content-Id将此数据链接回 SOAP 信封。
序列化数据:这是推送到代理队列的消息的主要数据负载,在我们的例子中是序列化的 .NET 对象
这些元素共同构成了结构化消息,MSMQ 可以在网络上路由、确定优先级并进行传递。通过了解此布局,我们可以组装 MSMQ 服务器可以接受的完整数据包。

  
+--------------------------------------------------+
|              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               |
+--------------------------------------------------+

--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

--MSMQ_SOAP_boundary_12345
+--------------------------------------------------+
| Content-Type: application/octet-stream           |
| Content-Length: 1300                             |
| Content-Id: uuid:123456789                       |
+--------------------------------------------------+
|               MSMQ Message                       |
|         [Binary data or serialized object]       |
+--------------------------------------------------+
--MSMQ_SOAP_boundary_12345--

结论

那么,我们今天看到了什么?

我们已经了解了如何通过 HTTP 利用不小心暴露的 MSMQ 实例来针对 Citrix Virtual Apps and Desktops 启用未经身份验证的 RCE。我们已向读者介绍了如何使用现成的小工具来制作漏洞利用程序,从而形成一个稳定可靠的漏洞利用“链”,使其易于利用。

这实际上不是 BinaryFormatter 本身的 bug,也不是 MSMQ 的 bug,而是 Citrix 依赖文档中不安全的 BinaryFormatter 来维护安全边界而导致的不幸后果。这是在设计阶段 Citrix 决定使用哪个序列化库时出现的“bug”。

这个特殊案例的有趣之处在于,即使无法访问 MSMQ 端口,也有可能被利用。事实上,这是一个兔子洞,大多数(我们希望“所有”)攻击者此时都会放弃,甚至完全忽略攻击面。

编者注:公平地说,大多数攻击者可能忙于向防火墙和管理设备之间的供应商特定管理接口发送 shell 命令。这只是一个大胆的猜测。

浏览 (209)
点赞
收藏
打赏
评论