ヤバイサイトのセキュリティはどれだけヤバイのか検証してみた
1. 前置き
先日、このツイートがエンジニア界隈でちょっとだけ話題になりました。
https://twitter.com/ymotongpoo/status/866929418073120769
事の発端は、このツイートにあります。
https://twitter.com/e_Traning_html5/status/866677588315447297
WebRTCと言えば、ブラウザを介してリアルタイムボイスチャットをするのに用いられる技術、という認識が一般的ですが、
『ポケモンGOに使われている技術』という「ん?」な説明をしており、
その一方でこの人はホームページでHTML5, WebRTCに関する教材販売をしていて、
しかしそのサイトのデザインはとうていサイト作りを教える人のものとは思えない物。
というなかなか強烈なキャラクターを見せてくれました。
ttp://yours-company.jp
https://twitter.com/e_Traning_html5/status/867038207946272768
私の知ってるHTML5と違う・・・。
さて、このようなサイトで気になってくるのがセキュリティです。
…というのも、このようなツイートを見たからです。
https://twitter.com/mpyw/status/866962107606089728
このスクショ1枚だけだったので、おそらくHTTPSでなくHTTPで送信している問題を上げているのでしょう。
とはいえ、そんなサイトは現在まだまだあります。
このサイトであれば、他のセキュリティ的な問題があるかもしれない、
と、セキュリティ素人ながら気になって眺めてみました。
2. パスワードの認識されないログイン画面
このような購入画面を経て、
ログイン画面に行き着きます。
ここでなんとなくメールアドレスに「 a@a.com 」と書いて
パスワードに「aaaa」と書くと通ったので、「たまたま合ってしまった?」と思いました。
ところが、パスワードを「a」としてもやはり通ってしまいます。
これは妙です。どういった判定がなされているのか見てみました。
3. コードの中身は?
HTMLにJavaScriptがそのまま貼られているので、コードを追うのに苦労はしません。
function password_cheack(num){ var err = 0; if(num == 0){ var str = $('#password').val(); if(str == ""){ alert("パスワードを入力してください。"); $('#password').focus(); //フォーカスを合わせる return -1; } return 0; }else if(num == 1){ var str = $('#password').val(); var str2 = $('#password2').val(); if(str2 != str){ err = -1; alert("パスワードが違います。"); $('#password').focus(); $('#password').select(); //入力された文字列を選択状態にする } return err; } };
パスワードチェックはこの部分で、
引数 num = 0 のときは入力フォームである #password の値が空白でないか調べ、空白であればアラートを表示し-1。空白でなければ0を返します。
引数 num = 1 であれば、入力フォームである #password と、hiddenフォームである #password2 の値を比較し、一致していなければアラートを表示し-1、一致していれば0を返します。
ということは、パスワードが一致していない場合でも、hiddenフォームを見れば正しいパスワードを見られるわけで、
その時点でこのパスワードチェックは意味をなしていないわけですが、それは置いておくとして、呼び出し側を見てみましょう。
$('#login').click(function(){ var err = mail_cheack(); if(err == -1){ return -1; } var err2 = password_cheack(0); if(err2 == -1){ return -1; } check_database(1); });
password_cheack(0)
!
そう、必ず引数は 0 を渡していたのです。
これでは、パスワード欄が入力されているかのチェックしかなされず、たとえパスワードが合っていなくとも、ログインが完了してしまう。そういう仕組みなのでした。
4. 漏れる個人情報
前章にてhiddenフォームである #password2 に正しいパスワードが代入されると書きましたが、
では、どのようにパスワードが代入されているのかを見てみましょう。
function check_database(num){ var key; if(num == 0){ key = $('#number').val(); }else if(num == 1){ key = $('#mail_address').val(); alert(key); }else if(num == 2){ key = $('#password').val(); } var url = 'http://yours-company.jp/School_Site/account/PHP/database_search.php'; //var url = 'http://yours-company.jp/School_Site/account/PHP/post.php'; $.ajax( { url: url, dataType: 'html', data: { search_no: num, search_key: key }, success: function( data ) { var datas = data.split(',', 7); var err_flag = datas[0]; var search_type = datas[1]; var search_key = datas[2]; var number = datas[3]; var password = datas[4]; var mail = datas[5]; var name = datas[6]; if(err_flag == 0){ // パスワードのチェック $('#paypal').attr('disabled',false); $('#password2').val(password); $('#mail_address2').val(mail); $('#name').val(name); $('#userID').val(number); var html = ""; html = "<p>" + name + "様</br />ご購入頂きありがとうございます。</p>"; html += "<p>お支払い手続き終了後に" + mail + "のアドレスにご確認メールを致します。ご確認の手続き終了後のご送付になります。</p>" $("#kakunin").append(html); $("#kakunin").show(); }else{ alert('アカウントが違います。またはアカウント登録がされていません。'); $('#paypal').attr('disabled',true); $('#kakunin').html(''); $("#kakunin").hide(); } }, error: function( data ) { alert( '読み込み失敗' ); } }); };
check_databaseメソッドにおいて、database_search.php を叩き、
取得した値を変数 data に代入させたのち、 ,
で区切って配列 datas に格納し、必要な値を取り出し、
hiddenフォームなどに値をセットしています。
ところで、呼び出し元では
password_cheack メソッドのあとに check_database メソッドでしたから、
これでは password_cheack メソッドの段階では #password2 に値はセットされていません。
password_cheack メソッドの引数が 0 なのは、パスワード判定がどうしても失敗することに困った末の苦肉の策なのかもしれません。
さて、「漏れる個人情報」の話に戻りましょう。
ここの非同期通信(Ajax)のメソッドを見るに、
http://yours-company.jp/School_Site/account/PHP/database_search.php
にアクセスすれば容易に値が取得できそうなことがわかります。
見ず知らずの人のアカウントを取得するのは倫理的に良くないので、会員登録をして試してみます。
このようにアカウントを取得してみました。
会員番号の値をもとに情報を取得するには search_no = 0,
メールアドレスの値をもとに情報を取得するには search_no = 1,
パスワードの値をもとに情報を取得するには search_no = 2,
ということがコードから読み取れますから、まずはメールアドレスで試してみましょう。
curl "http://yours-company.jp/School_Site/account/PHP/database_search.php?search_no=1&search_key=test@example.com"
このような値が返ってきます。
0,1,test@example.com,856,testtest,test@example.com,テスト 太郎,1
返ってきた値は、左から順に
- エラーコード : 0
- search_no : 1
- search_key : test@example.com
- 会員番号 : 856
- パスワード : testtest
- メールアドレス : test@example.com
- 会員の姓名 : テスト 太郎
となっています。(一番右の 1 はダミー?)
パスワードが暗号化せずに保存されていることは、 #password2 との値の比較でパスワードの成否判定をしていたことから今さら驚かないですが、
もちろんこれも良くないですね。
ですが、一番良くないのは全会員情報が簡単に引き出せることにあります。
ブルートフォースアタック(総当たり攻撃)を仕掛けずとも、
連番で登録されている会員番号を1から順番に指定して取得を繰り返す、一行で書けるような簡単なシェルスクリプトを書くだけで、
全会員のメールアドレス、パスワード、会員の姓名を取り出せてしまいます。
5. 考えたこと
今回は極端な例ですが、思ったのは、
あまりいろんなサイトで真面目に会員情報を登録するのは怖いな、ということです。
大きな会社ですと情報漏えいはニュースになるくらいなのでまだいいのですが、
DTMで音源を買う関係で、海外サイトや、個人サイトで会員登録をすることも多く、
そういうところで情報漏えいが起きたところで、私(もしくはそのサイトの運営者)は気付くことがないので、ちょっと怖いなと思いました。
それから、JSコードは minify なり uglify をかけておくと、
多少ダメなコードでも、その難読性から少しは脆弱性対策になるな、と、
今回のコードが読みやすかったぶん感じました。
以上、セキュリティは気を付けないと顧客情報が大変って話でした。
ユーザー登録者としてもウェブ開発者としても、こういう話は明日は我が身です……。
ちなみに、フォームで任意のJavaScriptコードを実行できる問題もありました。