読者です 読者をやめる 読者になる 読者になる

Cat.6(ねころっく)

脆弱性の金で焼肉が食べたい

PHPオブジェクトインジェクションの攻撃

PHPオブジェクトインジェクションの攻撃に関してまとめました。Webのインジェクション攻撃というと、SQLインジェクションやOSコマンドインジェクションなどが先に思い浮かびますが、PHPにはオブジェクトインジェクションと呼ばれるものがあります。

PHPにおけるオブジェクトインジェクションは、データのシリアライズに関係するunserialize関数を使用している場合、外部から安全でないシリアライズされた値が注入されることにより、脆弱性になりうる可能性があります。どのような影響があるかどうかは、コードの書き方によって様々です。

本稿は、独自の検証、調べによるもののため、厳密には誤りであったり、そもそも違っているということがあるかもしれません。

 

脆弱性を再現するコード

簡単に脆弱性を再現するコードを用意しました。次の攻撃シナリオはローカルのhostsファイル(/etc/hosts)をオブジェクトインジェクション攻撃によって露呈させるものです。(/etc/passwdなども表示させることが可能です。)

事前準備として、次の2つのPHPファイルを適当なWebディレクトリに配置します。

 index.php

<?php
require_once 'Viewer.php';
unserialize(base64_decode($_GET['serialize']));

Viewer.php

<?php
class Viewer {
  private $file;

   public function __construct($filename) {
      $this->file = $filename;
   }

   public function __destruct() {
    echo file_get_contents($this->file);
  }
}

index.phpBase64の値を付加した、次のURLにアクセスします。

index.php?serialize=Tzo2OiJWaWV3ZXIiOjE6e3M6MTI6IgBWaWV3ZXIAZmlsZSI7czoxMDoiL2V0Yy9ob3N0cyI7fQ== 

f:id:reinforchu:20170327115303p:plain

その結果、オブジェクトインジェクション攻撃が成功し、ローカルのhostsファイルが表示されました。

なお環境は、macOS上のXAMPP 5.6(PHP5.6)で検証しました。Windows環境の場合は/etc/hostというファイルパスが存在しないので失敗します。

 

●説明

index.phpは、QueryStringのパラメータ名serializeで受け取った値をBase64デコードを行いデシリアライズするものです。

Viewer.phpは、コンストラクタの引数に与えられたファイルパスをメンバ変数に格納し、デストラクタが呼ばれた時に、メンバ変数に格納されたファイルパスの内容を表示するクラスです。

index.phpのコードにはViewerクラスのインスタンスを生成するコード(new演算子)は書かれていないのにも関わらず、何故/etc/hostsファイルが表示されてしまったのでしょうか。※インスタンスが生成されていたとしても、書き方によっては影響を受けます。

まずは怪しいBase64データをデコードしてみます。

O:6:"Viewer":1:{s:12:"Viewerfile";s:10:"/etc/hosts";}

かなり怪しい文字列が出てきました。これはViewerクラスのオブジェクトをシリアライズしたものです。 このシリアライズされたデータは、Viewerクラスのメンバ変数fileに/etc/hostsがセットされた「状態」のオブジェクトです。そのシリアライズされたデータをデシリアライズすることにより、そのオブジェクトの状態がインスタンス化されます。

unserialize関数は、任意のコード実行する、任意のメソッドを呼ぶ、という挙動はせず、unserialize関数自体は悪さをしている訳ではありません。PHPにおけるオブジェクトインジェクション攻撃は、unserialize関数を介して生成されたオブジェクトのマジックメソッド(__destruct()など)の挙動を悪用した攻撃です。マジックメソッドは明示的に呼び出されなくとも実行する挙動をします。

つまり、今回作成した脆弱性を再現するコードを例に取ると、unserialize関数によって外部から任意のオブジェクトが生成され、そのオブジェクトの__destruct()が呼び出されることによって攻撃が成功し、/etc/hostsファイルが露呈しました。

 

◆参考情報

PHP: マジックメソッド - Manual

 

●攻撃へのアプローチ

実際に攻撃を試みる場合、次のような検証コードを書いてExploitを作っていくことになります。ソースコードが分かっている場合は、非常にやりやすいです。ターゲットとなるクラスのソースコードや、構造をエスパーするのは時間がかかるかと思います。

特定のフレームワークなど、ソースコードが既に分かっている場合に攻撃を受けると思われます。(後述を参照)

evil.php

<?php
require 'Viewer.php';
$obj = new Viewer('/etc/hosts');
echo 'Base64: ';
echo base64_encode(serialize($obj));
echo '<br>Serialize: ';
echo serialize($obj);
echo '<br><pre>';

このコードを実行すると、次のような結果が得られます。

f:id:reinforchu:20170328154728p:plain

オブジェクトをシリアライズするために、ターゲットのクラスをrequireします。その後インスタンス化し、そのオブジェクトをシリアライズするものです。今回、Base64エンコードしてデータを受け渡すので、Base64エンコードしています。

その結果、serialize=Tzo2OiJWaWV3ZXIiOjE6e3M6MTI6I... という値にたどり着きます。

 

●実際にあった脆弱性の例

オブジェクトインジェクション攻撃が成立する条件として、少なくとも外部からの入力値をデシリアライズする必要があり、実際のところ、(主観的ですが)脆弱性になりうる可能性は低いと思われます。ある程度はCookie, Sessionで対応可能ですので、serializeが採用される(が適切とされる)シーンは少ないと思われます。シリアライズによるデータの受け渡しを行っていたとしても、今回作成した脆弱性を再現するコードのような、あからさまに脆弱性を仕込むような書き方(ファイルを書き込む・読み込むなど)もそう無い思いますし、実際の攻撃にはクラス名が特定・推測(エスパー)されるか、ソースコードが露呈しているような状況を要するため、時間がかかる面倒くさいタイプです。

しかしながら、一つを気をつけていただきたいのが、フレームワークCMSやライブラリを使用している場合脆弱性になりうる可能性があります。開発者側がシリアライズを使用していなくとも、フレームワーク等の内部的な処理においてシリアライズが使用されているケースがあり、その実装に問題があった場合、脆弱性に繋がります。さらに攻撃者の都合の良いことに、それらフレームワークソースコードは誰でも入手可能である場合が多く、Exploitが作成しやすくなります。

実際過去に、CakePHPでunserialize()に起因する任意のコードが実行可能の脆弱性が報告されました。下記に既知脆弱性の例をいくつか挙げます。

CakePHP "unserialize()" PHP Code Execution Vulnerability - CVE-2010-4335

Securityコンポーネントにおいて、一連のCSRF Token処理で入力値の検証を行わずにunserialize関数に渡されるため、悪意のあるコードを仕込んだデータを送ることにより、キャッシュの設定によっては任意のコードが実行可能です。

Payload

:B:3:"Ncc":4:{f:7:"__pnpur";f:3:"onz";f:5:"__znc";n:2:{f:4' \
':"Pber";n:1:{f:6:"Ebhgre";f:42:"../gzc/pnpur/crefvfgrag/pnxr_pber_sv' \
'yr_znc";}f:3:"Sbb";f:49:"<? ernqsvyr(\'../pbasvt/qngnonfr.cuc\'); rkv' \
'g(); ?>";}f:7:"__cnguf";n:0:{}f:9:"__bowrpgf";n:0:{}}

鋭い方はお気づきかと思いますが、内部ではrot13で処理していたようですね。Exploitも出回っています。

◆参考情報

CVE - CVE-2010-4335

Security Advisory SA42211 - CakePHP &quot;unserialize()&quot; PHP Code Execution Vulnerability - Secunia

 

PHPにおける任意の型のセッションデータを挿入される脆弱性 - CVE-2016-7125

昨年のPHPカンファレンス2016の徳丸先生の公演で、PHPのext/session/session.cの脆弱性を組み合わせ、オブジェクトインジェクション攻撃を成功させる内容がありました。当時のスライドが公開されているのでご紹介します。一時話題になったJoomla!のセッション汚染の脆弱性など、まだ記憶が新しい内容です。

www.slideshare.net

私もPHPカンファレンス2016に参加しました。

◆参考情報

CVE - CVE-2016-7125

JVNDB-2016-004620 - JVN iPedia - 脆弱性対策情報データベース

 

以下JVNの情報のみ列挙(すべてではありません)

PEAR HTML_AJAXPHP Serializer における PHP オブジェクトインジェクションの脆弱性

JVNDB-2017-001736 - JVN iPedia - 脆弱性対策情報データベース

・Simple Machines Forum の LogInOut.php における PHP オブジェクトインジェクション攻撃を実行される脆弱性

JVNDB-2016-007678 - JVN iPedia - 脆弱性対策情報データベース

・Magento CE および EE における PHP オブジェクトインジェクション攻撃を実行される脆弱性

JVNDB-2016-007051 - JVN iPedia - 脆弱性対策情報データベース

・Subrion CMS の includes/classes/ia.core.users.php における PHP オブジェクトインジェクション攻撃を実行される脆弱性

JVNDB-2017-001116 - JVN iPedia - 脆弱性対策情報データベース

・CS-Cart 用アドオン「Twigmo」における PHP オブジェクトインジェクションの脆弱性

JVN#55389065: CS-Cart 用アドオン「Twigmo」における PHP オブジェクトインジェクションの脆弱性

Malware Information Sharing Platform における PHP オブジェクトインジェクション攻撃を実行される脆弱性

JVNDB-2015-007234 - JVN iPedia - 脆弱性対策情報データベース

WordPressプラグイン「Welcart e-Commerce」における PHP オブジェクトインジェクションの脆弱性

JVN#47363774: WordPress 用プラグイン「Welcart e-Commerce」における PHP オブジェクトインジェクションの脆弱性

WordPressプラグイン「Ninja Forms」における PHP オブジェクトインジェクションの脆弱性

JVN#44657371: WordPress 用プラグイン「Ninja Forms」における PHP オブジェクトインジェクションの脆弱性

・SPIP の ecrire/inc/filtres.php の encoder_contexte_ajax 関数における PHP オブジェクトインジェクション攻撃を実行される脆弱性

JVNDB-2016-002065 - JVN iPedia - 脆弱性対策情報データベース

Joomla! における PHP オブジェクトインジェクション攻撃を実行される脆弱性

JVNDB-2015-006456 - JVN iPedia - 脆弱性対策情報データベース

これだけではありません、探せばまだまだ出てきます。2015年〜2016年頃の脆弱性情報の一部を列挙しましたが、年に10個あるかないか程度出てきてますので、今後(2017年)も出てくるのではないでしょうか。CMSWordPressプラグイン等に多くある傾向のようなので、注意が必要ではないかと思います。 

 

●対策方法 

結局のところ、外部からの入力を伴うデータの受け渡しには、シリアライズを使わないことが対策になります。Cookie, JSONなど様々な手法がありますので、そちらを検討ください。内部的に完結するのであれば、Session変数を使うなども対策案の一つです。入力値を検証する対策も一定の効果はありますが、完全ではありません

PHPの公式ドキュメントにも、警告と対策方法が記載されています。

ユーザーからの入力をそのまま unserialize() に渡してはいけません。 アンシリアライズの時には、オブジェクトのインスタンス生成やオートローディングなどで コードが実行されることがあり、悪意のあるユーザーがこれを悪用するかもしれないからです。 シリアル化したデータをユーザーに渡す必要がある場合は、安全で標準的なデータ交換フォーマットである JSON などを使うようにしましょう。json_decode() および json_encode() を利用します。

外部に保存されているシリアル化されたデータをアンシリアライズする必要がある場合は、 hash_hmac() を使ったデータの検証を検討しましょう。 他者によるデータの改ざんがないことを確かめるためです。

引用元:PHP: unserialize - Manual

PHP7の場合、unserialize関数の問題を解消するために、Filtered unserializeが実装されました。この実装によって、unserializeの引数にクラスを指定ができるようになり、受け付けるクラスを制限することが可能です。とはいえ、完全な対策になる訳ではありません。(詳細は後述)

 

●PHP7におけるFiltered unserialize

上述のとおり、PHP7ではunserialize関数に受け付けるクラスを指定できるようになりました。使い方は次の通りです。

// 引数の省略, 既定でTRUEとして振る舞い全てのクラスを受け付けます
unserialize($str);
// 全てのクラスを拒否します
unserialize($strfalse);
// classAとclassBのみ受け付けます
unserialize($strarray("classA""classB"));

Filtered unserializeにより、意図しないクラスのインスタンス化は抑止出来ますが、許可したクラスを悪用されることも想定されるため、安全とは言い切れません。デシリアライズを配列のみ使用しているケースなどには有効です。

 

◆参考情報

PHP: unserialize - Manual

 

●関連情報

PHPにおけるオブジェクトインジェクション攻撃に関しての関連記事です。

PHPのunserialize関数に外部由来の値を処理させると脆弱性の原因になる | 徳丸浩の日記

PHP unserialize()が__destruct()を実行する? - Shin x blog

PHPにおけるオブジェクトインジェクション脆弱性について — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something