CVE-2024-40711&CVE-2025-23120 Veeam Backup .NET 二次反序列化触发及绕过分析
CVE-2024-40711&CVE-2025-23120 Veeam Backup .NET 二次反序列化触发及绕过分析
原创 KCyber 自在安全 2025-03-30 08:00
漏洞概述
Veeam Backup & Replication 通报的
CVE-2025-23120 是
CVE-2024-40711
的延续,两者均为 .NET 反序列化漏洞,都包括两次反序列化过程,分别是白名单绕过的 Remoting 反序列化以及黑名单绕过的
BinaryFormatter 反序列化,漏洞触发过程比较复杂。
Remoting服务分析
Veeam 定义了很多 .NET Remoting 服务,比如:
6170/tcp open remoting MS .NET Remoting services
9392/tcp open remoting MS .NET Remoting services
9396/tcp open remoting MS .NET Remoting services
9501/tcp open remoting MS .NET Remoting services
9509/tcp open remoting MS .NET Remoting services
10003/tcp open remoting MS .NET Remoting services
梳理端口对应的进程服务如下:
6170 -> Veeam.Backup.MountService.exe
9392 -> Veeam.Backup.Service.exe
9396 -> Veeam.Backup.UIServer.exe
9501 -> Veeam.Backup.BrokerService.exe
9509 -> Veeam.Backup.Cdp.Service.exe
10003 -> Veeam.Backup.CloudService.exe
...
.NET Remoting 与 Java RMI 服务类似,是微软 .NET Framework 中 RPC 的一种具体实现方案。
.NET Remoting 可以使用 http/tcp/ipc 三种协议进行传输。默认状态下(除非在实例化时指定 _sinkProvider 属性),服务器将通过调用 CreateDefaultServerProviderChain 函数来定义 IServerChannelSinkProvider 对象。
对于 .NET Remoting 反序列化,如果有时间我将单独做一次分享,这里不过多赘述了。
以 Veeam.Backup.MountService.exe 为例,启动的服务端口为 6170 ,分析 TcpServerChannel 关键字,找到了其中一个实现类 CSrvTcpChannelRegistration :
CSrvTcpChannelRegistration 构造函数中完成了 TcpServerChannel 注册,并通过 CSrvTcpChannelRegistration.GetSinkProvider 函数实例化了 IServerChannelSinkProvider 类型:
.NET Remoting 中的 MountService 对应服务端口正好为 6170 (cmountServiceRegistryOptions.Port):
Veeam.Backup.MountService.Program.StartProcess 函数中实例化了 CMountServiceServer 对象,然后通过 StartLeasing 函数注册该 .NET Remoting 服务,终结点 URI 取值为 PermanentSessionService :
CVE-2024-40711
1.
CBinaryServerFormatterSinkProvider 反序列化
回到 CSrvTcpChannelRegistration 构造函数,Veeam 自定义了 3 种 IServerChannelSinkProvider ,分别是:
CBinaryServerFormatterSinkProvider
CActivityMonitorServerSinkProvider
CImpersonationServerSinkProvider
CSrvTcpChannelRegistration.GetSinkProvider 根据输入参数的不同,实例化不同类型的 IServerChannelSinkProvider 对象:
重点关注其中的 CBinaryServerFormatterSinkProvider ,对应的 Sink 为 CBinaryServerFormatterSink 类型,反序列化操作通过 ProcessMessage 来完成,该函数与 .NET Framework 自带的 BinaryServerFormatterSink 类似,反序列化函数为 DeserializeBinaryRequestMessage :
函数中不仅限定了 TypeFilterLevel 为 Low ,还针对 BinaryFormatter 反序列化类型调用 RestrictedSerializationBinder 进行黑白名单检查,这里 mode 取值为 FilterByWhitelist ,对应白名单检查:
2.
二次反序列化实现黑白名单Bypass
白名单定义在 CWhitelist 类中,除了加入常见的类型外,其他的白名单定义在 whitelist.txt 文件中:
既然启用了白名单,那么只能尝试在合法的类型中寻找可利用对象。同时 Veeam 中还定义了黑名单(定义在 CBlacklist 类中,黑名单位于 blacklist.txt),只不过在 CBinaryServerFormatterSinkProvider 中没有用到黑名单:
大部分已经曝光的 gadget 被加入了黑名单,但是也有一些比较新的链被遗漏了,比如 ObjRef 。那么哪些地方用到了黑名单呢?通过搜索找到了 CProxyBinaryFormatter 类,该类中的 Deserialize 函数在反序列化时采用的是黑名单方式:
很容易想到可以尝试通过二次反序列化来进行绕过。重点关注位于白名单且调用了 CProxyBinaryFormatter.Deserialize 函数的对象,因为这样的对象既可以绕过 CBinaryServerFormatterSinkProvider 白名单限制,又可以结合 ObjRef 绕过后面的黑名单。分析后发现下面 3 个类满足要求:
CEpContainerSaveInfo
CDbCryptoKeyInfo
CUserRequestSpecificationConfigurationBackup
关注其中的 CDbCryptoKeyInfo 类,该类继承了 ISerializable 接口, BinaryFormatter 反序列化时会自动调用构造函数,在序列化时将调用 GetObjectData 函数:
这里通过 .NET 泛型类型定义的方式将反序列化对象限定为 CRepairRec 类,注意到 CRepairRec 类中存在一个名为 EncryptedKeys 的字节数组,正好可以将反序列化的 gadget 放入其中,这样在 CBinaryServerFormatterSinkProvider 反序列化检查白名单时,CDbCryptoKeyInfo 对象可以绕过,同时在其构造函数中会触发 CRepairRec 对象的二次反序列化,结合 ObjRef 链可以绕过黑名单:
综上,
通过二次反序列化可以绕过了黑白名单限制。
- 利用过程
完整利用过程如下:
最终成功触发 RCE :
- 修复方式
将 ObjRef 加入了黑名单:
但是第一次绕过白名单触发 .NET Remoting 反序列化的过程没有修复,这也为后续的绕过提供了可能性。
CVE-2025-23120
修复补丁只将
ObjRef 加入了黑名单,如果能够找到新的 gadget ,那么就能够再次 RCE。BinaryFormatter 存在很多公开的 gadget ,比如 TextFormattingRunProperties 或者 DataSet 等等,但是这些 gadget 早就被加入了黑名单。可以尝试在 Veem 自生类中寻找继承了这些类的子类。搜索找到 3 个继承 DataSet 的类: xmlFrameworkDs 、 BackupSummary 和 CDataSetSurrogate ,
并且这些子类均不在黑名单中:
xmlFrameworkDs 和 BackupSummary 类构造函数会调用父类 DataSet 构造函数,但是 CDataSetSurrogate 不会,因此 xmlFrameworkDs 和 BackupSummary 作为 gadget 也可以触发 RCE :
Veeam 黑名单检测主要对 assemblyName 和 typeName 进行检测,
可以尝试在 ysoserial.net 中新增生成 xmlFrameworkDs 或者 BackupSummary 类的 gadget 生成代码。重写 GetObjectData 函数,将 assemblyName 和 FullTypeName 设置为合法值即可
:
触发二次反序列化的主要调用栈如下:
成功实现 RCE :
针对 BinaryFormatter 反序列化,可以搜索那些继承了经典 gadget 的子类,如果其构造函数也调用了父类构造函数,那么就有可能用来绕过黑名单限制。
由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。