THJCC CTF 2024 Winter - official writeup
Author: 堇姬Naup
source code: https://github.com/Naupjjin/THJCC-CTF-2024-2th/tree/main
這次出了兩題滅台題,希望參賽者喜歡www
It’s Mygo!!!!!🎤🎸🎸🥁🎸 Golang’s Funeral 🎹
tag: web
、reverse
、pwn
、golang
、MyGo!!!!!
IDA分析
我沒有拔掉debug symbol(因為我發現拔掉好像會太難逆)
以下可以搭配釋出的source code跟golang官方文檔,裡面有該函數原本的樣子,比較容易看懂
golang官方文檔
https://pkg.go.dev/net/http
https://pkg.go.dev/os/exec
先從入口main.main開始看
net_http__ptr_ServeMux_Handle
他會設置route的handler
包括 “/mygolang”、”/itsmygo” 和”/“
https://pkg.go.dev/net/http#ServeMux.HandleFunc
ServeMux具體在golang實現會向是這樣
1 | http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { |
ida如下
1 | v9.tab = (runtime_itab *)net_http_DefaultServeMux; |
這東西跟處理filehandler有關係的,他也創建了一個route /static/,並要求他當一個prefix
https://pkg.go.dev/net/http#StripPrefix
1 | v9.tab = (runtime_itab *)runtime_newobject((runtime__type *)&RTYPE_http_fileHandler); |
off_70B7E0,應該是印出跟開在哪個host 或是 port,等等的資訊
1 | v9.data = (void *)&handler; |
off_70B7E0
1 | .rodata:000000000070B7E0 off_70B7E0 dq offset aServerStartedO |
設定 listen 在哪個 port (unk_69871D) .rodata:000000000069871D a000020000 db '0.0.0.0:20000' ; DATA XREF: main_main+13D↑o
1 | p_http_Server = (http_Server *)runtime_newobject((runtime__type *)&RTYPE_http_Server); |
看到這邊web server setting其實差不多了,接下來去分析其他地方,這裡我們直接鎖定重點
main.mygoooHandler
這裡根據網站是處理compiler的頁面
1 | if ( r->Method.len == 4 && *(_DWORD *)r->Method.str == 'TSOP' ) |
POST會進入到if,否則直接顯示該頁面
以下是complier 透過 POST method處理邏輯
處理跟user request有關,錯誤就印出ERROR
看到以下他先創建了一個 Object叫做 CompileRequest
https://pkg.go.dev/github.com/open-policy-agent/opa/test/e2e#TestRuntime.CompileRequest
也創建了 json_Decoder object 並針對傳入的 body (POST data)去做操作
https://pkg.go.dev/encoding/json#Decoder
之後就調用 JSON 解碼器的 Decode 方法
1 | ra = r; |
decode出問題則進入,並輸出error(這邊其實看組語更清楚)
這邊可以更清楚看到進入到error分支
這裡是顯示的頁面
1 | .rodata:000000000069B88B aStaticMygolang db './static/mygolang.html' |
如果decode正確繼續則往下走,這部分會去生成Random hash,這是負責生成檔案名稱隨機值
1 | RandomHash = main_generateRandomHash(); |
透過實際執行跟ida的內容不難知道./userFile會儲存兩種檔案.json .go 並且檔案名稱會加入上方生成出來的hash值
接下來可以看到hash randon的值會被拿來幹嘛
這邊建一張表方便對應上面一些value是哪些string
變數 | value |
---|---|
byte_6977B6 | ./userFile |
byte_698B34 | %s/%s_env.json |
name是random hash的value
1 | name.str = RandomHash._r0.str; |
v39先是./userFile
,之後丟給v29.m256_f32[2]
(想像成array的第一個值)
接著name被丟給v39之後丟給v29.m256_f32[6]
,v29給v48,v39後來則拿到%s/%s_env.json
,被丟入到fmt_Sprintf
最後變成 fmt_Sprintf("%s/%s_env.json", "./userFile", <random hash>)
並把串好的值丟回給name
這裡就可以知道這邊組成了一個路徑,你在source code中也可以清楚的看到該目錄
1 | *(_OWORD *)v29.m256_f32 = v3; |
之後就是做json_Marshal(對傳入的req->Env),他是一個可以去循環遍歷的一個function,將傳入的資料轉成Json,之後去做writefile
看一下writefile的長相WriteFile(filename string, data []byte, perm fs.FileMode)
這邊基本上可以確定的是req->Env被丟入後轉成json被寫入到userFile下
https://pkg.go.dev/encoding/json#Marshal
https://pkg.go.dev/io/ioutil#WriteFile
1 | v40._type = (runtime__type *)&RTYPE_map_string_string_0; |
這邊我一樣列出對應關係,不過基本上跟上面一樣
變數 | value |
---|---|
byte_6977B6 | ./userFile |
byte_696DA9 | %s/%s.go |
這邊基本上可以確定的是req->Code被寫入到userFile下(一個.go)
1 | *(_OWORD *)v29.m256_f32 = v3; |
之後就進了main_mygoooHandler_func1
1 | v17 = (runtime_funcval *)runtime_newobject((runtime__type *)&stru_67DD20); |
進重點先提一下這個,這是為了防止他在你的command卡住所以設了timeout,你在解題時就會發現,你用curl他其實會重複好幾次,但在遠端跑可能只Request兩三次就斷開就是這個原因
https://pkg.go.dev/context#WithTimeout
1 | val = *(_QWORD *)(v0 + 48); |
這部分重點就兩個第一部分
1 | LABEL_39: |
這裡要關注的我們寫進去的json env做了甚麼,他被當成環境變數去做設定了
https://pkg.go.dev/os#Setenv
1 | v81.str = (uint8 *)v47.len; |
寫進去的環境變數會去做檢查黑名單(檢查value是否有這些字串),IDA在解析有跑掉,不過透過下方數字可以知道長度
1 | ((void (__fastcall *)(char *))loc_464614)((char *)&File + 544); |
為何可以知道他是黑名單,因為這部分是檢查相關的,如果錯會噴error,或是可以透過error message直接知道這裡是黑名單
第二部分是看.go做了甚麼
先看這裡
1 | v56 = v2; |
這裡是把userEXE用fmt串成路徑
這部分則是傳入arg,分別是
byte_69601D(go)
unk_6964D5(build)
unk_695F87(-o)
1 | arg.array = (string *)&unk_6964D5; |
最後被傳入os_exec_command,基本上到這裡就可以看出來
傳進去的.go會被編譯成執行檔(go build -o “your.go”)
這裡會發現,你無法控os_exec_command,所以第一個坑點,Command injection不在這
第二個坑點,你傳進去的.go不會被執行,所以沒有任意golang code執行
1 | p_arg = &arg; |
逆向到這裡其實差不多了,簡單梳理流程就是 送出 POST -> 將 request 的 env跟code存起來到檔案 執行os setenv去更改環境變數(根據剛剛存的檔案也就是你輸入的環境變數),並去檢查你env的value是否吃黑名單,最後build你送進去的檔案 這題就是任意控env跟code他會幫你編譯卻不會執行的題目
attack
這題目標要 RCE
這邊可以先看看golang會有哪些環境變數
輸入go env可以知道
1 | GO111MODULE="" |
順便觀察一下golang在編譯時候的行為,我們去編譯這個
1 | package main |
1 | naup@naup-virtual-machine:~/Desktop/dist$ go build -x m.go |
單純看下來其實控環境變數對於golang編譯的行為其實不大,大部分是設定路徑跟使用package之類的行為
這題還讓你們控編譯的code那當然就沒那麼單純只是修改環境變數就可以RCE了
如果你認真觀察golang的環境變數會發現,他有跟gcc相關的環境變數,但卻沒有用到gcc
在 golang 中撰寫函式庫時,通常這些函式庫只能供 golang 使用。這是因為 golang 在提供跨語言支援上並不如一些其他語言靈活,相比之下,C、C++ 或是 Rust 等語言提供了更好的選擇,因為它們在性能、跨語言互操作性上表現更佳。
除此之外,許多現有的 C 或 C++ 函式庫已經被使用多年,並且運行穩定,沒有理由僅僅因為想轉換到 golang 而將這些函式庫重新實作。因此,最合理的方式是讓 golang 直接利用這些現有的 C 或 C++ 程式碼,而不是重寫
golang官方就開發了cgo
https://pkg.go.dev/cmd/cgo
仔細想想,要使用cgo一定要有C / C++ 編譯器之類的
而 CC 這個環境變量的 g++ 就是指定了編譯器,並去使用他
1 | package main |
我們去 import C
再來看看編譯行為
1 | naup@naup-virtual-machine:~/Desktop/dist$ go build -x m.go |
他確實執行了gcc
那如果我們試著去修改CC這個env
再去執行
1 | naup@naup-virtual-machine:~/Desktop/dist$ CC='MyGo!!!!!' go build -x m.go |
他好像執行到了 MyGo!!!!!
我們改成 sh -c ‘whoami’
1 | naup@naup-virtual-machine:~/Desktop/dist$ CC='sh -c "whoami"' go build -x m.go |
成功RCE,接下來就用curl 或 wget的方式將結果送到webhook就可以解了!(黑名單所有都可以透過在中間塞一個${x}來繞過)
其實 golang官網有提到CC這個環境變數相關資料
exploit
另外像是輸出時被換行之類的或是emoji之類的問題就用 base64 + tr -d
解決
1 | import requests |
Flag: THJCC{MyGo!!!!!https://www.youtube.com/channel/UC80p_16pSSHA8YmtCVdX51w_OuO_ItsMygo!!!!!🎤🎸🎸🥁🎸GolangsFuneral🎹}
🎭🎭🎭🎭🎭Welcome to AVE Mujica🎶
tag: pwn
分析
1 |
|
他malloc一塊chunk並存放AVEmujica(flag)
並且使用者可以輸入一些東西,這裡存在無長度限制的buffer overflow
攻擊
有buffer overflow了但是會碰到很多問題,首先是保護全開
再來是可以寫ret address但是不知道要寫甚麼,ROPgadget不夠,又沒辦法leaklibc
我們只有flag的位置而已
這邊觀察一件事,當我們在glibc 2.23觸發stack smashing detect的時候,他會噴出./demo,也就是你ELF的位置
他是去哪裡找到ELF的路徑的
接下來來翻source code
通常如果有開canary會在最下方看到__stack_chk_fail
追進去看會call __fortify_fail
https://elixir.bootlin.com/glibc/glibc-2.23.90/source/debug/stack_chk_fail.c
1 | void |
__fortify_fail
會call __libc_message,ELF路徑就是__libc_argv[0]
https://elixir.bootlin.com/glibc/glibc-2.23.90/source/debug/fortify_fail.c#L26
1 | void |
Flag: THJCC{Welcome_To_AVe_Mujica_CRYCHIC_is_dead_QQ}