2013年10月21日月曜日

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

PHPでスクリプトを書いていて、setcookieの第2パラメータ(クッキーの値)の変数をタイプミスしたところ、以下のレスポンスヘッダが送信されていました。
setcookie('A', $misspelled_variable);
 ↓ 結果
Set-Cookie: A=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT
日付が大昔になっているし、クッキーの値に「deleted」は指定していません。これは、クッキーを削除する時の書き方ですが、PHPでクッキーの削除というと、expiresに過去日付を明示する方法をよく見かけますが、単に第2パラメータを空文字列にすればよかったのか…と思いマニュアルを見たら、一応書いてありました。
http://php.net/manual/ja/function.setcookie.php
陥りやすい失敗
クッキーは設定されたものと同じパラメータで削除する必要があります。 値が空文字列あるいは FALSE で、その他の全ての引数が前に setcookie をコールした時と同じである場合に、指定された名前のクッキーが リモートクライアント上から削除されます。 内部的な動作として、これは値を 'deleted' に変更した上で有効期限を 1 年前に設定しています。
「一応」と保留したのは、マニュアルの本文ではなく「陥りやすい失敗」という項に書いてあったからです。それに、マニュアルには「1年前」とありますが、実際に設定される日付はずっと古い日付です。
そう思って、PHP5.2.6で試したら、以下のように、1年前の日付になります。
Set-Cookie: A=deleted; expires=Sat, 20-Oct-2012 07:17:38 GMT
どこかで仕様変更されたのだろうと思い、バイナリサーチで調べたところ、PHP5.3.6までは「1年前」、PHP5.3.7以降が1970年1月1日に変更されていました。

ソース上で確認すると、ext/standard/head.cのphp_setcookie関数で以下のように修正されていました。
// PHP5.3.6
if (value && value_len == 0) {
  time_t t = time(NULL) - 31536001;
  dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, t, 0 TSRMLS_CC);
  snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s", name, dt);
  efree(dt);
} else {


// PHP5.3.7
if (value && value_len == 0) {
  dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, 1, 0 TSRMLS_CC);
  snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s", name, dt);
  efree(dt);
} else {
この変更はChangeLogにも載っていないので変な感じですが、クッキーを削除したい場合はexpiresをできるだけ古い時刻にするという考え方は妥当です。そうしないと、端末の時計が狂っている場合に、クッキーが削除されずに、「PHPSESSID=deleted」という形に設定される危険性が高くなります。これが複数ユーザ存在すると、一種のセッション固定の状態になります。PHP5.5.2以降で導入されたstrict sessionsを設定していれば大丈夫ですが、まだstrict sessionsを設定しているサイトはごくわずかでしょう。

時計をあわせていない利用者の方が悪いという方もいるでしょうが、例えばこちらの事例のように、携帯機器のブラウザが「4年前の日付」になっている例もあるので、1年前くらいでは確かに不安です。「1時間前」の時刻をセットしてクッキーを削除する例をよく見ますが、端末の時計が狂っている前提では、意図に反して脆弱になる可能性があります。

私は「ログアウト時にセッションクッキーを削除する必要はない」という意見を持っています(参考:「ログアウト機能の目的と実現方法」)が、仮に削除するのであれば、端末の時刻が狂っている場合を想定して、削除時のクッキーの値には暗号論的に安全な乱数(openssl_random_pseudo_bytes関数か、/dev/urandomが利用できます)を設定しておくのがよいと思います。そうすれば、万一クッキーが削除されず設定された場合でも、脆弱性にはなりません。

2013年10月15日火曜日

session_regenerate_id関数の第1引数はtrueにすべきか

以前tumblrに書いたエントリ「データベースのデータを信用してはいけないか?」にて、PHP技術者認定試験の想定問題について取り上げましたが、その後、書籍「徹底攻略 PHP5 技術者認定 [上級] 試験問題集 [PJ0-200]対応」が刊行されたことを知り、購入しました。
同試験は、比較セキュリティの配点が高い(12%)ことから、試験問題集にはセキュリティの独立した章として第10章が割り当てられ、セキュリティの問題が21個集められています。

先のエントリで紹介した「ITトレメ PHP技術者認定・初級 過去問題一覧 - @IT自分戦略研究所」の問題を見た時の印象は、問題の癖が強く、独自の用語を使っている箇所が多いことが懸念点でしたので、そのような観点から同書第10章「セキュリティ」の問題を確認したところ、全体的に下記の印象を持ちました。
  • 用語としてIPA等で使われている一般的なもの(例:静的プレースホルダ)が用いられ、妥当と考えられる
  • 対策手法についても、IPA「安全なウェブサイトの作り方」などで説明されている方法を正とし、独自の手法はほとんど見られない
  • 「上級試験」ということで、かなり際どいところを攻めているという印象
ということで、私が懸念した問題は見当たりませんでした。執筆陣の努力を称賛したいと考えます。
しかし、最後に指摘した「際どいところ」に関しては、正誤という点では問題ない(力のある解答者なら正当に到達できる)ものの、微妙なところで議論の余地があるなと感じました。(こちらの「大、小、展、外、誤」参照)。
それは、国語入試問題必勝法的な微妙さであって、明らかに正しい選択肢を載せるとすぐに答えがわかるために、微妙な問題を残すものを正答としているのではないかと勘ぐりたくなりました。
ここでは、その例として、セッション固定化攻撃の対策方法として、session_regenerate_id()関数の使い方に関する問題を取り上げます。

session_regenerate_idの第1引数はどうすべきか

同書P324のセキュリティ問題13には、セッション固定化攻撃の対策として適切なものを2つ選択するように支持していますが、ここではその選択肢CとDに関係する話題です。元の本が問題集なので、引用はせずに要旨のみを示します。
if (認証OKの場合) {
  $_SESSION['auth'] = true;
  session_regenerate_id( ● );
}
●のところ、片方が空、片方は true となっています。実は「国語入試問題必勝法」的には正答はあきらかで、trueを指定した方が正答です。この関数の第1引数は、元のセッションIDに紐づくセッションを破棄することを指定するもので、過去のセッションを破棄したほうが安全方向に倒れることは明らかです。すなわち、これがセキュリティの問題という前提では、trueを指定しないほうが正答ということは通常ありえないわけで、先に「正誤という点では問題ない」と書いた理由はこれです。
しかし、上記のスクリプトを検討すると、微妙な問題、いや、はっきり言えば正答にもバグがあります。一方で、誤答の方にも実質的な危険性があるわけではありません。

誤答がただちに危険なわけではない

問題文のスクリプトは、まずセッションに「認証状態にある」ことをセットしたあとで、セッションIDの再生成を行っています。このため、問題の趣旨としては、元のセッションIDに対して認証状態がいったんセットされるため、古いセッションを破棄しないとセッション固定化攻撃が成立してしまうとしています。
しかし、このスクリプトを動かしてみると分かりますが、古いセッションを保存するファイルには、認証情報は保存されません。その理由は、session_write_closeが呼ばれていないからです。ファイルに認証情報が保存されていない以上、そのファイルを積極的に破棄する必要もありません。例外として、「ログイン前セッション固定化攻撃」の問題がありますが、これについて別のエントリにしたいと思います。
ということで、誤答(trueを指定しない方)に実質的な危険性があるわけではありません。

正答が模範的なわけではない

一方、正答に問題がないわけではありません。現状のスクリプトでも外部から攻撃できるわけではありませんが、PHPのセッションが仮にsession_write_closeを呼ばなくてもファイルに書き込まれる実装に変更された場合は、一瞬とはいえ元のセッションIDで認証状態になります。これは直ちに、session_regenerate_id関数の第1引数 true により削除されるわけですが、いわゆるレースコンディションの状態となり、第三者にセッションハイジャックされる危険性があります。これらを下表にまとめました。

第1引数falseあるいは指定なしtrue
現状のPHPの実装問題なし問題なし
セッションが直ちにファイルに書き込まれる実装セッションハイジャックされるレースコンディションによりセッションハイジャックされる可能性

本来は、認証確認後に直ちにsession_regenerate_idすべし

前述のように、この問題は現状のPHPの仕様(実装)では誤答・正答とも実質的な問題はありませんが、PHPの仕様が変わると、正答の方がベターな書き方であるものの、ベストではない、ということになります。
では、ベストの書き方はどうかというと、下記のように、session_regenerate_idしてから認証状態をセッションにセットすべきです。
if (認証OKの場合) {
  session_regenerate_id( ● );
  $_SESSION['auth'] = true;
}
これにより、一瞬たりとも元のセッションIDが認証状態になることはないので安全です。そして、●の部分は、それ以前のセッションをどうすべきかで判断することになります。これはすなわち、「ログイン前セッション固定化攻撃」の対策をどうすべきかということになりますが、こちらは一層ややこしい問題であるので、別稿にて説明します。一般的には true を指定した方が安全ということには、異論ありません。

まとめ

PHP5 技術者認定 [上級] 試験問題集を題材として、session_regenerate_id関数の第1引数について検討しました。これをtrueにすることは、保険的な対策としては有効であると私も考えますが、trueを指定しないために脆弱性となる例を作るのはかなり難しいと考えます。
PHP5 技術者認定 [上級] 試験問題集の第10章13番の問題は、「trueを指定しないために脆弱となる」例を無理に作ろうとしたために、前述したように「際どい」問題になってしまいました。それにより、正答が模範的なスクリプトでないため、この問題集により「正しいPHPアプリの書き方が身につくわけではない」という点も気になったところです。(たぶん続く)

2013年9月30日月曜日

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

寺田さんのブログエントリ「他人のCookieを操作する」には、通信路上の攻撃者がいる場合は、SSLを使っても、Cookieの盗聴を防ぐことはできるが、Cookieの改変を防ぐことはできないと指摘されています。いかにも寺田さんらしい簡にして要を得たエントリで、これに付け加えることはあまりないのですが、残念ながらまだ読んでいない人が多そうだと言うことと、より広い読者に向けて具体的に説明した方がよいだろうと考えました。
そこで、通信路上に攻撃者がいる典型例として、公衆無線LANの偽AP(アクセスポイント)があるケースを題材として、「HTTPSを使ってもCookieの改変は防げない」ことを説明します(Secure属性使うと盗聴は防げますが、改変は防げません)。長いエントリなので結論を先に書いておきます。
  • Secure属性がないCookieはHTTPSでも盗聴できる
  • Cookieの改変についてはSecure属性でも防ぐことはできない

偽APを用意する

まるでハッカージャパンの記事みたいな内容なので、この節は文体もハッカージャパンを真似してみましょう。
ここで紹介する実験は、「偽アクセスポイント」の偽物を作ることである。そのまま悪用可能なので、良い子の皆さんは絶対に悪用しないように。約束だぞ。
偽APの構成を下図に示そう。


(1)無線APの準備
 適当に設定すればよいが、SSIDと事前共有鍵を実在の公衆無線LANと同じにすればだまされやすいだろう。ここではだますことが目的でなく実験なので、そこまではやらない(やるなよ)。無線の暗号化は強固なものにしても問題ない。盗聴・改ざんは有線で行うからだ。むしろ、強固な暗号化を選択した方が、だまされやすくなると思うぞ(だが、やるなよ)。

(2)DHCPサーバーの設定
 DHCP(ルーターの機能を利用しても良いし、解析用PCにDHCPサーバーを立てても良い)の設定で、DHCPサーバーが配信するデフォルトゲートウェイを「解析用PC」のIPアドレスにしておく。これにより、被害者からの通信を解析用PCに誘導する。

(3)解析用PCの設定  解析用PCはデフォルトゲートウェイとして動作するので、Ubuntuの設定で、/etc/sysctl.confを以下のように修正する。
#net.ipv4.ip_forward=1
↓ コメントを取る
net.ipv4.ip_forward=1
この後、PCを再起動する。これで、AP→解析用PC→ルータ→インターネットという通信が可能となる。
この段階で既に、哀れな被害者がこのAPに接続したら、その通信は解析用PCを全て通過するので、Wiresharkを起動すれば、暗号化されていない通信は全てキャプチャされる。以下にYahoo!にリクエストした場合の様子を示そう。


ここで示しているのはリクエストだが、もちろんレスポンスについてもキャプチャできる。すなわち、偽APの利用者の通信は、文字通りだだ漏れになるのだ。しかし、SSL通信については暗号化されているため中身を見ることはできない。

Secure属性のないCookieを盗聴する

さて、ここからが本題です。文体も元に戻します。SSLを使っていても、Cookieを盗聴できる場合があるというお話です。タイトルにつけたように、CookieにSecure属性がないと、盗聴が可能になります。
 まず、よくあるのは、サイト全体はHTTP(平文)だが、個人情報を扱うページのみHTTPSというサイトです。この場合、CookieにSecure属性がないと、平文通信でもCookieが送信されてしまいます。以下、実験で確認しましょう。

 まず、被害者が https://www.city.machida.kanagawa.jp/login.php にアクセスしていて、セッションクッキーが付与されているとしましょう。クッキー情報は下図の通りです。

Secure属性がついていないことに注目してください。
この状態で、前述のAPを利用して http://www.city.machida.kanagawa.jp/ (HTTPSではない)にアクセスすると、以下のリクエストが送信されます。ばっちりCookieが見えていますね。


次に、「うちのサイトは、HTTPSのみで80番ポート閉じているから大丈夫」と言う意見をよく目にしますが、これは誤解です。少し手順は増えますが、Cookieを盗聴することは可能です。
それは、罠をしかけて、 http://www.city.machida.kanagawa.jp:443/ (httpsではなくhttp)にアクセスさせることです。罠の例を以下に示します。利用者(被害者)の通信は全て解析用PCを通過するので、わざわざ罠サイトを作る必要はなく、適当にHTTPレスポンスを改変して以下を突っ込めばよいことになります。
<img src="http://www.city.machida.kanagawa.jp:443/" width="1" height="1">
これによるリクエストは下記となります(デスティネーション443ポートをHTTPとしてデコードするように指定しています)。Cookieを含むHTTPリクエストが平文でキャプチャされています。


【結論】Secure属性のないCookieは盗聴の危険性がある

ということで、秘密情報を含むCookie(セッションIDのCookieを含む)にはSecure属性をつけましょう。

クッキーを強制する

次に、この環境を用いて、Cookieの強制(追加あるいは変更)をしてみましょう。ここまで出てきたツールではCookieの変更はできないので、Burp Suiteを使うことにします。Burp Suiteを透過Proxyとして使用するために以下の2点を設定します。
  • iptablesにより80番ポート宛のパケットをローカルの8080ポートにリダイレクトする
$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
  • Burp Suiteをinvisibleモードに設定する

これで、偽APを通る80番ポート向けTCPパケットは、全てBurp Suiteを通ることになります。
次に、Burp Suiteの機能で、全てのレスポンスにSet-Cookieヘッダを付与する設定を追加します。


以上で、「偽APを通過した80番ポート宛のHTTPリクエストすると、もれなくPHPSESID=ABCD12345というクッキーが設定される」という罠ができあがりです。
さっそく試してみましょう。罠を使うなどして、偽AP利用者に http://www.city.machida.kanagawa.jp/ を閲覧させます。Burp suiteの画面は下記となります。


確かに、Set-Cookie: PHPSESSID=ABCD12345 というレスポンスヘッダが付与されています。

80番ポートを閉じているサイトの場合のクッキー強制方法

次に、攻撃対象サイトが80番ポートを閉じている場合について検討します。この条件では上記の方法は使えません。その理由は、HTTPレスポンスが帰ってこないので、そのレスポンスにSet-Cookieヘッダを付与することもできないからです(リクエストは飛ぶので、リクエストのCookieヘッダを見ることは可能です)。
しかし、やりたいことはSet-Cookieヘッダを含むHTTPレスポンスを返すだけなのですから、PROXYではなく、Webサーバーを立てて、それにリクエストをリダイレクトすることにしましょう。ということで、今度はApacheの出番です。
$ sudo iptables -t nat -L --line-numbers         ← 現在の状態を確認
Chain PREROUTING (policy ACCEPT)
num  target     prot opt source               destination
1    REDIRECT   tcp  --  anywhere             anywhere             tcp dpt:http redir ports 8080
... 省略
$ sudo iptables -t nat -D PREROUTING 1           ← 1番のルールを削除
$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dst 133.242.129.62 --dport 80 -j REDIRECT --to-port 80          ← 133.242.129.62:80来たパケットをローカルに
これにより、偽APから IPアドレス133.242.129.62 (偽町田市サイトのIPアドレス)宛のTCPパケットを解析用PC上のapacheにリダイレクトします。
さらに、クッキー設定用のPHPスクリプト(image.php)を解析用PC上に配置します。Base64エンコードされた文字列は、1ピクセル×1ピクセルのGIF画像です。
<?php
  header('Content-Type: image/gif');
  setcookie('PHPSESSID', 'image.php');
  echo base64_decode('R0lGODlhAQABAIgAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==');
後は、利用者に罠を仕掛けて、http://www.city.machida.kanagawa.jp/image.php を閲覧させるだけです。このリクエストは、解析用サーバー上のapacheにリダイレクトされて、1x1のGIF画像を表示するとともに、PHPSESSID=image.phpというCookieが、利用者のPCにセットされます。

Cookieの強制はサイト側で防御できない

以上のように、元サイト側で80番ポートを閉じている・いないに関わらず、HTTPSでCookieを利用しているサイトは、中間者攻撃により偽のCookieをセットされてしまうことが分かりました。これは、以下の理由によるものです。
  • Cookieはポートやプロトコル(http/https)をまたがって共有されている
  • Cookieのsecure属性は平文でCookieを送信しないという設定であり、Cookieをセットする(受信する)場合には効果がない
  • 既にsecure属性つきCookieがあっても、HTTPのsecure属性なしCookieで上書きされる(IE10、Google Chrome、Firefoxで確認)

Cookie Monster Bugの影響のないドメイン名でもセッション固定対策はしっかり行おう

ここまで説明したように、HTTPSを利用しているサイト(通信路上の盗聴・改ざんを許容していない)では、外部からCookieを強制されること自体は防御できないので、「Cookieを改変されても構わないようにサイトを設計する」必要があります。これには、以下が重要です。
  • セッション固定脆弱性対策を行う。具体的には、ログイン成功後にセッションIDを振り直す
    session_regenerate_id(true);
  • Cookieには、外部から変更されると困る値は入れない
  • Cookieに攻撃文字列を入れるタイプのXSSの影響を受けるので、攻撃経路がCookieだからという理由で許容してはいけない

緩和策としてHTTP Strict Transport Securityが有効

先ほど、Cookieの強制はWebサイト側で防御することはできないと書きましたが、緩和策としてであれば、HTTP Strict Transport Security (HSTS) が有効です。HSTSとは、特定サイトへの通信をHTTPSに強制する機能です。HSTSを利用するには、HTTPレスポンスヘッダとして下記をブラウザに送信します。includeSubdomainsはサブドメインまで含めてHSTSを強制するというオプションです。
Strict-Transport-Security: max-age=有効期間(秒); includeSubdomains
これを受け取った後は、max-ageで指定した期間、指定サイトにHTTPで接続することはできなくなり、強制的にHTTPSで接続されます。このため、HTTP(平文)でCookieの盗聴や強制も防御されます。PHPでの利用例を下記に示します。
header('Strict-Transport-Security: max-age=2592000; includeSubdomains');  // HSTS30日間有効
HSTSが完全な防御ではなく緩和策である理由は下記の通りです。
  • 対応ブラウザが本稿執筆時点でGoogle Chrome、Firefox、Operaであり、IEとSafariは未サポート
  • 利用者が初めて訪問するサイトや、HSTSの期限切れの状態では効果がない
このため、前述の対策を施した上で、HSTSヘッダを緩和策として実施するとよいでしょう。
一方、従来よく用いられてきたサーバー側でのHTTPSへの強制リダイレクトは効果がありません。この場合は、最初のリクエストはHTTPで送信されますが、Cookieの盗聴・強制には1リクエストあれすば十分だからです。

※追記(2013/9/30 10:15) このエントリ公開時にincludeSubdomainsの指定が抜けておりましたが、これがないと、サブドメイン上のページ(DNSも偽装すれば、存在しないサブドメインでも攻撃できる)でCookieの改変が可能になることを、はせがわようすけさんから指摘いただきました。ありがとうございます。もしも、既にHTTPで稼働しているサブドメインがある場合は、includeSubdomainsは指定できないので、HSTSによる緩和策は有効ではありません。

まとめ

通信路上に攻撃者がいる場合でも、SSLの正しい利用により通信路上でのHTTPメッセージの盗聴・改ざんを防ぐことができますが、Cookieに関して言えば、Secure属性の付与により盗聴は防げるものの、改ざん(強制・改変)については防御できないことを示しました。これにより、セッション固定攻撃の他、Cookieを攻撃経路とするXSS等の攻撃が現実の脅威となります。
結論としては、Cookieを改変できるかどうか(通信路上の攻撃者、Cookie Monster Bug)とは無関係に、Cookieを攻撃経路とする脆弱性は常に対処することを推奨します。また、盗聴防止として、CookieのSecure属性は必ず設定しましょう。


2013年9月20日金曜日

PHP5.5.4にてstrict sessionsのバグ(bug65475)が修正されたがテストがないことに気づいた

以前のエントリで、PHP5.5.2にて大垣さん提案のstrict sessionsがマージされたと報告しましたが、PHP5.5.4にて、このバグ(bug65475)が修正されました。バグの例として紹介したアクセスカウンタも、カウントアップすることを確認しました。

しかし、bug65475のテストを見て、重大な抜けがあることに気づきました。
$ cat bug65475.phpt
--TEST--
Bug #65475: Session ID is not initialized when session.usr_strict_mode=1
--INI--
session.save_handler=files
session.name=PHPSESSID
--SKIPIF--
<?php include('skipif.inc'); ?>
--FILE--
<?php
ob_start();

echo "Testing file module".PHP_EOL;
session_start();
$_SESSION['foo'] = 1234;
$_SESSION['cnt'] = 1;
$session_id = session_id();
session_write_close();

session_start();
var_dump($session_id === session_id());
$_SESSION['cnt']++;
session_write_close();

session_start();
var_dump($session_id === session_id());
var_dump($_SESSION['cnt']); // Should be int(2)
session_write_close();

--EXPECTF--
Testing file module
bool(true)
bool(true)
int(2)
このテストは、session.use_strict_mode=1 の指定がないと意味がないはずですが、見あたりません。use_strict_modeのデフォルトは 0 ですから、これだと strict sessions モードでない状態でのテストになります。
試みに、上記指定を追加して、同バグのあるPHP5.5.2およびPHP5.5.3で上記をテストしてみました。
ockeghem@php552:~/php-5.5.2/ext/session/tests$ pear run-tests bug65475.phpt
Running 1 tests
PASS Bug #65475: Session ID is not initialized when session.usr_strict_mode=1[bug65475.phpt]
TOTAL TIME: 00:00
1 PASSED TESTS
0 SKIPPED TESTS
なんと、テストをPASSしてしまいました。バグがある状態でFAILしてくれないと、テストの意味がありません。ということで、上記テストは、use_strict_mode=1の指定がないだけでなく、テストとして不完全なようです。

それでは、他のテストはどうなんだろうと思って同じディレクトリをgrepしてみましたが、use_strict_mode=1に設定したテストはないようです。
PHP5.5.1からPHP5.5.2(strict sessionsが実装された)で、ext/session/tests内のテスト(*.phpt)は197で増えていません。PHP5.5.4でphptの数は200に増えましたが、上記の他にbug65359に関するもののようです。

まとめ

  • PHP5.5.4にてstrict sessionのバグ(65475)が修正され、ようやくstrict sessionsが使えるようになった
  • 上記バグに関するテストbug65475.phptにはバグがあり、PHP5.5.3以前でもFAILしない
  • PHP5.5.4までの時点で、strict sessionsに関するテストが1つも存在しない
ということで、strict sessionsはとりあえず使えるようになりましたが、テストが一つもない状態は今後の品質上不安ですので、テストを強化することを希望します。

2013年9月11日水曜日

PHPカンファレンス2013でトークします

PHPカンファレンス2013にてトークしますのでご案内します。
  • 日時:2013年9月14日(土曜日) 10時00分~(徳丸の出番は12:00~12:45)
  • 場所:大田区産業プラザ(PiO)
  • 費用:無料(申し込みはこちら
  • 講演タイトル:安全なPHPアプリケーションの作り方2013
PHPカンファレンスでは2009年以降毎年トークさせていただいております。今年は、上記タイトルにて以下のテーマを取り上げます。
  • パスワードの守り方
  • セッションフィクセイション結局どうする
  • PHPのライフサイクルにどうつきあう?
  • HTML5セキュリティ入門
上の2つはPHPの最新版PHP5.5に関する話題です。また、PHPのリリースサイクルが短いということから、ライフサイクルについても(少し)お話しします。

HTML5のセキュリティについては少しずつ情報が出てきましたが、難しい、抽象的、結局なにが問題か分からない、という感想が多いのではないでしょうか。
そのため、架空の脆弱なレジストラ(ドメイン名屋さん)「おネーム.COM」に登場いただき、「HTML5で起こる実際の悪いこと」を見ていただこうと思います。
現在デモを準備中ですが、以下を予定しています。
  1. XHR Level2によるCSRFにてパスワード変更
  2. JSONハイジャックによる個人情報漏えい
  3. 広告モジュールのDOM Based XSS(単独では影響なし)
  4. ドメイン名販売ページでの潜在的なDOM Based XSS(単独では影響なし)
  5. 3と4の合わせ技で大変なことに…
関連技術としては下記があります。
  • XMLHttpRequest Level2 
  • localStorage
  • postMessage
  • クロスサイト・リクエストフォージェリ(CSRF)
  • DOM Based XSS

それでは、PiOでお会いしましょう。

2013年9月2日月曜日

ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策

既に報道されているように、ロリポップ!レンタルサーバーに対する改ざん攻撃により、被害を受けたユーザー数は8428件にのぼるということです。ここまで影響が大きくなった原因は、報道によると、(1)「WordPressのプラグインやテーマの脆弱性を利用」し、不正なファイルがアップロードされた、(2)パーミッション設定の不備を悪用されて被害が拡大した、ということのようです。
29日夜の時点では、攻撃者の改ざん手法について「WordPressのプラグインやテーマの脆弱性を利用」し、不正なファイルがアップロードされて「wp-config.phpの」の設定情報が抜き出されたと説明していたが、30日午後7時過ぎの説明で、この脆弱性が侵入経路となって同社のパーミッション設定の不備を悪用されたことが原因だったことを明らかにした。
「ロリポップ」のWordPressサイト改ざん被害、原因はパーミッション設定不備より引用
これ以上の詳細は本校執筆時点で公表されていないので憶測は控えますが、ロリポップからのリリースに以下の内容があることが気になるところです。
サーバーの設定を変更しFollowSymLinksを無効にしました。
当社サービス「ロリポップ!レンタルサーバー」ユーザーサイトへの第三者による大規模攻撃についてより引用
これは、httpd.confにて元々FollowSymLinksが有効になっていたか、レンタルサーバーの利用者が.htaccessによりFollowSymLinksを有効にできる状態であったという意味でしょう。この状況では、レンタルサーバーの悪意の利用者(サーバーへの侵入者を含む)が、同じサーバーを共有する別の利用者の秘密情報をシンボリックリンク攻撃により盗み読みすることができます。以下、シンボリックリンク攻撃の原理と脅威、対策について説明します。

デモ環境の説明

以下は、ロリポップに似せた設定の架空のレンタルサーバーのホームディレクトリ設定です。ユーザはsuzukiとtanakaで、それぞれsuzuki.example.jpとtanaka.example.jpがホスト名です。以下は、suzuki.example.jpの表示です。


ホームディレクトリの設定は下記となります。
$ ls -l
drwx-----x  3 suzuki      LolipopUser 4096 Aug 31 17:26 suzuki/
drwx-----x  3 tanaka      LolipopUser 4096 Aug 31 17:39 tanaka/
$
各利用者のホームディレクトリのパーミッションが701となっていて、同じグループ(LolipopUser)に属していますが、これはレンタルサーバー特有の設定です。これは、レンタルサーバーの利用者同士は同じグループに属するため、他のユーザのファイルにアクセスできず、apache等のサービスは、LolipopUserグループに属さないユーザの権限で動くため、各ユーザのファイルにアクセスできるという設定になっています。

レンタルサーバーの利用者間では、ファイルは閲覧できない

ここで、tanakaさんが、suzukiさんのファイルを閲覧できないことを示します。
$ su - tanaka
Password:  ←パスワードを入力
tanaka$ pwd
/home/tanaka
tanaka$ ls -l
total 8
-rw-r--r-- 1 tanaka LolipopUser  465 Sep  1 23:45 log.txt
drwxr-xr-x 2 tanaka LolipopUser 4096 Sep  1 23:39 www
tanaka$ ls -l ../suzuki/
ls: cannot open directory ../suzuki/: Permission denied  ← 別ユーザのディレクトリは参照できない
tanaka$ cat ../suzuki/www/index.html
cat: ../suzuki/www/index.html: Permission denied  ← 別ユーザのコンテンツも同様
tanaka$

閲覧権限がないファイルにシンボリックリンクを設定できる

ところが、tanakaさんは、suzukiさんのファイルに対してシンボリックリンクを設定することは可能です。
tanaka$ cd www
tanaka$ ln -s ../../suzuki/www/index.html suzuki.html
tanaka$ cat suzuki.html
cat: suzuki.html: Permission denied  ← シンボリックリンクは作れるが参照はできない
tanaka$
上記のように、シンボリックリンクは、権限のないファイルに対して設定することができます(存在しないファイルにも可)。上記のように、このシンボリックリンクを指定してもファイルを閲覧することはできませんが、apacheによる表示は可能です。apacheは、シンボリックリンク自体とリンク先の両方にアクセス権があるからです。


ただ、これだと、元々公開している情報を別のホストで表示しているだけなので、攻撃としての価値はあまりありません。問題は、この方法で以下が可能になる場合があることです。
  • CGIプログラムやPHPスクリプトのソースが閲覧できる
  • 非公開ディレクトリのファイルが閲覧できる場合がある

閲覧できない情報をシンボリックリンク攻撃により表示する

以下、これを試してみましょう。suzukiさんのホームページ上にメールフォームがあり、そのファイル名が inquiry.php であることが分かっているとします。以下のように、これに対して inquiry.txt というシンボリックリンクを設定します。
tanaka$ ln -s ../../suzuki/www/inquiry.php inquiry.txt
これを閲覧すると、下記となります。


拡張子を.txt に変更したことで、PHPスクリプトのソースが見えてしまっています。
実は、拡張子が.php のままだと、レンタルサーバー環境の場合、PHPスクリプトは実行できないと考えられます。レンタルサーバーの場合は、CGIプログラムやPHPスクリプトは、suEXECにより、各ユーザの権限で動作します。上記のシンボリックリンクの場合、tanakaの権限により、suzukiのスクリプトを実行しようとしますが、今まで見たように、tanakaの権限ではスクリプトファイルの読み込みができないからです。しかし、apacheの実行ユーザだと読み込み権限があるため、ソースの閲覧は可能です。
さて、上図のソースから、ログファイルが ../log.txt だと分かります。このファイルにもシンボリックリンクを設定して、閲覧できるか調べてみましょう。
tanaka$ ln -s ../../suzuki/log.txt log.txt
閲覧画面は下記となります。


上記のように、通常は閲覧できないディレクトリ上の、攻撃者に読み込み権限のないデータファイルも、シンボリックリンクを設定することで、外部から閲覧できることが分かりました。この際のlog.txtのパーミッションは下記となっています。
tanaka$ su - suzuki
Password:
suzuki$ ls -l log.txt
-rw-r--r-- 1 suzuki LolipopUser 465 Sep  1 23:45 log.txt
suzuki$
すべてのユーザーに対して読み込み権限が与えられています。これは、PHPスクリプトのfopenで作成したファイルに与えられるパーミッションですが、仮にアプリケーションの実行に最低限の権限(600)が設定されていたら、apacheから読み込むことができず、このファイルに対するシンボリックリンク攻撃は成立しません。

シンボリックリンク攻撃が成立する条件

冒頭に書いたように、シンボリックリンク攻撃が成立する条件は下記の両方が成立する場合です。
  • FollowSymLinksが有効になっているか、攻撃者が.htaccessによりFollowSymLinksを有効にできる
  • 攻撃者が、公開ディレクトリにシンボリックリンクを設定できる
通常のWebサーバーでFollowSymLinksが問題にならないのは、後者の条件が成立しないからです。一方、レンタルサーバーの場合は、以下のいずれかにより、攻撃者が公開ディレクトリにシンボリックリンクを設定可能です。
  • 攻撃者がレンタルサーバーのユーザとなる(お試し等でも可)
  • 攻撃者がレンタルサーバーのユーザtanakaを攻撃して、tanakaの書き込み権限を得る
通常のWebサーバーでも、攻撃者が書き込み権限を得ることはあり得ますが、他のユーザの権限を得る動機があまりないところが、レンタルサーバー環境との違いです。

シンボリックリンクを作成する時点で、相手側のディレクトリ一覧は参照できない場合が多いのですが、以下の手順でファイル名が推定可能です。
  • 外部公開のURLからファイル名を推定する
  • スクリプトのソースファイルからファイル名を得る
  • WordPress等の標準的なファイル構成からファイル名を得る

シンボリックリンク攻撃の脅威

一般的には、シンボリックリンク攻撃が成立すると、攻撃者の有する権限よりも高い権限を獲得することができます。上記の例では、攻撃者が持っているtanakaの権限に加えて、apacheの権限により、権限を持たないファイルも閲覧可能になります。
おおざっぱに言って、シンボリックリンク攻撃の影響は、ディレクトリトラバーサル攻撃の影響と似ています。

対策

一般に、シンボリックリンク攻撃が問題になる場合は、ファイルのオープン時にシンボリックリンクかどうかを確認して、シンボリックリンクの場合はエラーにします(参考)。apacheの場合は、FollowSymLinksを無効にすることで、これが実現できます。
しかし、これだけではだめです。攻撃者が.htaccessの設定で、FollowSymLinksを設定する可能性があるからです。このため、AllowOverride ディレクティブにて、Noneを指定するか、Options=にて、許可したいオプション(FollowSymLinks以外)を明示的に列挙する方法があります。
利用者側でシンボリックリンクを安全に設定したい場合は、SymLinksIfOwnerMatchを利用できます。これは、「シンボリック先のファイルまたはディレクトリが、 シンボリックリンクの所有ユーザ ID と同じ場合にのみシンボリックリンクを たどれるようにします。」というものです(参照)。
レンタルサーバーの利用者が上記を確認する方法についてはここでは説明しませんので、レンタルサーバー事業者に個別に確認いただくのがよいと考えます。レンタルサーバー事業者は、今回の事件を受けて、自社の設定状況と安全性を説明いただく(あるいは急いで設定を修正する)とよいでしょう。

まとめ

シンボリックリンク攻撃の脅威と対策について説明しました。これは、シンボリックリンクを悪用して、上位権限のプロセスにファイルをアクセスさせる手法です。前述のように、レンタルサーバー環境で、apacheのFollowSymLinksが有効な場合、シンボリックリンク攻撃が成立してしまいますが、これはレンタルサーバー利用者の中に悪意のユーザが存在する可能性があるからです。
似たような状況として、Androidアプリケーションがあります。Androidアプリケーションは、アプリケーション毎にLinuxのアカウントが割り当てられるので、悪意のアプリケーション(マルウェア)が端末に導入された場合、root権限で動作するプロセスに対してシンボリック攻撃を仕掛け、マルウェアがroot権限を奪取するような攻撃に使われます。

追記(2013/09/03 10:00)

さとうふみやすさんから、apacheの-FollowSymLinksでは、レースコンディションによりシンボリックリンク攻撃の防御が回避されてしまうという指摘をいただきました(参照: Apache HTTPD: `Options -FollowSymLinks` は不完全)。さとうさん、ありがとうございます。
これは、TOCTOU 競合状態という問題で、参考文献として紹介したJPCERT/CCのドキュメントでも言及されています。また、TOCTOU競合を避ける方法についても、同じJPCERT/CCの解説に、「POS35-C. シンボリックリンクの有無のチェック時の競合状態を避ける」として紹介されています。ただ、これはapache(などのhttpd)を開発する側でとれる対策で、apacheの利用者としては、apache側で対策してくれるのを待つしかありません。
さとうさんは、『この攻撃の根本的な対策方法は「シンボリックリンクを作らせない」 しかない』としていますが、これもなかなか難しいと思います。現在のレンタルサーバーの多くは、シンボリックリンク攻撃のTOCTOU競合については受容している(あきらめている)のだと理解しています。
利用者側でとれる対策としては、CGIやPHPのスクリプトや、これらからアクセスするファイルのパーミッションを600や400として、apacheから読み取らせないようにすることです。ただし、.htaccessや.htpasswdはapacheから読めないとまずいので、この方法では保護できません。
ということで、この問題が受容できない利用者は、さとうさんの言及されている「ユーザーごとに別権限の Web サーバーを立ち上げる」サービスを提供しているレンタルサーバーやVPSなどに移行するしかないと考えます。
(追記終わり)

参考文献

2013年8月26日月曜日

PHP5.5.2以降のstrict sessionsモードでセッションフィクセイション対策はどうすればよいか

先日のエントリ『【速報】PHP-5.5.2にて大垣さんのstrict sessionsが実装されました』にて、PHP5.5.2でセッションアダプションが解消されたことを報告しました(session.use_strict_mode=1の場合)。
セッションアダプションとは、未初期化のセッションID(たとえばPHPSESSID=ABC)をPHPが受け入れる問題のことです。strict sessionsを使用すると、PHPが生成し、現在有効であるセッションIDのみを受け入れ、そうでない場合、PHPはセッションIDを振り直します。

あいにくPHP5.5.2(PHP5.5.3も)にはバグがあり、session.use_strict_mode=1によるstrict sessionsは使用できませんが、既に大垣さん自身によりバグ修正されているので、PHP5.5.4からは使えるようになるでしょう。

strict sessionsの主な目的は、セッションフィクセイション攻撃に対する影響緩和ですが、では、strict sessionsを用いると、セッションフィクセイション対策はどのように変わるでしょうか?

従来のPHP、すなわちセッションアダプションがある状態では、セッションフィクセイション対策は下記の通りでした。
  • ログイン直後にsession_regenerate_id関数によりセッションIDを振り直す
これに対して、セッションアダプションがない状態ではどうでしょうか。先に『セッションアダプションがなくてもセッションフィクセイション攻撃は可能』で説明したとおり、攻撃者が、対象サイトのセッションIDを取得することにより、セッションフィクセイション攻撃は可能です。従って、strict sessionsの状況でも、セッションフィクセイション対策は下記の通りです。
  • ログイン直後にsession_regenerate_id関数によりセッションIDを振り直す
何も変わらないわけです。

…とここで終わりにしてもよいのですが、せっかく大垣さん8年越しのstrict sessionsが使えるようになったので、セッションIDの振り直しをしないでセッションフィクセイション対策ができないか考えてみましょう。

先に説明したように、セッションアダプションがない状況では、攻撃者は、対象サイトから有効なセッションIDを取得する必要があります。ここに着目し、「攻撃に使えるセッションIDを攻撃者に渡さない」アプローチを考えてみます。セッションアダプションがなく、セッションIDがとれない状況では、セッションフィクセイション攻撃はできません。

まず、上記がだめな状況として、ログイン状況でなくてもセッションを使っているサイトが挙げられます。この場合、サイトにアクセスさえすれば有効なセッションIDが取得できるため、セッションフィクセイション攻撃が可能になります。このため、以下では、セッションはログイン状態でのみ使用しているという前提とします。

以下、ログイン状態でのみセッションを使っているアプリケーションを想定して、セッションIDの振り直しをしないでセッションフィクセイション対策する方法を検討します。以下の処理毎に検討します。
  • ログイン前
  • ログイン処理
  • ログイン状態の確認処理
  • ログアウト処理

ログイン前

前述のように、ログイン前にセッションを有効にすると、そのセッションIDを悪用してセッションフィクセイション攻撃ができてしまいます。このため、ログイン前にはセッションは使えません。これによる副作用として、ログイン時にCSRF対策したい場合でも、それが困難になります。CSRFの標準的な対策にはセッションを利用するからです。

ログイン処理

ログイン処理の注意として以下があります。
  • ログインに失敗した場合は、セッションID悪用防止のためセッションを破棄する
  • 元々ログイン状態の場合は、ログイン処理を継続しない。攻撃者のログイン状態のセッションIDによる攻撃を防ぐため
これを実現する擬似コード例を下記に示します。
session_start();
if (ログイン済みの状態) {
  echo "ログイン済みです";
  // マイページなど、遷移可能なページへのリンクを表示
  // あるいは、異常事態としてセッション破棄するという考え方もあり
} else if (idとパスワードが有効) {
  // ログイン処理
  $_SESSION['user'] = $id;
  // その他の処理
} else {
  echo "IDまたはパスワードが違います";
  // 認証失敗の場合はセッションを破棄
  session_destroy();
}
重要なポイントとしては、(1)ログイン済みなのに他のユーザでログインすることを許さない、(2)認証に失敗した場合は必ずセッションを破棄する、ということがあげられます。
また、懸念点として、ログイン処理中にアプリケーションが異常終了すると、session_destroyが呼び出されず、有効なセッションが残ってしまう可能性があります。対策は、セッションを有効にする区間をできるだけ短くすることですが、詳しい説明を割愛します。

ログイン状態の確認処理

ログイン前の攻撃者がセッションIDを取得する方法として、攻撃者が、認証の必要なページにアクセスするというものがあります。
典型的なログイン確認は以下の通りです。
session_start();
if (! isset($_SESSION['user'])) {
  echo 'ログインしてください';
  // ログインページへのリンクを表示
  exit;
}
これだと、セッションIDは生成され、セッションは有効なままなので、攻撃者は有効なセッションIDを取得できます。これを防ぐには、exitする前にセッションを破棄します。
session_destroy();
これにより、セッションは有効でなくなるため、セッションフィクセイション攻撃に使えるセッションIDは取得できなくなります。この処理は、ログイン状態を確認するページ全てで、もれなく実装する必要があります。

ログアウト処理

ログアウト処理において重要なポイントは、かならずセッションを破棄するということです。そうしないと、ログアウト後に残ったセッションのセッションIDを用いて、セッションフィクセイション攻撃ができてしまいます。
具体的には、下記のようなログアウト処理はだめだと言うことです。
$_SESSION['user'] = false; // 認証ユーザをfalseにすることでログアウトとする

アプリ側のセッションタイムアウトに対する注意

アプリケーション仕様によっては、PHPのセッションは有効だが、アプリ側で定めたセッションタイムアウトになっているという状況が考えられます。この場合、「ログアウト状態だがセッションIDは有効」という状態になり、セッションフィクセイション攻撃に使えるセッションIDができてしまいます。
この対策としては、下記が考えられます。
  • ログイン状態の確認処理の中で、タイムアウトしたセッションを破棄する
  • ログイン処理においては、タイムアウトしたセッションは、いったん破棄して、再度セッションを開始する。
ログイン状態の確認…については、前述の処理でカバーされているとも考えられますが、ログイン処理の方は特に注意が必要です。

一方、PHP処理系側でタイムアウトしたセッションは、既に破棄されているため、アプリケーション側で特に注意することはありません。

まとめ

strict sessionsにおいても、セッションフィクセイション攻撃対策として、認証成功直後のsession_regenerate_idは必須ですが、敢えてこれをしないで、セッションフィクセイション攻撃対策する方法を検討し、以下が必要であることを示しました。
  • ログイン前にはセッションを使用しない
  • ログインに失敗した場合は、セッションを破棄する
  • 元々ログイン状態の場合は、ログイン処理を継続しない
  • ログイン状態の確認において、ログイン状態でない場合はセッションを破棄する
  • ログアウト処理ではセッションを破棄する
  • ログイン状態の確認処理の中で、タイムアウトしたセッションを破棄する
  • ログイン処理においては、タイムアウトしたセッションは、いったん破棄して、再度セッションを開始する
ご覧のように、strict sessionsによってセッションフィクセイション脆弱性を気にしないですむどころか、至る所でセッションフィクセイション脆弱性に配慮しなければならないことがわかりました。上記の1カ所でも漏れると、セッションフィクセイション脆弱性の可能性が生じます。

したがって、strict sessionsにおいても、下記を推奨します。これだと、セッションフィクセイション対策は、ログイン処理1カ所に集約できます。
  • ログイン直後にsession_regenerate_id関数によりセッションIDを振り直す
ということで、strict sessionsにおいても、アプリケーションの書き方は変わらない、というのが結論です。

フォロワー

ブログ アーカイブ