2017年9月25日月曜日

安全でないデシリアライゼーション(Insecure Deserialization)入門

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

(2017/11/22追記)
OWASP Top 10 2017に正式に公開され、そのA7に安全でないデシリアライゼーション (Insecure Deserialization) が入りました。これは、本稿で扱うオブジェクトインジェクションと同内容ですが、OWASPの表記にならい、タイトルを変更しました。

以下、「そんなプログラムあり得るか?」という現実性についてはあまり気にしないで、原理的にオブジェクトインジェクションがどのようなものかについて順を追って説明していきます。以下、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セキュアソリューションズ株式会社

フォロワー

ブログ アーカイブ