Post

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

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

Description

Author: Siam Thanat Hack Company Limited
เว็บโจทย์การแข่งขัน: https://web1.ctf.p7z.pw
เป้าหมายการเจาะระบบ:

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

Solution

Flag 1

เมื่อเปิดเว็บไซต์ที่โจทย์ให้มาจะเจอกับหน้า login โดยถ้าเรา inspect (Ctrl+Shift+I) ดู source code หน้านี้ก็จะพบว่ามี credential ที่คอมเมนต์ไว้อยู่

1
2
username: test
password: test

sthweb1_1

นำ credential ที่ได้ มา login ระบบจะพามาหน้า userinfo ซึ่งมีการแสดง username, role และ status สังเกตว่าระบบมีการแสดงผล role เลยคิดว่าน่าจะมี role admin และ น่าจะเกี่ยวกับ token ที่เอาไว้กำหนด role

sthweb1_2 มาดูในส่วนของ Cookies จะพบว่า มี Cookie ชื่อว่า remember_me ซึ่งมีค่าที่อยู่ในรูปแบบของ JWT (JSON Web Tokens) ตอน login ต้องติ้กเลือก remember me ด้วย ระบบถึงจะมีการกำหนดค่า Cookie ตัวนี้

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6ImI4MTk0M2JhLWQxYzUtNDk1YS04NDI3LTQ3MTFjMzkyNTZiZiJ9.Rlk_a69lx16hNhwn4nBfRxhiMGmEDoPIcxfr1_7JdH8

JWT (JSON Web Tokens) ประกอบด้วยสามส่วนหลักที่แยกจากกัน โดยแต่ละส่วนจะถูก encode ให้อยู่ในรูปแบบ base64 และนำมาต่อกันโดยมี . คั่น

  1. Header - ใช้เพื่อระบุประเภทของ Token และอัลกอริธึมที่ใช้ในการเข้ารหัส เช่น HS256 หรือ RS256
  2. Payload - ใช้เก็บข้อมูลต่างๆ ที่จะถูกส่ง เช่น ข้อมูลผู้ใช้ ข้อความ หรือข้อมูลที่ต้องการแชร์
  3. Signature - ใช้เพื่อยืนยันความถูกต้อง (Integrity) ของ Token โดยจะถูกสร้างจากการเข้ารหัส Header และ Payload ด้วย Secret Key

sthweb1_3

ดังนั้นเราสามารถนำ JWT ที่ได้มาไป decode จาก base64 ให้อ่านรู้เรื่องได้โดยใช้เว็บไซต์ https://jwt.io/
Payload ด้านในมีการระบุ token ตรงนี้คิดว่าเป็น token ของ user test เราต้องหา token ของ admin แล้วนำมาแก้ไขในส่วนนี้ซึ่งการที่จะแก้ไข JWT ได้จำเป็นต้องมี secret key ตามที่ได้อธิบายในส่วนของ signature ไปด้านบน

sthweb1_4

สามารถ bruteforce หา secret key ได้โดยการใช้ John The Ripper (เครื่องมือยอดนิยมสำหรับการ crack รหัสผ่านจาก hash หรือข้อมูลที่ถูกเข้ารหัสด้วยวิธีการ bruteforce, dictionary attack) โดยระบุ format เป็น HMAC-SHA256 และใช้ rockyou.txt เป็น wordlist ในการ bruteforce หา secret key

1
2
3
4
5
6
7
8
9
10
11
12
$ cat jwt.txt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6ImI4MTk0M2JhLWQxYzUtNDk1YS04NDI3LTQ3MTFjMzkyNTZiZiJ9.Rlk_a69lx16hNhwn4nBfRxhiMGmEDoPIcxfr1_7JdH8

$ john --format=HMAC-SHA256 --wordlist=rockyou.txt jwt.txt
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
"bobcats"        (?)     
1g 0:00:00:01 DONE (2025-03-27 01:18) 0.5780g/s 8291Kp/s 8291Kc/s 8291KC/s "chinor23"..*7¡Vamos!
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 

จากผลลัพธ์ secret ket ของเราเป็น "bobcats" (รวม double quote ลงไปใน key ด้วยนะครับ ตรงนี้ตอนแรกทำให้ผมสับสนนิดนึง5555 นึกว่า key คือ bobcats) ตอนนี้มี secret key ไว้เปลี่ยน payload แล้ว แต่ยังขาด token ของ admin

กลับมา inspect ดู soucre code อีกรอบที่ไฟล์ script.js ในหน้า userinfo.php

sthweb1_5

บรรทัดที่ 35 มี api endpoint api.php?action=get_alluser ไว้ใช้สำหรับดู user ทั้งหมด เลยลองเข้าไปดู เจอ 2 users คือ test ที่เรามี credential อยู่ตอนนี้ และ admin-uat

sthweb1_6

บรรทัดที่ 23 api.php?action=get_userinfo&user=test น่าสนใจตรงที่มี parameter user เพิ่มเข้ามา ถ้าลองเปลี่ยน user ตรงนี้เป็น admin-uat จะได้ข้อมูลหรือป่าว ปรากฎว่าสามารถดูข้อมูลได้ และ ได้ remember_me_token มาใช้ login เป็น admin-uat แล้ว เหลือแค่นำ token ตรงนี้ไปใส่ใน payload ของ JWT

sthweb1_7

นำ 73eb7063-f8c3-4e50-bea2-07c05681aa92 ใส่ใน payload ของ JWT และ ใส่ secret key "bobcats" ให้เรียบร้อย ทีนี้ก็จะได้ JWT ของ admin-uat มาแล้ว

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IjczZWI3MDYzLWY4YzMtNGU1MC1iZWEyLTA3YzA1NjgxYWE5MiJ9.IFc2uZiX_3x1ihXgRaANOPvmySpQzFz_wMD0up8Ny0I

sthweb1_8

จากนั้นนำ JWT ของ admin-uat ไปใส่ใน Cookie remember_me และลบ PHPSESSID แลัว refresh ก็จะพบว่าตอนนี้เราเป็น admin-uat เรียบร้อยแล้ว

sthweb1_9 sthweb1_10

ในไฟล์ script.js ก่อนหน้านี้ บรรทัดที่ 34 มีคอมเมนท์ admin.php ซึ่งน่าจะหมายถึง path เว็บไซต์ของ admin ลองเข้าไปดู แล้ว inspect ดู source code ก็จะได้ Flag 1 มาแล้ว!

sthweb1_11

Flag 1: STH1{310052ba6883872435f7c5aafa850813}

Flag 2

sthweb1_12 ต่อจาก Flag 1 เลย อ่าน source code บรรทัดที่ 50-66
ฟังก์ชัน validateNumber($input) - ใช้ preg_match() เพื่อตรวจสอบว่า $input ตรงกับ Regular Expression นี้หรือไม่ /^[0-9]+$/m

  • ^ - เริ่มต้นบรรทัด
  • [0-9] - ตัวเลข 0 ถึง 9
  • + - 1 ตัวอักษรขึ้นไปของสิ่งที่มาก่อนหน้า กรณีนี้คือตัวเลข
  • $ - สิ้นสุดบรรทัด
  • m - การใช้งานในหลายบรรทัด

สรุปคือ ตรวจสอบว่าเริ่มต้นด้วยตัวเลขและลงท้ายด้วยตัวเลขหรือไม่
บรรทัดที่ 60 นำ amount มาเช็คกับฟังก์ชัน validateNumber และ เช็คว่าใน amount มี string STH อยู่หรือไม่ โดยใช้ฟังชันก์ strpos() ถ้าเช็คทั้งสองอย่างแล้วใช่ทั้งคู่ ก็จะมี output message ออกมาเป็น Flag 2 ที่เราต้องการ จึงได้ลองใช้ payload เป็น

1
2
ASTH
1234

บรรทัดแรก ASTH ทำให้ strpos() คืนค่าออกมาเป็น 1 ถ้าไม่ได้เพิ่ม A ไปด้านหน้า ฟังชันก์จะคืนค่ามาเป็น 0 ซึ่งเป็น false

strpos() - ค้นหาตำแหน่งแรกที่พบของ substring ใน string ถ้าค้นหาเจอจะคืนค่าเป็นตำแหน่ง (index) ของการเจอครั้งแรก ถ้าไม่จะคืนค่าเป็น false

บรรทัดสอง 1234 ทำให้ preg_match() คืนค่าออกมาเป็น true เพราะ preg_match() ใช้ m ซึ่งมีอย่างน้อยแค่บรรทัดเดียวที่ match ก็จะคืนค่าเป็น true เลย

เราสามารถ input หลายบรรทัดได้โดยการเปลี่ยน code ของหน้านี้ ตรง tag input ให้เป็น textarea หรือ สามารถใช้ Burp Suite ดัก request แล้วเปลี่ยนก็ได้ (ใช้ %0D%0A ในการคั่นแต่ล่ะบรรทัด) แต่ ผมขอใช้วิธีแรกเพราะว่าง่ายดีครับ :)

sthweb1_13 sthweb1_14

ได้ Flag 2 เรียบร้อย!

Flag 2: STH2{d9d2532fd8ad5419450b5ea34ed93f32}

This post is licensed under CC BY-NC 4.0 by the author.