(0.5 day) Micro Focus Operations Bridge Manager Pre-Auth Deserialization to RCE

(0.5 day) Micro Focus Operations Bridge Manager Pre-Auth Deserialization to RCE

·

15 min read

//Vì dường như là vendor không quan tâm lắm về cái 0day này nên mình quyết định uncensor và publish toàn bộ content,

Mấy ngày nay mình có lượn lờ quanh các trang mạng để tìm kiếm nguồn cảm hứng mới cho công việc, hết twitter, reddit, ròi tới facebook …

Tình cờ có liếc qua published advisories của ZDI thì thấy khá nhiều bug về deserialization được publish, đếm sơ sơ thì từ đầu năm đến giờ cũng đã có tới ~100 bug về deserialization được report cho ZDI:

Hmm, nếu vậy thì con số bug ngoài thực tế (mà ko đc report cho ZDI) có thể sẽ còn lớn hơn gấp 4–5 lần thế này.

Vì không phải researcher nào cũng có hứng với ZDI, nhiều người họ lấy việc tìm bug làm thú vui, public PoC trước khi có patch lại là 1 thú vui bệnh hoạn hơn nữa =))).

Xem xét kỹ hơn thì có 1 điểm đáng chú ý ở đây: trong số 100 lỗi về deserialization thì có tới 40 lỗi nằm trên 1 product: Micro Focus Operations Bridge Manager

Điều này làm mình khá tò mò và thầm nghĩ trong đầu:

Quèo, tính sơ sơ mỗi bug ZDI nó trả 750$, 750x40 = 30k$ cmnr.”

Làm kinh tế không khó với ZDI 🤣🤣.

Thế là lại bắt đầu công cuộc setup lab và debug thôi!

#SETUP

Cũng khá may mắn cho mình, Micro Focus Operations Bridge Manager (OBM) cho phép download bản trial tại đây. Cần 1 acc login vào để down, nếu lười reg thì có thể dùng acc share tại bugmenot như mình đã làm tương tự với ibm và oracle ¯_(ツ)_/¯.

Một lưu ý nho nhỏ khi setup là nên follow guide tại đây, nếu ko sẽ gặp nhiều rắc rối mà ko rõ nguyên nhân, một số điểm chính như sau:

  • Disable UAC trong registry

  • Sử dụng 1 cái domain cho lab vì OBM ko chấp nhận sử dụng ip

Target này khá là nặng và ngốn rất nhiều tài nguyên, cấu hình recommend cho lab để đảm bảo debug ổn định: 16GB RAM, 8 core, 100GB disk. Công cuộc setup mất khoảng 40–50p gì đó, khá là lâu, để khởi động cũng mất thêm 15–20p nữa 😂.

Sau khi install xong thì sẽ tới đoạn basic setup, tại đây mình có 1 số lưu ý nho nhỏ sau:

  • Tại TLS Setup, bỏ tick Enable HTTPS đi, bởi vì sẽ bị lỗi linh tinh về cert hay gì đó, mình fix mãi ko đc nên đành disable nó đi.

  • Tại đoạn set OBM URL, nhớ điền đúng domain đã đặt tên cho ip của lab, như trong lab của mình là example.com A.K.A 192.168.139.134.

Đoạn này nếu để nguyên IP thì sau đó OBM sẽ vẫn hoạt động nhưng 1 số chức năng lại yêu cầu FQDN mới cho sử dụng, cần cân nhắc khi setup!

Sau khi setup xong thì đã có thể start OBM rồi, quá trình start diễn ra trong khoảng 15–20p, có thể check status với Operations Bridge Manager Status:

Chờ startup xong thì chúng ta có được thành quả như sau:

.

.

.

#Debugging …

Phần này là hướng dẫn cho newbie, nếu đã biết rồi bạn có thể bỏ qua và xem tiếp tại phần #The known bugs và #The unknown bugs *phía dưới

Chợt nhận ra sau khi mình publish bài về CVE-2020–14882, rất nhiều bạn có nhắn tin hỏi mình, đại khái là làm sao để debug …

Vậy thì sau đây mình sẽ trình bày về quá trình từ tìm đối tượng để debug, tới setup debug với intellij như thế nào …

Sẽ lấy đây làm bài học cơ bản để cho các bạn bắt đầu nghiên cứu về lỗ hổng trên Java có thể tham khảo và biết cách setup debug.

  1. Setup phía server

1.1. Xác định đối tượng cần debug

Tiếp tục với target OBM, sau khi setup xong thì chỉ biết được là OBM chạy ở port 80, check process nào đang handle port 80 với Process Hacker:

Binary httpd.exe này chính là Apache HTTP Server, được đặt tại: C:\HPBSM\WebServer\bin, vậy có nghĩa là file config của nó sẽ chỉ ở loanh quanh đâu đó bên ngoài thôi. Như trong lab của mình thì config được lưu tại: C:\HPBSM\WebServer\conf.

Do hiện tại chưa có thông tin gì về vị trí của các file webapps, mà Apache lại đang handle tất cả các request ở port 80, có thể nó hoạt động như 1 proxy, hoặc nó xử lý trực tiếp servlet, vậy nên đọc file config là cách tốt nhất để tìm ra chúng.

Sau một hồi đọc file config, để ý với các Alias:

Có thể thấy các file war được deploy đều nằm trong C:\HPBSM\AppServer\webapps\:

Tiếp tục đọc httpd.conf, tại dòng 844 thì include thêm các file conf tại folder “conf.d

Trong này chỉ có duy nhất 1 file “load_mod_jk.conf

Theo như thông tin tìm được trên wikipedia, mod_jk là 1 module của Apache cho phép kết nối Tomcat servlet với Apache, hoặc IIS qua giao thức AJP (port 8009):

Trong trường hợp này, process của Jboss.exe đang listen port 8009

=> Web server của OBM = Apache + Jboss

Trong đó Jboss đóng vai trò là thằng xử lý chính các webapps, để debug thì chúng ta cần phải sửa startup param của Jboss.

Kiểm tra trong Process list thì Jboss.exe được spawn ra từ 1 script tại: C:\HPBSM\bin\start_as.bat

File này chỉ đơn thuần làm nhiệm vụ thiết lập jboss env, sau đó tiếp tục gọi “%JBOSS_HOME%\bin\standalone.bat” để start Jboss:

Ở đầu file startup “standalone.bat” của Jboss có hướng dẫn là chỉ cần thêm option “--debug” là sẽ có thể debug được.

Tuy nhiên, để có thể làm với tất cả các target java khác thì mình sẽ hướng dẫn setup debug bằng tay.

Trước tiên là cần tìm biến môi trường có chứa các tham số startup của Java process, biến môi trường này thường có tên là JAVA_OPTS:

Thêm vào đâu đó trong startup script dòng sau (với Linux thì các bạn tùy biến lại 1 chút):

set "**JAVA_OPTS**=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,address=**5005**,server=y,suspend=n"

Thêm vào đâu cũng được, miễn là trước khi Java process được gọi:

Thực hiện stop và start lại process Java (ở đây là Jboss) để load debug config vừa thêm vào, các bạn có thể để ý ở Command line output có dòng “Listening for transport dt_socket at address: 5005”, nghĩa là đã setup debug thành công!

Vậy là đã setup debug ở phía server thành công, việc tiếp theo là setup ở phía client, hay là ở máy của người đang debug!

1.2. Xác định các resource cần debug

Như đã đề cập ở bên trên, các file war của webapps được deploy tại C:\HPBSM\AppServer\webapps

Đối với các webapp này thông thường sẽ có cấu trúc chung như sau (như weblogic, tomcat, … cũng tương tự):

Trong đó:

  • lib/ chứa các library mà web này sẽ sử dụng

  • classes/ chứa các file class của web app này

  • web.xml là file config của webapp

  • có thể sẽ có thêm nhiều file config khác trong cùng folder

Một ví dụ cụ thể:

Trong đó, đáng quan tâm chính là folder “WEB-INF/lib/”, để setup debug ở phía client, cần phải bê nguyên cái folder này ra máy client.

2. Setup debug phía client

Phía client, mình thường sử dụng IntellIJ để debug, nó khá là tiện lợi và cung cấp License free cho sinh viên (😘😘).

Để debug thì Create 1 project Java như bình thường:

Create xong project thì tiếp tục bấm vào File > Project Structure > Libraries

Bấm vào dấu “+” để thêm các library của webapp vào đây

Ở cửa sổ chọn library thì nhớ bôi đen chọn tất cả các jar file nhé, nếu chọn mỗi folder không sẽ ko có tác dụng, sau đó hỏi gì cũng cứ Ok, Next next thôi:

Import library xong thì phải chờ mất 1 lúc để IntellIJ index các file vừa add vào, (SSD recommended):

Tiếp theo chọn Add Configuration ở trên Tool bar

Chọn thêm 1 config Remote như sau:

Với Host chính là địa chỉ ip của server đang cần remote debug

Setup xong và có thể thử lại bằng cách Start Debug, nếu response tại console là như vậy thì việc setup đã thành công!

.

.

.

Quá trình setup debug trong thực tế, với mỗi đối tượng khác nhau thì có thể sẽ khác nhau, và mất nhiều thời gian hơn 1 chút. Cần phải dựa vào cách hoạt động của từng target mà tùy chỉnh sao cho phù hợp.

#The known bugs

Trên ZDI có tới 40 bugs, nhưng mình pick đại lấy 1 cái để xem mặt mũi nó ra sao, ở đây mình chọn bug tại SAMDownloadServlet

Bug này được giới thiệu là sử dụng Deserialization để từ 1 authenticated user có thể RCE được:

Search qua 1 vòng ở các webapp của OBM thì thấy SAMDownloadServlet thuộc webapp: opr-web.war, có servlet là com.hp.opr.legacy.sitescope.servlet.SAMDownloadServlet

Tìm tiếp servlet-name SAMDownloadServlet trong web.xml của opr-web thì có thể thấy đang url-pattern để access là “/legacy/topaz/sitescope/conf/download

Đối với các target khác, cũng có thể dựa vào tag servlet-name trong web.xml để tìm được:

  • servlet nào đang handle

  • url-pattern như thế nào thì truy cập được servlet đó

Quay trở lại IntellIJ, sử dụng tổ hợp phím Ctrl + N để tìm được class theo tên, ở đây là SAMDownloadServlet, nằm trong file opr-legacy.jar:

  • Nếu như không tìm thấy được class dựa vào tên thì có thể bạn đã import thiếu library, cần phải tìm thêm.

Đoạn code xử lý của SAMDownloadServlet khá đơn giản như sau:

Đọc sơ qua cũng biết được servlet này nhận vào input từ phía client và thực hiện deserialize ngay sau đó.

Quá đơn giản phải ko ¯_(ツ)_/¯, ai mà nghĩ được trong 1 product Enterprise sẽ xử lý như vậy. Ez money rồi!

Lúc đầu mình cũng ko tin là ez như thế, xem tiếp các servlet khác thì cũng xử lý stupid tương tự, ví dụ như RegistrationServlet:

Để confirm bug, mình tạm sử dụng gadget URLDNS, kết quả như sau:

Xem xét các servlet khác được khai báo trong web.xml của opr-web thì cũng có kha khá các entrypoint hứa hẹn có thể sử dụng để Deserialize được, chỉ có điều tất cả đều yêu cầu authenticated!

#The unknown bugs

Sau khi xem xét một hồi và khẳng định rằng tất cả các entrypoint tại opr-web đều yêu cầu authen, mình chuyển qua xem các web app khác, đối tượng có tiềm năng nhất đó là webapp site.war. Web app này cũng tương đối phức tạp, có nhiều chức năng, và thú vị hơn là ở webapp này có 1 số unauthen entrypoint

Trong số đó, có 1 unauthen entrypoint đáng chú ý: /kpiQueryServiceProxy, được handle bởi RemoteProxyServlet

Chức năng của RemoteProxyServlet cho phép nhận vào input từ phía client và thực hiện deserialize ngay sau đó:

Để confirm thì lại tiếp tục sử dụng gadget URLDNS

Như vậy là đã đủ điều kiện để report cho ZDI và đem về 1 CVE 9.8/10 rồi, nhưng …

Và rồi mình quyết định không report nữa, vì dù sao bọn này cũng làm ăn khá lề mề, mình thì không chờ được nửa năm để nhận response của họ ¯_(ツ)_/¯.

Đành tiếp tục mổ xẻ, tìm hiểu nguyên nhân và viết gadget RCE cho bug này, vì hiện tại mới chỉ có thể PoC ở mức dns request, các known gadget đều đã bị patch cả rồi!

#The root cause

Vấn đề đầu tiên cần làm rõ: tại sao có url bị check authen, còn 1 số thì lại được bypass!

Chày cối đặt breakpoint, step and repeat, cuối cùng cũng đã tìm được vị trí mà server kiểm tra, xác định url có phải authen hay không,

Tại LWSSOValidationUtils.validate(), method này gọi tới isNonsecureURL() để xác định xem url hiện tại có phải là non-secured request hay không (cũng có thể hiểu là có nên để request này unauth hay không). Nếu đúng thì nó sẽ set thêm attribute “LWSSO_REQUEST_SECURED_ATTRIBUTE” cho request hiện tại, để cho phép request này unauthen:

Bên trong method isNonsecureURL() có 1 list url pattern để xác định, và entrypoint “/kpiQueryServiceProxy” cũng nằm trong đó:

List này bao gồm các pattern sau (regex):

./topaz/loginBanner.jsp. ./topaz/generalError.jsp. ./topaz/jsps/act/ui/components/popupmessage/popup.jsp. ./topaz/blank.jsp. ./topaz/app/usermanagement/general/TopLogoutInvoker.jsp. ./LoginValidation.jsp. .topaz/TopazSiteServlet. ./HttpSSOlogin.jsp. ./services/EntityNotificationPort. ./bam/BAMOpenApi. ./authorizationcontrol. ./authorizationmanagment. ./ITU/MWCWizard.do. ./LB_Verify.jsp. ./verifyIDM.jsp. ./redirectToLogin.jsp. ./openapi/OpenAPI.jsp. ./SsoConfigurationErrorPage.jsp. ./services/technical/time. ./mobileConsole.do. .topaz/services/technical. ./topaz/jsp/aw/awlogin.jsp. ./topaz/kpiQueryServiceProxy. ./topaz/JavaScriptServlet. ./topaz/ws/urest/v1/authenticated_user. ./topaz/api/v1/server_availability.

Tuy nhiên, cách check pattern của OBM lại dựa theo regex, việc bypass cơ chế kiểm tra này hoàn toàn đơn giản, ví dụ:

/topaz/authenticated_required_url**/mobileConsole.do**

Do url chứa pattern whitelist là “.*/mobileConsole.do”, request này sẽ lừa được cơ chế kiểm tra và vẫn được xác định là 1 unauthenticated entrypoint!

Tiếp tục có thêm vấn đề cần xem xét: làm thế nào mà lại gọi được tới LWSSOValidationUtils.validate()

Xem lại call stack và để ý kỹ sẽ thấy một số điểm như sau:

Các method doFilter() của Filter Class được gọi lần lượt, nếu để ý thì trong file config web.xml cũng có khai báo các filter này theo thứ tự:

Sau khi search qua 1 vòng thì mình biết được cái Filter này được định nghĩa trong config file, và cứ mỗi request sẽ gọi tới method doFilter() của các class này. (For more info: https://www.journaldev.com/1933/java-servlet-filter-example-tutorial)

  • Vấn đề này có thể sẽ hơi rối, mình sẽ giải thích vào 1 dịp khác!

#The RCE gadget

Quay trở lại với bug pre-auth deserialization tại entrypoint /kpiQueryServiceProxy.

Hiện tại PoC mới chỉ ở mức URLDNS, nguyên do như đã nói ở trên là các library trong này khá mới, một số class lại còn bị restrict bởi Jboss nên khó mà có thể RCE được bằng các gadget đã biết! Nhưng ko sao, đó giờ mình làm chuyện khó quen rồi 👌,

Một số lỗi xảy ra khi thử các known gadget như sau:

  • CommonsBeanutils1: TemplatesImpl not found

Điều này khá là dị, mình kiểm tra thì thấy rõ ràng là Jboss vẫn dùng java runtime của sun, vẫn tồn tại TemplatesImpl trong rt.jar như thường, tại sao lại có lỗi như thế kia …?

Lại chày cối debug tiếp thì mình biết được rằng các app của Jboss không sử dụng trực tiếp rt.jar của jre, thay vào đó classpath của app chỉ dựa vào các Jboss module được cung cấp!

Với trường hợp OBM hiện tại, các module này nằm trong C:\HPBSM\application-server\modules\system\layers\base

Các folder này được coi như là các package của java, khi load tới package nào thì Jboss sẽ lấy lần lượt các module trong đó, file module.xml sẽ xác định vị trí library cần load lên, ví dụ như Apache Commons Collections sẽ có dạng như sau:

Quay trở lại với trường hợp load class: com.sun.org.apache.xalan.internal.xsltc.tra..

Trong folder com/sun/ không hề có folder org, do đó khi Jboss lookup module sẽ không thể tìm thấy được TemplatesImpl mà ta mong muốn 🤨.

.

.

Thử rất nhiều gadget, cho tới khi thử tới gadget AspectJ shell drop (có đề cập trong bài viết này) thì lại may mắn thay: trên lý thuyết thì tất cả các class cần thiết cho gadget đều tồn tại trong classpath.

Gadget này yêu cầu các library sau: org.aspectj.weaver, commons-collections

Tuy nhiên khi gen payload và gửi tới server thì lại gặp ngay lỗi sau:

Server báo không tìm thấy class của aspectj weaver: org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap

Lỗi này trông khá lạ,dài dòng hơn so với những lỗi Class Not Found của gadget khác.

Rõ ràng là khi kiểm tra, SimpleCache có trong classpath của web app. Để chắc chắn hơn, mình tạo lại object của SimpleCache và gửi cho server deserialize object này, kết quả như sau:

Như thế có nghĩa là class này CÓ tồn tại trên classpath, sao lại bị not found khi deserialize theo chain đc nhỉ 🤔.

Đọc lại stacktrace của exception khi báo class not found, server đẩy ra exception ngay trong 1 chain của gadget: LazyMap.readObject()

Đoạn code xử lý của LazyMap.readObject() rất đơn giản như sau:

Có lẽ dòng gây ra lỗi chính là đoạn “this.map = (Map)in.readObject();”, (trong đó theo lý thuyết thì this.map sẽ có giá trị của object SimpleCache đã tạo).

Vì khi deserialize, in.defaultReadObject() được gọi đầu tiên để tạo lại các serializable field của Object, đối với các trường hợp đặc biệt: ví dụ như transient field không được serialized thì sẽ phải thực hiện tạo lại bằng custom code của class đó, trong case hiện tại thì field this.map cũng là 1 transient field, và được tạo lại theo đúng quy trình!

Dựa theo các dữ liệu bên trên, mình đặt ra 1 giả thuyết như thế này:

  • Object của SimpleCache được gán vào this.map, trong khi this.map lại là 1 transient Field

  • Khi deserialize, java runtime “không thực sự quan tâm” tới sự tồn tại của giá trị trong transient Field

  • Do đó đã không load các library cần thiết vào classpath

Với 80% tự tin rằng giả thuyết trên là đúng, mình nhanh chóng nghĩ ra 1 cách khắc phục như sau:

Nếu như khi deserialize, class SimpleCache được load vào classpath trước, sau đó mới thực hiện deserialize gadget AspectJ ban đầu thì có thể mọi chuyện sẽ suôn sẻ …

Vậy thực hiện như thế nào?

Mình tìm được trong library một vài class thú vị, ví dụ như “W_ObjectResultImpl”:

readExternal() sẽ được gọi khi deserialize(gần giống như readObject()).

Class này có gì đặc biệt?

Để ý 1 chút sẽ thấy ở các dòng từ 86 trở xuống, liên tiếp gọi in.readObject()

Dựa theo giả thuyết của mình, thì trong này mình sẽ sắp đặt cho this._ruleParameter = Object của SimpleCache, this._dv1 = real gadget.

Khi deserialize, các Object cũng lần lượt được reconstruct theo thứ tự sắp xếp trong readExternal(), dựa vào đó để load các library cần thiết lên classpath trước, tránh bị class not found như ban đầu.

Nói thì dài, nhưng có thể minh họa lại như sau:

Sau khi gen payload và gửi lên server được kết quả như sau:

Server trả về lỗi ClassCastException mình biết rằng, tới 90% là gadget này đã thành công rồi,

Để confirm lại một lần nữa, mình đặt breakpoint tại điểm sink của gadget: org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap.put()

Vậy là đã reach tới sink, file “ahi.txt” đã được tạo tại folder tùy ý:

Tuy nhiên do bị check bởi isNonsecureURL(), cần phải đăng nhập mới truy cập được file.

Quay trở lại với đoạn check URL bên trên, url được check bằng các pattern regex, ví dụ: “./topaz/blank.jsp.**”

Như vậy thì việc bypass lại trở nên quá đơn giản, với pattern bên trên, thì url “/topaz/blank.jsp.jsp” cũng có thể thỏa mãn điều kiện check.

Drop lại file shell mới với tên blank.jsp.jsp, thì đã có thể RCE thành công:

PoC video: https://youtu.be/X6p7Mtd4QgE

PoC Generator: https://gist.github.com/testanull/5e9ac445403f890be785308353abebab

#Kết

Mình không chờ đợi được mail reply từ phía ZDI nên đành show & public PoC luôn

Quá trình debug cũng phát hiện và học hỏi được thêm khá nhiều thứ, khuyến khích các bạn trẻ nên lab và debug để hiểu rõ hơn.

Bài viết lần này khá là dài, rất cảm ơn các bạn đã đọc hết tới đây,

Jang