CyberCon 2025 SafeUpload Web Challenge
CyberCon 2025 SafeUpload Web Challenge
Tổng quan challenge

Mở challenge lên thì ta thấy nó cấp cho ta một giao diện dùng để upload file nên nghi ngờ ban đầu sẽ là web này dính lỗ hổng file upload.
Tiến hành thử upload lên file php với nội dung:
1 |
|

Có vẻ như đã dính filter của bài có thể thấy nó đã xoá đi file mình upload lên, bây giờ ta thử upload 1 file php nhưng không có nội dung.

Vẫn là file đó nhưng không có nội dung thì hoàn toàn có thể upload bình thường lên hệ thống. Bây giờ ta tiến hành review source code của bài.
Phân tích source code
Source code challenge có 3 files chính đó là index.php, upload.php và i_dont_like_webshell.yar với index.php sẽ xử lý phần UI UX của web nên ta sẽ bỏ qua file đó và đi với 2 file chính là upload.php cùng i_dont_like_webshell.yar file upload.php sẽ xử lý logic của chức năng upload của bài và i_dont_like_webshell.yar là file rule của yara chịu trách nghiệm làm lớp filter cho chức năng upload.
1 |
|
Đây là phần chịu trách nghiệm xử lý logic chính cho chức năng upload với:
1 |
|
strict_types=1: Bật kiểm tra kiểu dữ liệu.display_errors=0: Không hiển thị lỗi.
Khai báo:- Thư mục lưu file tạm.
- Thư mục lưu file hợp lệ.
- Đường dẫn tới YARA và tập rule .yar.
1 | function four_digits(): string { |
Đoạn này sẽ tạo ra 4 chữ số ngẫu nhiên (0000 -> 9999) để dùng làm tên file tmp.
1 | function ext_of(string $name): string { |
Lấy nguyên phần extension của file từ file gốc.
1 | function bad($m,$c=400){ |
Hàm hiển thị lỗi và thoát chương trình.
1 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') bad('POST only',405); |
Hàm kiểm tra HTTP request và kiểm tra xem có upload đúng file hay không.
1 | $orig = $_FILES['file']['name'] ?? 'noname'; |
Xử lý file upload lên ở đây nó sẽ lấy tên gốc của file được upload lên sau đó lấy đuôi file gốc và gọi đến hàm four_digits để tạo số random từ đó gộp thành đường dẫn tạm thời nó sẽ có dạng /tmp/XXXX.php.
1 | if (!move_uploaded_file($_FILES['file']['tmp_name'], $tmp_path)) bad('save failed',500); |
Di chuyển file vào thư mục /tmp và gán quyền 0644.
1 | usleep(800 * 1000); // 800ms |
Delay 800 giây có vẻ để chống brute force.
1 | $out = []; $ret = 0; |
Chạy yara để kiểm tra file có phải mã độc không nó sẽ ghi kết quả vào $out và mã trả về vào $ret.
1 | $stdout = implode("\n", $out); |
- Gộp đầu ra thành chuỗi.
- Kiểm tra nếu rule tên ‘Suspicious_there_is_no_such_text_string_in_the_image’ có bị match không.
1 | if ($ret === 1 || $hitByName) { |
Xử lý file theo kết quả của yara scan trả về nếu $ret==1 thì file upload trên sẽ bị xoá.
1 | elseif ($ret === 0) { |
Với điều kiện $ret==0 thì sẽ đưa file đó từ /tmp sang thư mục /uploads và hiển thị link để truy cập file đó.
1 | else { |
Trong trường hợp yara trả về khác 0/1 thì sẽ trả về lỗi này.
Debug và POC
Với bài này vì không chắc là liệu bên phía backend có thực thi file đuôi php không nên tôi sẽ tiến hành debug trên docker.

Tiến hành đưa file shell.php vào thư mục /uploads.

Truy cập vào /uploads/shell.php có thể thấy php đã được thực thi nên ta có thể nhận định rằng server có thực thi file đuôi php.
Vậy hướng khai thác bài này ở đây là gì, ở đây sau khi tìm hiểu thì tôi nhận thấy lớp filter của yara khá là dày và sẽ rất khó có thể bypass qua được nên tôi tìm thêm hướng khai thác khác.
Sau khi đọc lại code tôi thấy có dòng:
1 | usleep(800 * 1000); // 800ms |
Ở đây theo tôi hiểu thì trước khi yara tiến hành scan thì sẽ có 1 khoảng thời gian sleep là vào khoảng 800ms hay 0.8 giây, vậy liệu ta có thể lợi dụng khoảng thời ngắn này để làm được việc gì không?
Sau khi tìm hiểu thì có phương pháp TOCTOU (Time-of-check to Time-of-use) là một loại lỗi phổ biến trong các tình huống race condition, nơi có sự không đồng bộ giữa quá trình kiểm tra và sử dụng tài nguyên (hoặc dữ liệu) trong một hệ thống.
Giải thích TOCTOU:
TOCTOU xảy ra khi có một sự khác biệt giữa thời điểm khi một điều kiện được kiểm tra và thời điểm khi điều kiện đó thực sự được sử dụng. Trong một hệ thống nhiều tiến trình (multi-threaded) hoặc có sự truy cập đồng thời (concurrent access), một tiến trình có thể kiểm tra một điều kiện (ví dụ: một file có tồn tại hay không) nhưng trong khoảng thời gian giữa lúc kiểm tra và lúc sử dụng tài nguyên đó, tài nguyên có thể đã thay đổi bởi một tiến trình khác.
Ví dụ về TOCTOU:
Giả sử bạn có một đoạn mã kiểm tra nếu một file tồn tại, sau đó tiến hành sử dụng file đó (ví dụ, đọc nội dung). Nếu trong khoảng thời gian giữa việc kiểm tra sự tồn tại của file và việc sử dụng nó, một tiến trình khác đã thay đổi trạng thái của file (ví dụ: xóa file, thay đổi quyền truy cập file, hoặc ghi đè lên file), thì có thể dẫn đến kết quả không mong muốn hoặc hành vi không xác định.
Vậy bây giờ kịch bản đưa ra sẽ là ta sẽ cố gắng lợi dụng thời gian 800ms đó để có thể thực thi cat flag ra và in nó ra vì như ở trên ta đã thử debug web server hoàn toàn có thể tự thực thi php và ta sẽ cố định tên file sẽ là 0089.php vì như đoạn code đã được phân tích trên tên file khi nó di chuyển vào /tmp sẽ được random nên ta sẽ cố định nó lại và chạy nhiều cặp request nhưng trước hết ta sẽ thử debug.
Ta sẽ tiến hành sử dụng burp proxy cùng với đó là script để thử 1 cặp request GET và POST:
1 | import requests |
1 | import requests |

Tiến hành chạy POST ở đây burp đã bắt được đoạn upload lên bây giờ ta sẽ chạy GET và forward xem nó sẽ trả về gì.

Sau khi forward có thể thấy GET vẫn được trả về nhưng kết quả sẽ là 404 vì ở đây nó sẽ random ra file khác nên nếu chưa trùng tên thì kết quả sẽ không ra bây giờ ta sẽ thử script khai thác.
Ở đây tôi sẽ viết script sẽ gửi POST và GET request sẽ xảy ra nhanh nghĩa là sau khi POST file php lên thì ngay lập tức gửi GET request để lấy nội dung và với vấn đề về đoạn random ở tên file trong thư mục /tmp thì mình sẽ để cố định dãy số nào đó (vd : 0086.php) và lặp đi lặp lại quá trình request đến khi nó chạm đúng vào file 0086.php và lấy được flag ở đây mình tạo 10001 request và chờ thôi nếu nhân phẩm tốt thì flag sẽ ra sớm còn không thì chờ.

Kiểm tra trong burp xem proxy có hiển thị đủ 2 request không, ở đây nó sẽ liên tục tạo từng cặp POST và GET nên không lo về vấn đề time.
Code Exploit:
1 | import requests |
Thành công lấy được flag.
