SharePoint Server và câu chuyện về research. Phần 2: Trái đắng và quả ngọt

Tiếp tục về SharePoint… Sau “cú dup” với CVE đầu tay, tôi tiếp tục đi sâu hơn vào chức năng này và tìm ra một flow khá hay ho. Đặc biệt hơn, flow này hoàn toàn dựa vào quá trình parse XML config để khởi tạo workflow – xin đính chính lại một chút là workflow config có định dạng là XAML.

Phân tích

Sau quá trình validate XAML config (đã nhắc đến ở phần 1) thì SharePoint sử dụng hàm WorkflowMarkupSerializer.Deserialize để tạo workflow. Không giống như XML đơn thuần, XAML cho phép sử dụng “compact format attribute”, các attribute này sẽ được bắt đầu bằng ký tự “{” và kết thúc bằng ký tự “}”.

1. ValueType với compact format

Hàm DeserializeFromCompactFormat được sử dụng để xử lý trường hợp này. Đây là một hàm rất dài, tôi sẽ tóm tắt chức năng như sau:

  • Lấy kiểu dữ liệu của SomeType, kiểm tra xem kiểu dữ liệu này có nằm trong allow list hay không. Nếu không thì throw exception.
  • Đọc và parse các argument thành một mảng các chuỗi [“Arg1”, “Arg2”, “Arg3”].
  • Sử dụng reflect để tìm kiếm một constructor của SomeType với số lượng tham số và kiểu dữ liệu phù hợp (3 tham số chuỗi).
  • Tạo và trả về instance của SomeType.

Trace bằng dnSpy chúng ta sẽ được flow:

Nội dung của các hàm sẽ như sau.

2. Hàm DeserializeSimpleMember
3. Hàm DeserializeFromCompactFormat
4. Hàm GetValueFromMarkupExtension

Hơi dài dòng và khó hiểu nhưng tóm gọn lại thì nếu SomeType kế thừa MarkupExtension thì ValueType = (new SomeType).ProvideValue(), ngược lại thì ValueType = new  SomeType.

Như đã nếu ở phần 1, ValueType sẽ mang giá trị là chuỗi System.Resources.ResourceSet. Như vậy thì chia ra 2 trường hợp. Một là SomeType phải là System.String, điều này khá khó thực hiện vì System.String không tồn tại constructor nào có tham số là chuỗi cả. Hai là SomeType.ProvideValue phải trả về một chuỗi. Sau khi xem xét toàn bộ các kế thừa của MarkupExtension thì tôi phát hiện một case khá thú vị – TypeExtension. Theo như document mặc định thì markup này chỉ trả về một Type (kiểu dữ liệu Type). Nhưng trong SharePoint thì không.

5. Lớp TypeExtension

Dễ dàng thấy được hàm ProvideValue trả về một chuỗi tại dòng 36 hoặc 38. Ngoài ra lớp này còn có constructor thỏa mãn điều kiện nêu phía trên. Do đó đây có thể coi là một ứng viên tiềm năng cho công cuộc bypass.

Chúng ta sẽ phân tích kỹ hơn về cách hoạt động của hàm này. Đầu tiên là thuộc tính typeName sẽ là chuỗi bất kỳ (đã nếu phía trên). Tại dòng 23-24, chương trình sẽ kiểm tra ký tự “:” trong typeName sau đó chia thành 2 phần (tương tự như namespace trong XML). Ví dụ: typeName là “abc:SomeType” thì text2 là “abc” còn text là “SomeType”. Dòng 28 sẽ kiểm tra và lấy ra kiểu dữ liệu từ config, nếu tồn tại thì sẽ trả về kiểu dữ liệu tìm được. Nhưng cái chúng ta cần thì ngược lại, dòng 28 phải trả về null mới có khả năng bypass. Lưu ý là tất cả namespace đều được kiểm tra tính hợp lệ. Cú pháp của namespace sẽ như sau.

Để ý kỹ hơn dòng 36, hàm chỉ đơn giản là trả về namespace + “.” +  text. Như vậy thử tưởng tượng rằng, nếu chúng ta có một namespace hợp lệ, ví dụ như System và text là Resources.ResourceSet. Vậy điều cần thiết ở đây là làm thế nào cho dòng 28 trả về null. Rất đơn giản, chúng ta chỉ cần để một assembly khác. Trong ví dụ này, tôi để mục assembly là System.Workflow.ComponentModel và code ở dòng 28 thực chất chỉ là tìm kiếm System.Resources.ResourceSet trong thư viện System.Workflow.ComponentModel. Điều này đương nhiên sẽ trả về null vì không tồn tại kiểu dữ liệu này, sau đó trả về chuỗi tại dòng 36. Như vậy sau một quá trình dài dòng thì chúng ta đã gán được ValueType là System.Resources.ResourceSet.

Quay trở lại với quá trình validate config, TypeExtension nằm trong allow list cho nên việc này hoàn toàn khả thi. Payload hoàn chỉnh của chúng ta sẽ là:

Ở đây tôi không dùng x:Type mà thay vào đó là ns1:TypeExtension, điều này để chắc chắn việc gọi hàm ProvideValue không bị nhảy vào markup mặc định.

Kết luận

Thật ra tôi đã phát hiện ra flow này trước khi tìm được bug ở phần 1. Nhưng nói gì thì nói, trong giang hồ vẫn tồn tại một ý niệm là có CVE thì lời nói uy tín hơn, và việc này cũng làm cho timeline hợp lý hơn vì theo đánh giá cá nhân, đây không phải là một bug “xàm”… Timeline thật sự sẽ như sau:

  1. Tìm ra và report bug phần 2.
  2. Microsoft chuyển qua Online Service và không đánh CVE (Trái đắng).
  3. Cay cú, tiếp tục tìm bug để có chỗ đứng trên “đấu trường danh vọng”.
  4. Tìm ra và report bug phần 1 (có CVE đầu tay).
  5. Quả ngọt – Microsoft quyết định trao bounty và bonus vì những báo cáo chuẩn mực của bug phần 2 😊.

Tôi không rõ lý do việc microsoft từ chối đánh CVE cho bug này, có thể là do dup mà chưa patch hết. Nhưng dù thế nào đi nữa, con số 26 như một happy ending cho chuỗi ngày dài đau khổ cùng SharePoint.

Leave a Reply

Your email address will not be published. Required fields are marked *