Some notes of Microsoft Exchange Deserialization RCE (CVE-2021–42321)
Bắt đầu như nào nhỉ, …
Tầm 1 tháng trước, mọi người trong giới researcher/APT/bluer đều đổ dồn ánh mắt về Trung Quốc khi mà VMWare, Exchange, Iphone … liên tục bị pwn tại Tianfu Cup.
Với mình thì thứ đáng chú ý lần này chính là Exchange lại bị pwn sau nửa năm im ắng.
Khá là không may mắn với đội dev của Exchange, khi mà quanh năm ngày tháng chỉ có ngồi ăn và đi fix bug, và sự xuất hiện của lỗ hổng gần đây nhất cũng có sự góp công của họ …
Đợt này thì không còn mấy hào hứng khi MS ra patch cho Exchange nữa, vì mình khá là bận và không còn muốn đốt hết hơn nửa thời gian của tháng vào việc ngồi diff patch. (Dẫu vậy thì sự tò mò đã không cho phép mình ngồi yên).
Ngày hôm qua thì trên blog của tổ chức X cũng có đăng bài phân tích về bug này, nhưng có lẽ vẫn hơi thiếu chút gì đó nên mình đành bắt tay vào viết bài này, dưới dạng 1 cái note để sau này có thể sẽ cần tham khảo lại và không bị miss những thông tin quan trọng, bắt đầu vào bài nào.
…
Từ Advisories của MS, ta có thể thấy đây là một lỗ hổng post-auth:
Và chỉ có patch cho đúng 4 phiên bản này của Exchange:
Điều khiến mình thắc mắc đầu tiên ở đây, là 1 bug RCE như vậy, được show off tại Tianfu Cup thì đáng ra phải là Pre-Auth chứ nhỉ 🤔. Có lẽ MS đã giấu đi đâu đó cái bug bypass authen rồi, phải chờ patch của tháng 12, tháng 1/2022 mới biết được thực hư nó ra làm sao.
Phiên bản mình làm lab lần này là Exchange 2019 CU9, và chỉ khi upgrade lên CU10 mình mới phát hiện ra bug ở đâu 🤣.
Trong quá trình tìm patch để diff, mình phát hiện ra vào 11/10/2021, Microsoft đã cố patch điều gì đó (ngay trước TianfuCup vài ngày)
Để có cái nhìn rộng hơn, mình chọn bản patch của tháng 7 để diff, nếu bug được đem ra show ở Tianfu thì có nghĩa là nó phải được tìm ra trước đó vài tháng rồi.
Sau khi decompile + xóa rác rồi diff thì chỉ còn đâu đó 270 files thực sự có thay đổi (╯°□°)╯︵ ┻━┻.
Hầu như tất cả sự thay đổi của bản patch lần này đều là muốn ngăn chặn Insecure Deserialization ở đâu đó (còn ở đâu thì chưa biết).
#THE SINK
Quanh quẩn với đám patch này được vài hôm, mình phát hiện ra có điều gì đó sai sai ở class TypedBinaryFormatter (class này đã bị remove trong bản patch mới).
Method Deserialize() của class này có nhận vào 1 tham số”SerializationBinder”, tuy nhiên phần code xử lý bên trong thì hoàn toàn không sử dụng gì tới cái “binder” này cả:
Method này gọi tới ExchangeBinaryFormatterFactory.CreateBinaryFormatter() để tạo 1 instance của BinaryFormatter cho việc Deser:
Method CreateBinaryFormatter() chỉ đơn thuần là tạo instance của BinaryFormatter với Binder=ChainedSerializationBinder.
Với các tham số truyền vào từ TypedBinaryFormatter.Deserialize(), chúng ta có một ChainedSerializationBinder với:
- strictMode = false
- allowList = System.DelegateSerializationHolder
- allowedGenerics = null
Điều đó có nghĩa là cái binder được truyền vào từ TypedBinaryFormatter.Deserialize() hoàn toàn không có tác dụng gì cả.
Tiếp tục soi vào method ChainedSerializationBinder.BindToType(), method này tiếp tục gọi tới ValidateTypeToDeserialize() để xử lý chính.
Và cách xử lý của method ValidateTypeToDeserialize() cũng có một số điều khá là thú vị (khó hiểu ?!)
Cách xử lý của đoạn check này như sau:
Nhánh (1):
- Nếu như strictMode = false, class hiện tại không nằm trong allow list, và class này nằm trong blacklist => throw InvalidOperationException().
- Mặc dù đoạn code này nằm trong try catch statement, nhưng phần catch nó không có catch InvalidOperationException mà chỉ catch BlockedDeserializationException.
- Do vậy, nếu nhánh (1) throw InvalidOperationException thì cũng coi như việc xử lý sẽ kết thúc, và data sẽ không được deserialize nữa
Soi kỹ hơn vào blacklist của ChainedSerializationBinder, blacklist này được build bởi method BuildDisallowedTypesForDeserialization():
Blacklist này bao gồm các gadgetchain phổ biến, tuy nhiên có vẻ như đã có một lỗi typo nhỏ ở đây:
Class đúng phải là System.Security.Claims.ClaimsPrincipal
Và trong cái blacklist này mình cũng thấy sự thiếu vắng của khá nhiều gadgetchain phổ biến, tiêu biểu như TypeConfuseDelegate (fact: ngay cả trong bản patch mới cũng chưa thấy class SortedSet của gadgetchain này bị chặn ( ͡° ͜ʖ ͡°) ).
Như vậy hoàn toàn có thể sử dụng các gadgetchain để bypass, việc xử lý theo nhánh (1) của ChainedSerializationBinder hoàn toàn vô dụng.
Tiếp tục với nhánh (2):
Nếu như Class không thỏa mãn điều kiện (bị blacklist, không nằm trong whitelist …) thì sẽ bị throw ra một cái BlockedDeserializeException …
Tuy nhiên ở ngay nhánh 2 phía dưới thì nó đã được catch lại rất gọn gàng, và chỉ throw khi flag = true. Với flag ở đây = strictMode, thứ mà mặc định được set = false
Tổng kết cả 2 thứ lại, chúng ta có được một cái ChainedSerializationBinder vô dụng =)))
Điều đó mang lại cho chúng ta một lỗ hổng Insecure Deserialization ngay tại TypedBinaryFormatter.Deserialize(), thứ mà được sinh ra để làm cho việc Deserialize được an toàn hơn 🤣
…
#THE SOURCES
Như thường lệ, mình trace ngược về các method gọi tới TypedBinaryFormatter.DeserializeObject()
Việc trace này hơi mất thời gian một chút, do dnspy bị ngáo trong việc find usage bằng interface, cho nên mình phải làm việc này bằng tay! (Hoặc có thể sử dụng Jetbrains Rider để tìm Hierachy)
Trong đó có OrgExtensionSerializer.TryDeserialize() đang gọi tới method ClientExtensionCollectionFormatter.Deserialize()
Method này lấy stream từ UserConfiguration.GetStream() rồi truyền vào Deserialize()
Sau khi search google một hồi thì mình biết được cái UserConfiguration này có thể được tạo ngay cả với một người dùng bình thường (https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration-operation).
Và khi trace code, mình phát hiện ra khi CreateUserConfiguration còn có field BinaryData nữa (không thấy trong doc của MS có nói về cái này).
Khi gọi CreateUserConfiguration.Execute(), method UserConfigurationCommandBase.SetProperties() cũng được gọi theo. Method này gọi tới SetDictionary(), SetXmlStream() và SetStream().
Trong đó, method SetStream() sẽ lấy dữ liệu từ field BinaryData, sau đó Decode Base64 và lưu lại thành stream:
=> Như vậy có nghĩa là ta hoàn toàn có thể control được giá trị của UserConfiguration.GetStream() mà không cần thêm quyền gì đặc biệt.
Để control dữ liệu này, request tới EWS chỉ đơn giản như sau:
Trong đó CfgName, folder id, change key cần phải chỉnh sửa cho phù hợp thì poc mới có thể hoạt động
…
Tiếp tục trace ngược code cho tới khi gặp class GetClientAccessToken
Search qua google một vòng, thì ra đây là một tính năng có thể gọi từ EWS (https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getclientaccesstoken-operation), một request mẫu sẽ có dạng như sau:
…
Đến đây thì việc chạm tới Sink coi như hoàn thiện, có thể được tóm tắt thành 2 step như sau:
- Step 1: Tạo UserConfiguration có chứa BinaryData = GadgetChain
- Step 2: Gọi tới GetClientAccessToken để trigger Deserialization
Chỉ cần sử dụng gadgetchain TypeConfuseDelegate và pop calc.exe …
Tóm lại là, chúng ta có stack trace khi exploit như sau:
…
#THE IMPROVEMENT
Tuy nhiên đó chỉ là để show off trên lab thôi, chứ thực tế thì nó sẽ là như này cơ:
Và trong thực tế thì tất cả những process spawn ra từ w3wp.exe đều bị các AV/EDR/sysmon … quan tâm rất đặc biệt. Nếu có thể RCE được thì một là trên lab, còn thứ 2 thì chúc mừng, rất có thể bạn đã lọt vào honeypot của một ai đó ( ͡° ͜ʖ ͡°)
Với trường hợp hiện tại, chúng ta có thể sử dụng gadgetchain TypeConfuseDelegate, hoặc một gadget khác là ClaimsPrincipal.
Với design hiện tại của TypeConfuseDelegate, chúng ta chỉ có thể RCE từ đó (có thể làm việc khác nhưng sẽ được đề cập sau), còn với ClaimsPrincipal, nó có 1 method là OnDeserializedMethod(), được gọi khi Deserialize thành công. Method này lại tiếp tục gọi tới DeserializeIdentities() để khôi phục field Identity của object này:
DeserializeIdentities() lại tiếp tục gọi tới BinaryFormatter.Deserialize(), từ đó mới gây ra chuyện Second-Order Deserialization, như đã được viết rất rõ tại đây (http://muffsec.com/blog/finding-a-new-datacontractserializer-rce-gadget-chain/):
Từ những dữ kiện này cho phép chúng ta có thể gói một gadgetchain phức tạp hơn — thứ mà đã bị filter trước đó, ví dụ như gói gadget “ActivitySurrogateSelector” cho phép load dll:
<ảnh minh họa>
Ý tưởng load dll được lấy từ blog post này: http://www.zcgonvh.com/post/analysis_of_CVE-2020-17144_and_to_weaponizing.html
Bug được phân tích trong bài trên cũng khá là giống với bug lần này của Exchange 2019, trong bài viết, tác giả có sự dụng gadgetchain “ActivitySurrogateSelector” để load một file dll, cho phép inject web shell vào memory:
Ý tưởng này khá là hay, mình cũng được mở mang đầu óc khá nhiều khi tham khảo blog này.
Và để áp dụng vào context hiện tại, thay vì gọi Process.Start(), mình chuyển qua sử dụng Microsoft.JScript.Eval.JScriptEvaluate() để eval JScript, một cách làm tương tự như chopper shell hoặc antsword.
Thực thi được JScript sẽ giúp chúng ta thoải mái hơn trong việc đọc file, thực thi lệnh mà khó bị phát hiện.
Đây là đoạn code được dùng để thay thế cho Process.Start()
Một lưu ý nhỏ là do không access được page context của IIS nên sẽ không thể gọi tới các method như: Response.Write, System.Thread … Nhưng ở đây chỉ cần như vậy là đủ rồi!
Việc tiếp theo cần làm là copy cái file DLL vừa compile vào cùng folder với ysoserial, rồi gói cái Object ActivitySurrogateSelector vào bên trong gadgetchain ClaimsPrincipal như dưới đây (ysoserial hiện tại chưa có gadget ClaimsPrincipal, tuy nhiên nó cũng khá giống với ClaimsIdentity nên có thể copy paste code và sửa một chút là sẽ hoạt động):
- payload gen được sẽ rất dài
…
Memory shell, payload đã được chuẩn bị hoàn thiện, tuy nhiên vẫn có một vấn đề rất quan trọng ở đây:
Các version .NET gần đây đã implement một số cơ chế kiểm tra để tránh việc lợi dụng gadgetchain ActivitySurrogateSelector, cũng do vậy mà ngay trong ysoserial cũng có thể thấy một gadgetchain được sử dụng vào việc disable cơ chế kiểm tra này:
Gadgetchain này chỉ đơn giản là sử dụng gadget TextFormattingRunProperties để gọi các method thay đổi giá trị của property “microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck”
TextFormatting cũng đã có trong blacklist của ChainedSerializationBinder, do vậy cần phải gói vào ClaimsPrincipal mới có thể thực thi!
=)))) I’ve been laughing for hours while making this meme
Và đây là kết quả khi sử dụng gadgetchain này:
Gadgetchain trả về một “InvalidCastException” và rồi process w3wp.exe bị crash! Điều này cũng đã từng được đề cập tại https://www.netspi.com/blog/technical/adversary-simulation/re-animating-activitysurrogateselector/
Cách hoạt động của gadgetchain này như sau: khi deserialize xong chương trình sẽ gọi tiếp constructor, trong constructor của class này gọi tới GetObjectFromSerializationInfo() và gọi tiếp tới XamlReader.Parse():
Trong đó XamlReader.Parse() chính là method xử lý và thực thi các lệnh trong gói payload xaml được truyền vào.
Sau khi thực thi, constructor của TextFormattingRunProperties lại ép kiểu sang kiểu “Brush”, tuy nhiên dữ liệu của chúng ta không thỏa mãn nên gây ra InvalidCastException, và chương trình cũng không catch cái exception này nên mới có chuyện bị crash cả process w3wp.exe.
Vậy là không thể sử dụng TextFormattingRunProperties để Parse Xaml Payload và disable check được. Cần phải tìm ra một gadget khác, cách kết hợp khác giữa các gadget để có thể disable được đoạn check, hoặc là tìm ra một gadget khác để gọi XamlReader.Parse().
Lòng vòng một hồi thì mình có xem lại cái TypeConfuseDelegate, gadgetchain này nhìn khá là đơn giản, và cũng không khó để modify:
Điểm mấu chốt của gadget này là phần chỉnh sửa invoke_list, cho phép truyền vào một static method đã được đóng gói, hiểu đại khái là ta có thể gọi tới một static method khác ngoài Process.Start().
( ͡° ͜ʖ ͡°)
Kết hợp với XamlReader.Parse() cũng là một static method thì mọi chuyện đã đơn giản hơn rất nhiều. Cách chỉnh sửa để kết hợp cũng rất đơn giản như vậy thôi:
Sau khi kết hợp với payload xaml disable check thì mọi thứ đã hoạt động ngon lành, việc tiếp theo sử dụng Eval JScript ra sao có thể mình sẽ nói trong một tập khác!
PoC video: https://www.youtube.com/watch?v=Fmx6JlSABAQ
PoC: I don’t think i will …
Kiểm tra tất cả các version hiện tại của Exchange thì mình xác định được PoC này chỉ hoạt động trên các phiên bản sau:
- Microsoft Exchange 2019 CU10, 11
- Microsoft Exchange 2016 CU21, 22
Có thể lỗ hổng này xảy ra do đội developer bị lúng túng khi migrate qua cách sử dụng ChainedSerializationBinder mới, hoặc vì một lý do gì đó khác … ¯\_(ツ)_/¯ who know.
____________
Cảm ơn các bạn đã quan tâm theo dõi
Thank @peterjson for collaborating and English version of this blog post, and an anonymous man for helping!
__Jang__