SharePoint not-so 0day (How I’ve failed at p2o Vancouver 2024)

Jang
8 min readMar 26, 2024

--

Cũng đã lâu mình chưa lên bài nào về kỹ thuật, lần gần nhất cũng là về entry SharePoint tại kỳ p2o Vancouver 2023.

Vừa rồi tại p2o Vancouver 2024 mình cũng đã có một entry sharepoint, tuy nhiên kết quả thì mọi người đã biết, mình ko thành công sau 3 lần thử.

Sau vài ngày setup lại và debug thì mình đã tìm ra nguyên nhân bị fail, bắt nguồn cũng chính từ sự chủ quan và ngộ nhận của mình. Bài viết này một phần là để chia sẻ về những ngộ nhận mà mình và có thể nhiều system admin/re-searcher gặp phải, cũng như phân tích chi tiết về bug (not so 0day) mà mình đã đem vào p2o vừa rồi!

Không dài dòng nữa, bắt đầu thôi!

#The bug

Mình tình cờ thấy bug này trong quá trình phân tích bản vá tháng 09/2023 (https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-38177).

Chi tiết của CVE này mình sẽ ko nêu ở đây (vì thực sự mình chưa reproduce được =]] ), tuy nhiên có một thay đổi đáng chú ý tại SPDistributedCacheUtils.ByteArrayToObject():

DataContractSerializer với type variable đã được sử dụng thay vì NetDataContractSerializer,

Theo mình dự đoán, đây có thể là phần sink của CVE-2023–38177, bởi NetDataContractSerializer cho deserialize data tùy ý, có thể bị lợi dụng để RCE dễ dàng, còn DataContractSerializer lại yêu cầu một kiểu dữ liệu rõ ràng trước khi deserialize.

Tại đây mình cũng nhận ra đó giờ mình chỉ toàn tập trung vào BinaryFormatter, XmlSerializer mà bỏ quên mất NetDataContractSerializer, cũng là một sink tiềm năng không kém.

Tiếp tục trace back từ SPDistributedCacheUtils.ByteArrayToObject() để tới được SPDistributedCache.GetObject()

Tại đây ta có thể thấy, byte[] array được lấy ra từ this._dataCachePointer, sau đó sẽ được truyền vào và deserialize bằng NetDataContractSerializer()

Bạn có nghĩ this._dataCachePointer đơn thuần chỉ là một dạng Cache Map lưu trực tiếp trên memory của process này, this._dataCachePointer.Get() chỉ là lấy element từ Map này mà thôi? Mình cũng đã từng nghĩ đơn giản như vậy và thường auto skip những đoạn call như vậy khi review code.

Tuy nhiên sự thật không phải như vậy, nội dung của this._dataCachePointer.Get() như sau:

DataCache.Get() -> DataCache.InternalGet() -> DataCache.SendReceive()

Tại method DataCache.SendReceive(), nếu reqMsg.UserObject != null, reqMsg.UserObject sẽ được serialize bằng Utility.SerializeUserObject() sau đó được gửi đi bằng RoutingClient.SendMsgAndWait()[1].

this._clientDRM.ProcessRequest() sẽ gọi tiếp tới DRM.SendToDestination() -> RequestBody.Send() -> DRM.SendRequest() -> WcfClientChannel.Send()

Tại đây, mình biết được this._dataCachePointer.Get() không đơn thuần chỉ là một cái Map để lưu cache, mà nó sẽ phải thực hiện gửi một request tới cache server thông qua WCF Binding để lấy dữ liệu về, cache server này trên sharepoint được gọi là AppFabric Server/DistributedCache Service.

*Lesson learnt: không bao giờ bỏ qua một chi tiết nào dù trông nó có nhỏ bé vô hại.

Quay trở lại phần [1], ta có thể thấy trước khi gửi Msg, dữ liệu được serialize bằng Utility.SerializeUserObject(), method này gọi tới Utility.NetDataContractSerialize() để serialize data:

Như vậy, chắc chắn ở phía Cache Server sẽ phải có phần xử lý serialized data này.

— — — — —

Chuyển target sang AppFabric Server, service này được chạy tại port 22233, 22234, 22236 và được mở kết nối firewall

Đây là một Managed Process nên mình hoàn toàn có thể attach dnSpy vào và debug, set breakpoint tại NetDataContractSerializer.ReadObject() và process đã hit breakpoint vài giây sau đó:

Method VelocityDataStore.ProcessMessageRequest() có nhiệm vụ xử lý các Wcf Request được gửi tới AppFabric Server, tại đây, nếu request có dạng Notification Request, request.Value sẽ được deserialize bằng Utility.Deserialize().

Method này hỗ trợ nhiều kiểu deserialize dữ liệu, trong đó có NetDataContractSerializer():

Do checkTypeToLoad được set = true, CustomSerializationBinder sẽ được sử dụng để check Type. Tuy nhiên cũng không cần lo lắng lắm, Binder này chỉ đơn thuần là kiểm tra xem có tìm được Assembly trong domain hiện tại hay không, chứ không restrict Type!

=> Tóm lại ta có thể gửi một Request Msg với type = notification và Value chưa deserialization gadgetchain để RCE?!

Mình đã thử viết script để send payload, tuy nhiên do chưa có thư viện python nào hỗ trợ tương tác với WCF net.tcp binding, mình dùng tạm thư viện có sẵn của AppFabric Client, gọi tới bằng Reflection. PoC Script được gửi chi tiết ở cuối bài!

PoC in action: https://www.youtube.com/watch?v=6O2X9ehNQ3Y

….

# The mistake

Yeah, câu chuyện sẽ là happy ending nếu chỉ dừng lại ở đó, nhưng thực tế không như vậy ¯\_(ツ)_/¯.

Mãi đến tận lúc vào demo tại p2o, mình vẫn khá tự tin là PoC hoàn chỉnh, không có một exception gì có thể cản được cả, tuy nhiên mình đã fail sau cả 3 lần thử, tất cả 3 lần đều trả về chung 1 lỗi mà mình chưa gặp bao giờ.

Ban đầu mình còn nghĩ có thể do máy ảo của BTC bị lag (đúng là trước đó nó rất lag, mất rất lâu để gửi được request tới). Tuy nhiên vào lần thử cuối cùng, request gửi rất nhanh nhưng lỗi vẫn như vậy.

Mình có tìm qua thông tin về lỗi này trên google và được câu trả lời là do máy chủ yêu cầu thêm cơ chế xác thực nhưng phía client cung cấp không đủ. Có nghĩa là mình đã bị miss mất đoạn authentication.

Đối với WCF Binding, phương thức authentication sẽ được setup bằng element securityProperties:

<securityProperties mode="Transport" protectionLevel="EncryptAndSign" />

Trên AppFabric Server, config này có thể được export bằng command:

export-cacheclusterconfig -file C:\temp\aa.txt

Với môi trường test của mình, AppFabric Cache đang có config như sau:

Theo như config này, mode = NoneprotectionLevel = None sẽ cho phép người dùng access tới Distribute Cache Server mà không cần qua bước xác thực nào cả!

Điều đó dẫn tới việc mình có thể gửi payload tùy ý tới môi trường test mà không gặp vấn đề gì cả.

Một điều đáng lưu ý là môi trường SharePoint Server của mình đã được install bản vá mới nhất của Microsoft tại thời điểm đó (March 2024 patch).

Vậy điều gì đã thực sự xảy ra?

..

Sau vài ngày lần mò trên khắp diễn đàn của Microsoft, mình đã gặp một topic như vậy:

Tại bản vá February 2023, user trên đã gặp phải một lỗi liên quan tới “SecurityMode ProtectionLevel”, điều này chắc chắn có liên quan tới phần Wcf Binding của AppFabric Cache.

Lội sâu hơn vào phần comment, mình biết được từ bản vá January 2023 đã có thay đổi ở phần SecurityMode và ProtectionLevel của SharePoint. Điều đó có nghĩa là cái bug mà mình tìm ra thực ra đã bị người khác thấy và fix từ lâu rồi.

Quay trở lại với cái topic trên, một phần đáng chú ý trong topic trên, đó là tác giả có đề cập tới việc chạy SharePoint Products Configuration Wizard sau khi update server:

Mình thấy ngờ ngợ có gì đó sai sai ở đây, vì đó giờ mình chỉ cài bản vá xong là xong, chưa bao giờ chạy thêm gì sau đó cả!

Sau khi kiểm chứng lại thông tin từ Microsoft (https://learn.microsoft.com/en-us/sharepoint/upgrade-and-update/install-a-software-update) và test trên chính lab của mình, exploit không còn hoạt động nữa. Và config mới của AppFabric Cache đã được thay đổi như sau:

Yeah, that moment he knew he fuked up

Mình đã thử hỏi nhiều anh em re-searcher, system admin khác gần như 80% đều nghĩ chỉ cần cài bản vá là xong.

Như vậy, theo mình thì hiện tại còn khá nhiều máy chủ SharePoint cũng gặp tình trạng chỉ được cài bản vá nhưng chưa chạy Products Configuration Wizard nên các target này vẫn bị ảnh hưởng bởi bug trên!

Bài học mà mình rút ra ở đây là:

P/s: Mặc dù port 22233 chỉ mở trong mạng nội bộ, tuy nhiên ai đảm bảo được trong mạng nội bộ này không có tổ chức nào đang nằm vùng ( ͡° ͜ʖ ͡°). Nên giới hạn port này chỉ trong phạm vi của các máy chủ sharepoint với nhau thôi!

PoC project: https://github.com/testanull/SharePoint-not-so-0day/

Thanks for reading!

--

--