A Quick Look at CVE-2021–21985 VCenter Pre-Auth RCE

A Quick Look at CVE-2021–21985 VCenter Pre-Auth RCE

·

12 min read

Mấy ngày gần đây thặc là những ngày tháng nặng nề đối với mình,

Khi mà trong vòng nửa tháng mà có tới 5–6 cái critical patch liền … Diff patch đến tr4m c4m cmnl,

Vào buổi sáng ngày 25/5, Vmware release bản vá, trong số các bug được vá lần này có 1 lỗ hổng Critical với CVSS lên tới 9.8.

Một lỗ hổng mà được gán số tới 9.8/10 thì đồng nghĩa nó là 1 lỗ hổng Pre-Auth RCE, có thể xiên server trong vòng 1 nốt nhạc mà ko cần phải chờ ạt min login vào hay gì cả. Hiểu được tầm quan trọng như vậy, có lẽ không chỉ mình mà nhiều bên khác cũng cắm đầu vào đi mổ xẻ patch và tìm kiếm PoC cho nó.

Cuối cùng, thật đáng buồn thay mình lại ko phải là người tìm ra nó đầu tiên, một anh chàng người tàu nào đó đã tìm ra trước và public nó. Mình chỉ là người đi sau cải thiện độ tiện lợi và ổn định của PoC này thôi!

PoC đầu tiên được đăng tại: https://www.iswin.org/2021/06/02/Vcenter-Server-CVE-2021-21985-RCE-PAYLOAD/

Trong quá trình cải thiện độ ổn định của nó, có một số thứ hay ho nên mình quyết định vẫn sẽ viết bài này để chia sẻ cũng như note lại các thông tin!

.

.

Đầu tiên là một số note của quá trình setup máy ảo lên VMware Workstation!

VCSA hoàn toàn có thể cài độc lập lên vmware workstation mà không cần esxi gì cả, chỉ có một lưu ý tối quan trọng: trong quá trình setup, tuyệt đối không thay đổi mật khẩu mặc định của user root (vmware), mình không biết tại sao nhưng cứ thay đổi mật khẩu này sang mật khẩu khác thì quá trình setup sẽ bị lỗi ở đâu đó mà không rõ nguyên nhân, để yên đó thì lại setup ngon lành mà ko gặp lỗi lầm gì ¯_(ツ)_/¯.

Ở đâu đó mình đã nghe câu:

Discovery requires experimentation!

Để khám phá rõ hơn về bug này thì cần phải debug, mổ xẻ ra mới được.

Mặc dù cũng chạy tomcat, nhưng vsphere-ui lại được khởi chạy thông qua 1 service riêng của VCSA, cần phải chỉnh sửa theo cách riêng để có thể debug!

Trên môi trường linux, file “/etc/vmware/vmware-vmon/svcCfgfiles/vsphere-ui.json” chứa config khởi chạy của vsphere-ui, bao gồm cả các biến môi trường:

Uncomment các dòng sau để enable debug mode:

Sau đó restart service vsphere-ui bằng cách gọi lệnh:

service-control --restart vsphere-ui

Lúc này port debug của service đã được mở, nhưng do config của firewall đã chặn nên chưa thể truy cập từ ngoài vào được:

Config lại firewall cho phép ACCEPT tất cả các gói với lệnh sau:

iptables -P INPUT ACCEPT

#Diff Patch

Phiên bản mình cài đặt để làm lab là VCSA 7.0.2.0, đem diff với VCSA 7.0.2.00100 thì có một số điểm khác biệt như sau:

Authentication Filter đã được thêm vào cho entrypoint “/rest/*

Tại class com.vmware.vsan.client.services.ProxygenController, thêm một đoạn code mới để kiểm tra sự hiện diện của Annotation @TsService trong method sẽ được invoke:

Các method đó có dạng như này nè:

Quên chưa nói về cái ProxygenController, class này làm nhiệm vụ handler tất cả các request đi tới entrypoint “/ui/h5-vsan/rest/proxy/*

Sau đó beanClass, method của bean class này được lấy từ url path, cùng với methodInput được lấy từ json body của request rồi đưa vào method invokeService() để xử lý tiếp:

beanClass sau khi được lấy từ url sẽ được kiểm tra sự tồn tại dựa vào một beanMap đã được định sẵn, nếu không tồn tại trong map này sẽ bị đẩy ra exception luôn:

Các bean này được định nghĩa trong các file .xml của bundle, cụ thể hơn ở đây là trong file “h5-vsan-service.jar/META-INF/spring/base/.xml*”:

Cách define có dạng như sau:

Tương ứng với cách xử lý của ProxygenController phía trên, ta có thể truy cập tới bean này với url như sau:

  • /ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/<method>

Dựa theo cách vá từ vmware, là thêm cơ chế filter cho entrypoint “/rest/*” mình có thể dám chắc là đã có vấn đề gì đó với một trong những bean đã được define, khiến cho nó bị lợi dụng và gây ra vụ RCE này.

Tuy nhiên sau khi export được tất cả các bean có trong entrypoint này thì mình cũng hơi nản 1 chút, có tới 177 class, mỗi class cũng có tầm 10 method, tính cả các method được implement từ class cha nữa thì có tầm 30 method/class => 177*30 = 5310 trường hợp (╯°□°)╯︵ ┻━┻.

Mình đã có gắng bới móc trong vô vọng nhưng không đem lại kết quả gì.

Sau gần một tuần ko ra kết quả gì, mình đành bỏ đó và đi phân tích 1 patch khác cho kịp công việc, …

*this’s what i actually do at that time … 🤣*this’s what i actually do at that time … 🤣

.

.

Loanh quanh một hồi, vài ngày sau thì một mẫu PoC được public trên blog của anh người tàu khựa, tuy nhiên cũng cần có outbound để gửi 1 request RMI ra ngoài, PoC có dạng như sau:

Mình nhanh chóng nhận ra điểm sai sót mà mình đã bỏ qua: đó là chỉ check các bean class mà không check các bean name nữa 😢.

Tìm trong phiên bản 7.0.2 mình đang làm thì không tồn tại bean “vsanProviderUtils_setVmodlHelper ” như trong PoC, sau khi kiểm tra lại thì thấy bean này tồn tại trên các phiên bản <6.7:

Vì trên phiên bản 7.0.2 không có bean này, nên mình sử dụng 1 bean khác được define tương tự để thay thế “vsanQueryUtil_setDataService”:

Còn class xử lý bean này đó chính là MethodInvokingFactoryBean, class này có chứa các method để set và gián tiếp invoke một method khác!

Mô hình kế thừa của class này có dạng sau:

Do đó, từ phía bean bị control, chúng ta hoàn toàn có thể gọi được các method của các class cha mà nó thừa kế: MethodInvokingBean->ArgumentConvertingMethodInvoker->MethodInvoker.

Quay trở lại với PoC của anh người tàu, ta thấy lần lượt các request như sau được gửi đi:

- POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/**setTargetObject**
- POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/**setStaticMethod**
- POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/**setArguments**
- POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/**prepare**
- POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/**invoke**

Nó tương ứng với việc gọi các method sau của class MethodInvoker:

Tham số truyền vào rất đơn giản, đa số đều là dạng String đơn thuần nên không có có khó khăn gì trong quá trình parse dữ liệu bởi ProxygenController.

Tại method MethodInvoker.prepare(), biến staticMethod đã set trước đó được xử lý để set giá trị cho targetClass targetMethod

Để thỏa mãn đoạn code trên thì staticMethod có dạng như sau:

Tuy nhiên ở đây cũng cần lưu ý 1 chút, targetClass không hẳn là dễ dàng trong việc lựa chọn, nó bị limit bởi beanClassLoader:

Do đó mà việc lựa chọn ra 1 staticMethod để lợi dụng cũng thêm khó khăn hơn, ví dụ như method org.springframework.util.SerializationUtils.deserialize() có thể được dùng để deserialize data, nhưng vì bị hạn chế nên không thể được load tại đây:

Sau khi prepare() thì bước cuối cùng chỉ cần gọi invoke() và method sẽ được setAccessible để đảm bảo có thể invoke cả private method và invoke luôn sau đó:

Đó là flow trigger RCE của bug này,

Tuy nhiên như đã đề cập từ đầu, PoC này sử dụng javax.naming.InitialContext.doLookup(), evilRMI các thứ rồi mới RCE được.

Trong thực tế thì các server VCenter này đều nằm trong mạng nội bộ và bị hạn chế kết nối internet đến mức tối đa, gần như không có đường nào khác đi ra ngoài, ngay cả DNS cũng chặn.

Mình bắt đầu lọc ra các package nằm trong beanClassLoader và thử tìm một vài method mới cho phép RCE 1 hit, không cần phải kết nối đi đâu cả.

Một số mục tiêu mà mình nhắm đến:

  • static method cho phép deserialize string hoặc data

  • static method cho phép thực thi lệnh hoặc inject lệnh

  • static method cho phép ghi file

*Attempt 1

Trong đó mình đã tìm được 1 method cho phép deserialize dữ liệu mà chỉ yêu cầu truyền vào 1 mảng byte org.apache.catalina.tribes.io.XByteBuffer.deserialize():

Yeah, tuy nhiên mình đã quên mất một điều quan trọng: hiện tại mình đang trong context bị limit library, làm méo gì có gadget để trigger RCE chứ

Vậy là trường hợp deserialize -> RCE không khả thi lắm

*Attempt 2

Lần này mình tìm được 1 method cho phép ghi file vào vị trí tùy ý: jdk.jfr.internal.Utils.writeGeneratedASM()

Nếu property “SAVE_GENERATED” được set thành true, method sẽ đi vào nhánh ghi file.

Việc này hoàn toàn có thể xử lý đơn giản bằng cách invoke java.lang.System.setProperty():

Tuy nhiên cũng cần lưu ý, SAVE_GENERATED chỉ được set giá trị khi giá trị ban đầu của nó là null, do vậy bắt buộc phải có request setProperty() trước khi gửi request writeFile(). Nếu không thì những việc làm sau đó sẽ đều là vô tác dụng, vì không có cách nào set lại giá trị SAVE_GENERATED này nữa ngoại trừ restart service!

Đã xác định được method và set, nhưng có 1 vấn đề tiếp theo cần xử lý đó là truyền arg làm sao? Method này cần 1 tham số string và 1 tham số dạng mảng byte[].

Tại ProxygenController, dữ liệu sau khi được lấy từ param methodInput sẽ được gọi tới ProxygenSerializer.deserializeMethodInput() để xử lý, mặc dù có tên là deserialize nhưng hoàn toàn không liên quan tới json deser hay java deser gì đâu nha:

Input data được deserialize dựa theo kiểu của các đối số mà method này sắp được gọi:

Các tham số để đưa vào invoke bởi MethodInvoker được set bằng method setArguments(), method này có thông tin đối số là 1 mảng Object[] như sau:

Do vậy mà tất cả các tham số truyền vào đều được “treat-as-Object”, cho dù nó có là String, … hay gì đi chăng nữa.

Như vậy làm sao truyền được mảng byte[] ??? ¯_(ツ)_/¯

!TypeConverter for the rescue

Do không biết làm sao để truyền vào mảng byte[] nên mình quyết định thử truyền bừa vào 1 chuỗi để thay thế:

Đương nhiên là đoạn này sẽ không xảy ra lỗi rồi, cần phải gửi tiếp request prepare() thì chương trình mới xử lý cái arg này cơ! Đặt breakpoint tại đầu method prepare() ta có thể thấy giá trị của 2 arg cần truyền vào đang là 2 chuỗi như này:

MethodInvoker.prepare() tiếp tục xử lý, thực hiện gọi getMethod() với argTypes chính là kiểu của 2 tham số mình truyền vào

Và do lúc này argTypes chưa đúng nên chương trình sẽ xảy ra exception và đi vào nhánh dưới:

Lúc này từ MethodInvoker.prepare() sẽ gọi tới ArgumentConvertingMethodInvoker.findMatchingMethod() để tìm kiếm method phù hợp

Nhưng vẫn null, tiếp tục gọi tới ArgumentConvertingMethodInvoker.doFindMatchingMethod(), method này có dạng như sau:

Method này tiếp tục xử lý và gọi tới TypeConverter.convertIfNecessary() để thực hiện convert argument sang kiểu dữ liệu mong muốn mà method đích đang yêu cầu.

Tiếp tục debug sâu hơn, và dừng tại điểm có Stacktrace như sau:

Biến “editor” có kiểu dữ PropertyEditor, được lấy dựa trên giá trị requiredType — chính là giá trị mà method argument đang yêu cầu, và may thay ở đây PropertyEditor tương ứng của mảng byte[] lại tồn tại, đó là ByteArrayPropertyEditor:

Sau khi qua công đoạn được xử lý bằng ByteArrayPropertyEditor, giá trị mới của argument được chuyển thành như sau:

Như vậy là đã giải quyết được vấn đề làm sao để truyền được kiểu byte ròi nhé

Btw, đây là danh sách một số kiểu dữ liệu có thể được xử lý bởi TypeConvert:

boolean, byte, char, class [B, class [C, class [I, class [J, class [Ljava.lang.Class;, class [Ljava.lang.String;, class [Lorg.springframework.core.io.Resource;, class [S, class java.io.File, class java.io.InputStream, class java.io.Reader, class java.lang.Boolean, class java.lang.Byte, class java.lang.Character, class java.lang.Class, class java.lang.Double, class java.lang.Float, class java.lang.Integer, class java.lang.Long, class java.lang.Short, class java.math.BigDecimal, class java.math.BigInteger, class java.net.URI, class java.net.URL, class java.nio.charset.Charset, class java.time.ZoneId, class java.util.Currency, class java.util.Locale, class java.util.Properties, class java.util.regex.Pattern, class java.util.TimeZone, class java.util.UUID, class org.xml.sax.InputSource, double, float, int, interface java.nio.file.Path, interface java.util.Collection, interface java.util.List, interface java.util.Set, interface java.util.SortedMap, interface java.util.SortedSet, long, short

Xử lý argument xong xuôi, việc tiếp theo cần làm đơn giản chỉ cần invoke thôi:

Và được kết quả như sau:

File thì ghi xong rồi, nhưng rồi làm gì tiếp đây??

Về vấn đề đuôi .class của tên file hoàn toàn có thể xử lý được, mình tìm ra một method có thể cho phép copy file với vị trí và tên tùy ý: org.apache.catalina.manager.ManagerServlet.copyInternal()

Tuy nhiên trên môi trường này không thể ghi shell và thực thi như môi trường windows được, cần phải có một cách nào đó khác nữa mới có thể RCE.

Sau khi xem xét lại mình có phát hiện ra method System.load(), mà theo description, method này cho phép load Native library vào JVM.

Giống với việc load DLL của windows, JNI của java cũng có function JNI_OnLoad() được gọi khi load nó lên. Như vậy thì mọi chuyện đã rõ, dựa vào cơ chế OnLoad này của JNI, mình có thể RCE được bằng cách inject code vào để thực thi.

Việc code cái native library này cũng không khó lắm, sau 1 đêm mày mò mình đã viết xong 1 cái lib để load lên và thành công trong việc RCE, content đơn giản như sau:

  • thực ra là do mình ngu đần code C nên phải gọi ngược về Java để exec code đó (ಥ _ ಥ)

Như vậy đã đạt được mục đích là RCE in one hit, nhưng vấn đề còn tồn tại là làm sao để execute command và lấy kết quả ngay tại http response, mình vẫn đang nghiên cứu thêm!

Tóm lược lại bug này như sau:

  • unauthenticated /rest enpoint

  • tồn tại bean MethodInvoker cho phép invoke static method tùy ý

  • TypeConverter tự động convert dữ liệu sang dạng phù hợp

  • JNI_OnLoad trigger code execution

Sau quá trình phân tích và viết PoC mình học hỏi được khá nhiều thứ bên lề, do nội dung của bài viết có hạn nên có thể sẽ có nhiều thiếu sót.

Mặc dù bài viết đã có full ảnh để đem lại cho người đọc cảm giác “đọc như debug”, nhưng mình vẫn khuyên nên setup và tự debug để nhận biết thực tế theo quan điểm của chính mình thay vì thu lu trong cái bài viết có phần phiến diện của mình!

Btw, do gần đây có một số bạn ý kiến không vui về việc public PoC của mình, nên là mình quyết định sẽ … public nguyên cái bộ gen ra PoC luôn =))).

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

PoC generator: https://github.com/testanull/Project_CVE-2021-21985_PoC

Cảm ơn các bạn đã theo dõi,

Jang