Quick note of vCenter RCE (CVE-2021–22005)

Mấy nay đầu óc đang rối ren, bộn bề trong biển việc, ngồi nghĩ mãi mà ko ra cái tên nào hợp lý cho cái blog này cả, không có tên thì lại không được, thôi đành để vậy cho nó xúc tích!

Cách đây vài ngày, mấy ae trong group có than thở là dạo này giới CyberSec yên bình quá: không có drama hay CVSS 9.8/10 nào cả,

Vừa dứt lời thì mấy tiếng sau, VMware release advisory về mấy quả bug CVSS 9.8 trên vCenter (ಥ _ ಥ)

Khi đọc cái tên của bug này mình cũng hơi có chút confuse: “Trên một product lớn như vậy mà cũng có lỗ hổng “arbitrary file upload” được à ?”

Mang theo câu hỏi lớn đó, mình bắt tay vào phân tích bản vá để xem lỗi ở đâu!

Phần setup debugging thì trong bài trước (https://testbnull.medium.com/a-quick-look-at-cve-2021-21985-vcenter-pre-auth-rce-9ecd459150a5) mình cũng đã nói rõ về cách sửa file config để có thể debug được rồi, các bạn có thể tham khảo lại guide cũ và chỉnh sửa một chút xíu để có thể debug như ý muốn!

#THE PATCH

Trong advisory của Vmware, họ có cung cấp cả một cái P̶o̶C̶ Workarounds khá là chi tiết,

Chi tiết đến nỗi mà có thể sử dụng nguyên cái request đó để làm PoC luôn ¯\_(ツ)_/¯

.

.

Thực ra đó là nói vui thôi, chứ để RCE được thì câu chuyện không phải là một đường thẳng như vậy.

Hướng đi ban đầu của mình là xem cái script Workarounds, tuy nhiên ngay sau đó đã nhận ra đây là một hướng đi sai lầm. Đường đi ngắn ko có nghĩa là sẽ tới đích, script workaround chỉ thực hiện xóa các endpoint liên quan tới service vmware-analytics như sau:

Dựa vào những thông tin từ workarounds, có thể nhìn ra ngay được bug đầu tiên trong bản vá này!

#BUG 1 — Arbitrary File Creation (Required CEIP enabled)

Dựa vào workaround, có thể thấy được tại các endpoint “/analytics/telemetry/ph/api/hyper/send” và “/analytics/ph/api/dataapp/agent” có điều gì đó nguy hiểm, trong số này chỉ có endpoint “/analytics/telemetry” là có thể access trực tiếp do config của service RhttpProxy:

Endpoint “/analytics/telemetry” được handle bởi class AsyncTelemetryController

Khi gọi tới endpoint /analytics/telemetry, một “TelemetryRequest” được tạo từ các tham số đã truyền vào qua HTTP, bao gồm: collectorId, collectorInstanceId, …

Tiếp tục đi vào TelemetryService.processTelemetry(), tại đây TelemetryRequest vừa được tạo ở trên sẽ được submit vào một hàng chờ và execute ngay sau đó, tại TelemetryRequestProcessorRunnable.run():

Sau khi đi vào TelemetryLevelBasedTelemetryServiceWrapper.processTelemetry(), server thực hiện getTelemetryLevel() từ các tham số collectorId, collectorInstanceId được truyền vào:

Theo flow hiện tại của chương trình, DefaultTelemetryLevelService.getTelemetryService() sẽ tiếp tục được gọi để lấy TelemtryLevel. Đoạn code xử lý như sau:

Ở đây ta có thể dễ dàng thấy được: nếu như tính năng CEIP bị disable thì chương trình sẽ luôn trả về Level của Telemetry là OFF!

Thực hiện lấy level xong và trở về với TelemetryLevelBasedTelemetryServiceWrapper.processTelemetry(), ta có thể thấy, nếu như TelemetryLevel = OFF thì server sẽ không tiếp tục xử lý request nữa và return luôn.

Do đó mà bug này có một yêu cầu đặc biệt là phải enable tính năng CEIP để có thể thực hiện được tiếp!

Giả sử như môi trường của chúng ta có enable CEIP, server tiếp tục đi vào nhánh LogTelemetryService.processTelemetry(), đoạn code xử lý tại đây chỉ đơn thuần là log lại cái TelemetryRequest vừa truyền vào, với nội dung của log chính là body request:

File log được lưu tại /var/log/vmware/analytics/prod/_c_i<instance name>.json

Và do là filename có chứa cả collectorId cùng với collectorInstanceId nên ngay khi nhìn thấy đoạn này, mình đã nghĩ tới trường hợp có thể thêm các ký tự “../” để path traveral và tạo file tại một folder khác tùy ý.

Tuy nhiên khi thử request đầu tiên thì không thấy hiện tượng gì xảy ra cả 🤷‍♀️, log ko được tạo, và cũng ko có một lỗi gì trả về cả.

Tiếp tục debug vào đoạn “this._logger.info()” để xem rõ hơn, F7, F8 một hồi thì đến được vị trí có fileName của logger sau khi bị path traversal như sau:

Trong “/var/log/vmware/analytics/prod/” không có folder nào tên là “_c_i”, do đó mà với path traversal “/var/log/vmware/analytics/prod/_c_i/../../../../../../../../../tmp/test1111111.json” cũng sẽ bị trả về not found.

Đoạn path traversal này sẽ chỉ hoạt động khi folder trước đó cũng tồn tại:

May mắn thay, sau một hồi mình có fuzz linh tinh thì cũng đã có thể create folder mới trên server (thực ra thì request ban đầu của mình hơi khác, sau đó có đc góp ý với request mới gọn hơn):

Với _c=”” và _i=”/<name>”, full path lúc này sẽ là:

/var/log/vmware/analytics/prod/_c_i/11247.json

Khi Logger call tới RollingFileManager.createManager(), server sẽ kiểm tra sự tồn tại của các folder parent, ở đây là “_c_i”, và do folder này chưa tồn tại nên sẽ được tạo mới ngay sau đó.

Với folder “_c_i” đã được create thì request path traversal để tạo file tùy ý phía trên đã có thể thực hiện thành công:

Tuy nhiên vậy chưa phải là hết, vấn đề vẫn còn rất nan giải,

Nội dung và path của file có thể sửa đổi tùy ý, nhưng bắt buộc ở tên file phải có đuôi “.json”, không thể write web shell và thực thi được!

Rất nhiều người đã tìm ra bug trên, nhưng cũng bị stuck tại đây và không thể RCE được, mình cũng là một trong số đó,

Về phương pháp để có thể RCE được bằng bug này mình xin phép không đề cập ở đây do cam kết với người chia sẻ, cũng như làm phần mở dành cho bạn đọc nghiên cứu thêm ( ͡° ͜ʖ ͡°).

.

.

#BUG 2 — Arbitrary Web Shell Creation

Cảm thấy workaround là chưa đủ để tìm ra một cái bug hoàn hảo: RCE mà không cần điều kiện gì ngặt nghèo, mình đành down patch về và diff,

Một số thay đổi nhỏ giữa 2 bản vá như sau:

Đa số trong đó đều là thêm các cơ chế kiểm tra collectorId, tên file … để tránh bị write shell.

Trong đó thì có bug tại AsyncTelemetryController mình đã nêu phía trên rồi, chắc hẳn bug còn lại đang nằm trong “DataAppAgentController”!

Trong bản mới, endpoint “/dataapp/agent” với action=collect đã hoàn toàn bị xóa bỏ:

À chết quên, vẫn còn một vấn đề rất quan trọng mình chưa có đề cập:

Trong khai báo của rhttpproxy, không có khai báo nào cho phép access tới endpoint “/analytics/ph/api/dataapp/agent”, hiện tại chỉ có thể access endpoint này từ local qua port 15080 mà thôi,

🤷‍♀️Nếu chỉ như vậy thì tại sao phải patch gắt vậy chứ, có ai access được đâu??? Chắc hẳn phải có cái gì đó để bypass và có thể access được rồi…” Mình thầm nghĩ…

Quay trở lại với advisory, trong số bug được vá lần này, có thêm một bug nữa là CVE-2021–22017 — rhttpproxy bypass, và cũng được report bởi tác giả đã report CVE-2021–22005 ( ͡° ͜ʖ ͡°)

Như vậy là chắc chắn ròi, ông tác giả này đã tìm ra cách bypass rhttpproxy, và kết hợp với lỗ hổng tại endpoint “/dataapp/agent” tạo thành một chain RCE-In-Onehit … ( ͡° ͜ʖ ͡°) #khalachackeo

Mình nghĩ vậy, và đồng đội cũng nghĩ vậy, nên là mọi người chia ra để nghiên cứu.

Ban đầu thì Peterjson có nghiên cứu về bug của Envoy, cho phép bypass url filter các thứ: https://github.com/envoyproxy/envoy/security/advisories/GHSA-4987-27fx-x6cf, Nghe có vẻ khá là uy tín nhưng khi áp dụng vào môi trường này thì lại không ăn thua(ಥ _ ಥ)

Đang bơi trong sự tuyệt vọng thì đột nhiên mình chạm vào một giao diện khá là quen thuộc …

( ͡° ͜ʖ ͡°)( ͡° ͜ʖ ͡°) Đó chẳng phải là tomcat sao …

Nghĩ lại thì từ đầu tới giờ mình quên mất tới trường hợp kinh điển của tomcat mà đó giờ vẫn được dùng để bypass proxy filter: “..;/” thần thánh.

Và đúng như dự đoán, “..;/” chính là chìa khóa:

Vấn đề làm sao để access vào endpoint đã được giải quyết, giờ chỉ là làm sao để có thể write được file nữa thôi.

Mình tập trung vào phần code đã bị xóa đi trong bản vá mới: dataapp collect, và đặc biệt focus đi vào nhánh agent.collect()

Quá trình debug ở đây khá là lâu và lắt léo, sau một thời gian F7, F8 triền miên, và bằng một phương pháp tâm link nào đó, mình dừng lại tại ResourceItemToJsonLdMapping.map():

ResourceItemToJsonLdMapping.evaluateMappingExpression() sau đó tiếp tục call tới Velocity để evaluate() template:

Với “this._mappingCode” là giá trị có thể control được:

Http Request:

Stack trace:

Từ đây ta có thể evaluate() một template tùy ý, nhưng như vậy là chưa đủ để RCE với những paylak thông dụng:

Phiên bản Velocity mới đã có một số blacklist để chặn gọi tới các method của class “java.lang.Class”:

Do đó ở đây không thể trực tiếp gọi Class.forName() hay một method nào đó của Class để execute được.

Mình có tham khảo một số phương pháp của @pwntester (https://i.blackhat.com/USA-20/Wednesday/us-20-Munoz-Room-For-Escape-Scribbling-Outside-The-Lines-Of-Template-Security.pdf), đại khái trong đó abuse việc load ClassLoader thay vì Class.

Tuy nhiên trong context hiện tại của vCenter không có các variable thích hợp như vậy, chỉ có một số var như sau:

Hy vọng cuối cùng được đặt vào 26 context có sẵn này,

Và may mắn sao, một trong số đó đã phát huy tác dụng, và có thể được sử dụng để write file tùy ý. Đó chính là “GLOBAL-logger”:

Dựa vào mỗi ảnh mô tả trên có thể bạn đọc cũng đã hình dung ra mình sẽ làm gì với nó rồi ( ͡° ͜ʖ ͡°).

Đây là các step lợi dụng $GLOBAL-logger để write shell:

Step 1: set log path sang một file tùy ý,

Step 2: write web shell qua chức năng logging

Step 3: close file log và trả lại giá trị cũ của log file name

File tạo ra sẽ có dạng như sau:

Để PoC chạy mượt mà thì cần thêm một số bước nữa, tuy nhiên theo mình như vậy là đủ để bạn đọc có thể hiểu và tự tạo cho mình một PoC rồi.

Do giới hạn về thời lượng cũng như trí nhớ nên nếu bạn đọc thực sự có nhu cầu học hỏi thì nên setup lại lab và thực hiện để hiểu rõ hơn những bước mình đã bỏ qua, như vậy sẽ nhớ hơn là cưỡi ngựa xem hoa!

Chúc bạn may mắn!

PoC video: https://www.youtube.com/watch?v=WVJ8RDR7Xzs

PoC script: https://gist.github.com/testanull/c2f6fd061c496ea90ddee151d6738d2e

__Jang__

asdasd asdasdasd asdasdasd

asdasd asdasdasd asdasdasd