SECCON Beginners CTF 2021 Writeup (Web)

# はじめに
初めてのQiita記事です。
SECCON Beginners CTF 2021 に参加したので Writeup というものを書いてみます。
常設ではないCTFは去年の SECCON Beginners CTF 以来2回目、今回もチームは組まずソロ参加でした。
簡単なWeb問題だけでも解けたらいいなと思って参加してみましたが、Web4問・Misc3問・Crypto1問・Reversing2問が解けて結果は862点 943チーム中の191位でした。思ったよりできたので嬉しいです。

追記：Web以外を別記事に書きました。 https://qiita.com/tm741_/items/bace80869d5cee8c393f

# Web問題Writeup
## osoba
> 美味しいお蕎麦を食べたいですね。フラグはサーバの `/flag` にあります！
https://osoba.quals.beginners.seccon.jp/
想定難易度: Beginner

適当にリンクを踏むとクエリ変数にパスらしいものが出てきます。
`https://osoba.quals.beginners.seccon.jp/?page=public/kikin.html`

配布ファイルからflagファイルがあるのが分かるので**パストラバーサル**でアクセスします。
`https://osoba.quals.beginners.seccon.jp/?page=../flag`

`ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}`

## Werewolf
> I wish I could play as a werewolf...
https://werewolf.quals.beginners.seccon.jp/
想定難易度: Easy

`player.role == 'WEREWOLF'`になっていたらflagが出力されるのに、ランダムで選ばれるroleの値のリストには`WEREWOLF`が入っていません。
下のようにリクエストの変数を全部読み込んでしまう実装なので正しい変数名で送信すればいいのは分かりましたが、とりあえず `__role=WEREWOLF` とか `role=WEREWOLF`を送ってみてもうまくいかない。

``` python:app.py
    if request.method == 'POST':
        player = Player()

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

配布ソースの通り`Player`クラスのオブジェクトを作って`__dict__`を表示させることで、正しい変数名は`_Player__role`であることがわかりました。

```python
import random

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

testplayer = Player()
print(testplayer.__dict__) # {'name': None, 'color': None, '_Player__role': 'VILLAGER'}
```

Bodyを`_Player__role=WEREWOLF`としたPOSTリクエストを送るとflagが返されます。
`ctf4b{there_are_so_many_hackers_among_us}`

**mass assignment脆弱性**というそうですが日本語訳の定訳を知りたいです。

## check_url
> Have you ever used `curl` ?
https://check-url.quals.beginners.seccon.jp/
想定難易度: Easy

惜しいところまで行っていたようですが正答できませんでした。

`$_SERVER["REMOTE_ADDR"]`が127.0.0.1になればflagが取得できるようです。

``` php:index.php
if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){
  echo "Hi, Admin or SSSSRFer<br>";
  echo "********************FLAG********************";
}
```

`$_SERVER["REMOTE_ADDR"]`はネットワーク層のIPアドレスが入っていてHTTPリクエストからは改変できないようなので、

https://christina04.hatenablog.com/entry/2016/10/25/190000

**SSRF**って書いてあるのもヒントに、問題サーバのcurlを使って自身へのHTTPアクセスを試みます。
ただ、`localhost`はブロックされ、`127.0.0.1`はピリオドがSuper sanitizing👻されてしまうので渡せません。

``` php:index.php
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!");
}
```

IPアドレスの10進数表記があったことを思い出し、変換してみた`2130706433`では**400 Bad Request**。
ほかに調べていてピリオドを使わない8進数表記`017700000001`を見つけたもののこれも**400 Bad Request**というところで挫折。

この記事に辿り着けなかったのが残念です。

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

[作問者様Writeup](https://qiita.com/satoki/items/36837fce2fff7a584947#web-check_url)より、正解は16進数表記の`0x7F000001`だったそうです。
`https://check-url.quals.beginners.seccon.jp/?url=https://0x7F000001/`
`ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}`

## json
> 外部公開されている社内システムを見つけました。このシステムからFlagを取り出してください。
https://json.quals.beginners.seccon.jp/
想定難易度: Medium

ローカルネットワーク(192.168.111.0/24)内から閲覧する必要があるそうです。
`bff\main.go`で`ClientIP()`のIPアドレスを見て制限をかけているようですが、go の`ClientIP()`は特に設定していないとHTTPリクエストの`X-Forwarded-For`ヘッダで偽装できるのが分かりました。

https://github.com/gin-gonic/gin/issues/1684

リクエストに`X-Forwarded-For: 192.168.111.1`ヘッダを追加して内部ページが表示されました。

Submitを押すとJSONをPOSTしますが、Flagを選択したときの`{"id":2}`は禁止されています。
bffとapiでJSONに同じキーが2個あるときの解釈が違っているのを予想し(apiのソースは読んでなかった) `{"id":2,"id":1}`を送信してみると正解。
`{"result":"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/

深夜に見るラーメン画像、なんと攻撃力の高いことか…。

DBではなくuser_id別に作られるファイルでユーザーデータが保存されています。
普通に購入していくとbalanceが足りなくなりますが、ユーザーデータを読み込んでからファイルに書き込むまでの間はロックされていないので**データ競合**の問題があります。

1つリクエストを出したときの`time.sleep(random.uniform(-0.2, 0.2) + 1.0)`の間に残り2つのリクエストが出されるように、`buy_noodles`を2回と`buy_soup`のリクエストを出せばいいことがわかります。
1秒前後も待ってくれるので特にコードを書かなくてもBuyボタンを続けて順番に押すだけで倍盛りラーメンを食べることに成功。
`ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}`

## magic
> トリックを見破れますか？
https://magic.quals.beginners.seccon.jp/
想定難易度: Hard

magic linkを上手く使ってcrawlerにXSSを踏ませる問題っぽいのは分かりましたが、CSPヘッダに`script-src 'self'`があるので普通にインラインスクリプトではダメというところから進まず解けませんでした。