4 năm trước mình đã có series phân tích một số gadgetchain trên Java Deserialization, ban đầu cũng vì mục đích lưu trữ và tham khảo lại trong trường hợp mình không nhớ nổi, và đúng thế thật, đôi khi đọc lại mình còn không nhớ là đã từng viết những thứ này.
Đợt này thì mình chuyển sang làm vấn đề tương tự, nhưng trên nền tảng .NET. Về cơ bản thì cơ chế tận dụng lỗ hổng Insecure Deserialization để Remote Code Execution trên .NET cũng vẫn khá giống với Java, vẫn phải lợi dụng các gadgetchain. Trước đó đã từng có một số bài viết về vấn đề này, các bạn tham khảo tại:
- https://medium.com/tradahacking/net-deserialization-101-b39b1f33f7c9
- https://medium.com/tradahacking/binaryformatter-textformattingrunproperties-8335a70e62e3
Bản chất của lỗ hổng Insecure Deserialization là trong quá trình deserialize, dữ liệu serialized sẽ được khôi phục lại thành object, với các object phức tạp, sẽ cần cơ chế deserialize phải gọi tới một số method, constructor, callback của class để khôi phục các object này. Tình cờ trong số các class đó lại có một số method, callback có thể kết hợp lại với nhau để thay đổi luồng thực thi theo ý muốn của attacker, đó được gọi là gadgetchain! Hình dưới đây mô tả một cách cơ bản nhất về cách hoạt động của các gadgetchain này:
#The difference
Một khác biệt nho nhỏ của .NET Deser vs Java Deser, đó là về cơ chế library loading của .NET Framework có phần dễ dãi hơn so với Java.
Trên Java, các class cần thiết của gadgetchain bắt buộc phải tồn tại trên classloader của target thì mới có thể sử dụng.
Còn đối với .NET, hầu hết các public gadgetchain (tham khảo từ ysoserial.net) đều tồn tại sẵn trên .NET Framework assembly (mình xin phép gọi là assembly thay cho library). Có nghĩa là chỉ cần chương trình có lỗi deserialization, chạy trên .NET Framework là có thể bị khai thác! Các assembly này thường có sẵn tại các folder: “C:\Windows\assembly”, “C:\Windows\Microsoft.NET”,
Mặc dù có thể khi dùng các công cụ process explorer, dnSpy attach vào process sẽ không thấy các assembly chứa gadgetchain được load lên, nhưng chỉ cần cung cấp đầy đủ Type name, Assembly name thì assembly sẽ được .NET Framework tự tìm trong folder assembly và load lên, đây là một lợi thế rất lớn khi exploit deserialization trên .NET Framework.
#Gadgetchains
Sau khi xem xét kỹ lưỡng thì mình quyết định sẽ phân tích gadgetchain TypeConfuseDelegate đầu tiên bởi đây là một dạng final gadgetchain, nghĩa là sẽ trực tiếp trigger code execution.
Các gadgetchain như: TextFormattingRunProperties, ClaimsIdentity, SessionSecurityToken được gọi là bridge/derived gadgetchain, nghĩa là nó không trực tiếp trigger RCE, mà nó sẽ gói một gadgetchain khác vào trong:
Mình sử dụng luôn công cụ ysoserial.net để phân tích gadgetchain này.
Sau khi build ysoserial, mình dùng dnSpy để debug, cho đến thời điểm này thì mình thấy dnSpy vẫn là công cụ hỗ trợ tốt nhất để debug .NET binary, config run mình để như sau:
Option -test là để ysoserial sẽ deser payload ngay sau khi generate.
Với dnSpy, mình có thể dùng tính năng Edit > Search Assemblies để tìm kiếm các Class, Method, Field, … bên trong project:
Gadgetchain TypeConfuseDelegate được handle tại class TypeConfuseDelegateGenerator như dưới đây:
Với method Generate() là method được gọi để generate gadgetchain:
Nội dung của method này cơ bản như sau:
public override object Generate(string formatter, InputArgs inputArgs)
{
bool flag = inputArgs.Minify && inputArgs.UseSimpleType && (formatter.Equals("binaryformatter", StringComparison.OrdinalIgnoreCase) || formatter.Equals("LosFormatter", StringComparison.OrdinalIgnoreCase));
object obj;
if (flag)
{
//skip this
}
else
{
obj = base.Serialize(TypeConfuseDelegateGenerator.TypeConfuseDelegateGadget(inputArgs), formatter, inputArgs);
}
return obj;
}
Từ Generate() sẽ gọi tới TypeConfuseDelegateGadget(), đây chính là method sẽ return object gadgetchain:
public static object TypeConfuseDelegateGadget(InputArgs inputArgs)
{
string cmdFromFile = inputArgs.CmdFromFile;
bool flag = !string.IsNullOrEmpty(cmdFromFile);
if (flag)
{
inputArgs.Cmd = cmdFromFile;
}
Delegate da = new Comparison<string>(string.Compare);
Comparison<string> d = (Comparison<string>)Delegate.Combine(da, da);
IComparer<string> comp = Comparer<string>.Create(d);
SortedSet<string> set = new SortedSet<string>(comp);
set.Add(inputArgs.CmdFileName);
bool hasArguments = inputArgs.HasArguments;
if (hasArguments)
{
set.Add(inputArgs.CmdArguments);
}
else
{
set.Add("");
}
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.Instance | BindingFlags.NonPublic);
object[] invocationList = d.GetInvocationList();
object[] invoke_list = invocationList;
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(d, invoke_list);
return set;
}
Nhìn thoáng qua ta có thể thấy Process.Start
, đây có thể chính là sink của gadgetchain này, hãy set breakpoint trong Process.Start và chạy để xem chương trình sẽ hoạt động ra sao!
Đây có thể cũng là một trick giúp các bạn làm blueteam trong quá trình phân tích gặp payload serialized, hãy cứ cho vào máy ảo, set breakpoint và deser cái payload đó, kiểu gì cũng sẽ biết được luồng thực thi!
Process dừng tại Process.Start, ta có call stack như sau:
Quay lại trước đó 1 frame (dùng chuột chọn frame đó trên call stack windows), tại ComparisonComparer.Compare():
Tại method này, ComparisonComparer.Compare() gọi tới this._comparison(). Với this._comparison là một delegate method của Process.Start(). Do đó, khi gọi tới this._comparison() tương đương với việc gọi tới Process.Start().
Tiếp tục xem xét tới frame trước đó, tại SortedSet.AddIfNotPresent(), logic của đoạn này cơ bản là sẽ sử dụng this.comparer.Compare() để so sánh kết quả đang được Add với các giá trị đã có sẵn trong Set, nếu không tồn tại sẽ add vào Set (với this.comparer hiện đang là ComparisonComparer).
Call stack tại đây như sau:
Ta có thể thấy rõ ràng call stack bắt nguồn từ BinaryFormatter.Deserialize():
BinaryFormatter.Deserialize()
-> ObjectReader.Deserialize()
-> ObjectManager.RaiseDeserializationEvent()
-> SortedSet.OnDeserialization()
-> SortedSet.Add()
Đây là minh chứng cho một lý thuyết mình vừa đề cập ở phần mở bài, đó là khi Deserialize, các cơ chế Deserialize sẽ gọi tới các callback method của class để thực hiện khôi phục object ban đầu!
Nội dung của SortedSet.OnDeserialization():
Đó, hoạt động của gadgetchain TypeConfuseDelegate này chỉ đơn giản như vậy thôi ¯\_(ツ)_/¯.
Để tóm tắt lại thì ta có gadgetchain như sau:
-> SortedSet.OnDeserialization()
-> SortedSet.Add()
-> SortedSet.AddIfNotPresent()
-> ComparisonComparer.Compare()
-> Process.Start()
Tuy nhiên đó chỉ là với môi trường .NET Framework, với .NET Core thì gadgetchain này lại không thể hoạt động. Nguyên nhân là do một thay đổi nhỏ trong class ComparisonComparer của phiên bản .NET Core, attribute Serializable đã bị loại bỏ nên gadgetchain này không thể được serialize/deserialize!
Thanks for reading!