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
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
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
Đợ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
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
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)
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
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
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()
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
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
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
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
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
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.
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.
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--
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--
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 patha
, patha/b
, patha/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á patha/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