SucuriによるWordPress脆弱性の統計

プラグイン作者必読!実例に学ぶ脆弱なWordPressプラグインの作り方、又はwp-adminを守る理由

セキュリティ・コンサルティング Sucuri2014年11月の記事 によると、WordPress プラグインの3大脆弱性は、SQL インジェクション(SQLi)、クロス・サイト・スクリプティング(XSS)、ファイル・インクルージョン(FI)とのことです。

冒頭のグラフ は最新の分析結果で、この事実を裏付けています。プラグイン開発の チュートリアル手引き には、脆弱性を作り込まないためのポイントが上手にまとめられているのに、なぜ無くならないのでしょうか?

「ヒトが作るものだから、バグがあって当然」と言ってしまえばそれまでですが、 Sucuri のブログ を読み漁っていると、こと脆弱性に関する限り、 WordPress に特有の「思い込み」や「見過ごし」といった、ヒトの心理的・認知的な盲点が原因の多くを占めているんじゃないかと思えてきます。

だから単に PHP のセキュリティ How To ではなく、私が公開している唯一のプラグイン の機能設計と脆弱性を作り込まない実装のために勉強したたこと − 実際に攻撃を試し脆弱性を追跡して見えてきたこと − を中心にまとめてみたいと思います。ただし具体的な攻撃方法は、元記事以上に詳しくは触れません、あしからず 😉 。

SQL インジェクション

端的には、 OWASP の SQL インジェクション チートシート に示されている「防御の3原則+2」が基本なんだと思います。

OWASP の SQL インジェクション チートシート

優先すべき防御策:
  • オプション #1: プリペアド・ステートメント を使う(パラメータ化されたクエリ)
  • オプション #2: ストアード・プロシージャ―を使う
  • オプション #3: ユーザーからの入力は全てエスケープする
追加の防御策:
  • 追加で強化すべき: 最小限の権限で実行する
  • 追加で検討すべき: 入力はホワイトリスト方式で検証する

これらの防御原則に対し WordPress では、 $wpdb->prepare()esc_sql()、あるいは 役割りと権限 を検証する current_user_can() などを準備することで、開発物をセキュアにするプログラミング環境を提供しています。

故に、これらの原則に従って対応する関数を正しく使っていれば、 SQLi は起きないハズなんですが…

次の例は「思い込み」に起因すると思われる SQLi の例です。

Custom Contact Forms に見る「思い込み」の盲点

次のコードは、① 設定を SQL ダンプしダウンロードする、② バックアップしておいた SQL をインポートする、という管理者向け機能のコードの一部です。

if (!is_admin()) {
    ...
    $custom_contact_front = new CustomContactFormsFront();
} else {
    ...
    $custom_contact_admin = new CustomContactFormsAdmin();
    ...
    add_action('init', 'adminInit');
}

/* 設定のエクスポート、インポート(管理者向け)*/
function adminInit() {
    $this->downloadExportFile();
    $this->downloadCSVExportFile();
    $this->runImport();
}

上記コードには、最低でも5つの問題点が考えられます。

  1. 「管理者」かどうかの検証を is_admin() に頼っている
  2. 予め想定された経路が前提で、別経路でアクセスされた場合を想定していない
  3. 生の SQL 文を出力する仕様のため、DB のプレフィックスが暴露される
  4. SQL のエクスポート、インポートに際し、権限を検証、限定していない
  5. インポートでは、任意の SQL 文が実行できる

結果、誰でも DB の書き換えが可能という深刻な脆弱性が生じてしまいました。

ダウンロードされた生のSQL文

ダウンロードされた生のSQL文

1. について言えば、ここでの is_admin() の使い方は必ずしも間違いじゃないと思います。ただし 2. においては、「正規にログインした管理者がダッシュボードからアクセスする」という「予め想定された経路」以外にも、 admin-ajax.phpadmin-post.php を直接叩くという経路があり得ます。これらには admin の名が付いてはいますが、管理者以外でも機能する仕様となっており、おまけに is_admin()true にします。

よって、ある条件で admin-ajax.php にアクセスすると SQL ダンプが始まってしまうという、「想定外の経路」が存在する構造になっていたのです。

Sucuri が作者に連絡するも応答がなかったため、WordPress のセキュリティ・チームが急遽、対策版をリリースしました。その内容は、単に init へのフックを削除し、 adminInit() の実行を阻止するというものでした。

仮に SQLi の防御原則に則った実装をするなら、以下の様にすべきだったでしょう。

  • nonce(後述)を検証して特定ページからの要求である事を確認し、
  • current_user_can('manage_options') で権限を検証し、
  • データベースへの入出力はすべてパラメータ化、
  • さらに入出力時には、値の検証と無害化をした上で、
  • 適切に組み立てられエスケープされた安全な SQL 文を実行する。

この原則が見逃されてしまった要因には、 is_admin() への誤った「思い込み」が盲点となった事が挙げられるのではないでしょうか。フォーラムでのディスカッション が、そのことを如実に物語っているように思います。

全く同じ「思い込み」の元凶に admin_init フックがあり、 admin-ajax.php などにアクセスするとトリガーがかかるようになっています。テーマ Platform任意の PHP コードが注入できてしまう脆弱性 や、プラグイン MailPoet任意のファイルがアップロードできてしまう脆弱性 が同種の例として報告されちゃってます。

admin の名が付いたファイルやフックには気を付けろ!」ってことですネ。

2015年2月18日 追記

admin_init の公式ドキュメント には、管理画面へのアクセスを管理者に限定するコードが載っていますが、一般ユーザーによる公開ページの Ajax も許容するコードになっているので、コピペする時は注意しましょう 😀 。

XSS

XSS の詳しい話は 専門家 に譲りますが、ほとんどの場合、外からやって来るデータ(DB から読み出したものを含む)の検証漏れか、出力前のエスケープ漏れでしょう(「安全なウェブサイトの作り方」によると、前者は「保険的対策」、後者は「根本的対策」とされています)。

まずは、「これじゃあ漏れも仕方ない」的な例を紹介します。

Blubrry PowerPress に見る「見過ごし」の例

下の表を見れば分かりますが、 Blubrry PowerPress では、最初から XSS が未対策だったワケじゃありません。

対策 PHP htmlspecialchars esc_html esc_attr
ファイル数 行数
42 36280 274 82 153
33 22807 160 53 131

対策前後のコードの一例を以下に示しますが… 何と言うか… 一言で表せば、追跡するのがとても厄介なコードです(すいません、想像してください 😐 )。そして至る所にパッチが当てられ、その対策の慌てっぷりがヒシヒシと伝わってきます 🙁 。

/* before */
<input type="hidden" ... value="<?php echo empty($_POST['tab']) ? 0 : $_POST['tab']; ?>" />
/* after  */
<input type="hidden" ... value="<?php echo empty($_POST['tab']) ? 0 : intval($_POST['tab']) ); ?>" />

/* before */
powerpress_page_message_add_error( ..., $_POST['feed_slug']) );
/* after  */
powerpress_page_message_add_error( ..., esc_html($_POST['feed_slug']) ) );

/* before */
echo '<rawvoice:metamark type="'. $MetaMark['type'] .'"';
/* after  */
echo '<rawvoice:metamark type="'. esc_attr($MetaMark['type']) .'"';

/* before */
'<em>'. htmlspecialchars($post_title) .'</em>',
'<em>'. $feed_slug .'</em>' );
/* after  */
'<em>'. htmlspecialchars($post_title) .'</em>',
'<em>'. htmlspecialchars($feed_slug) .'</em>' );

こういったヤッツケ仕事的パッチワークの問題点は、「対策漏れ」がないかの検証が難しいこと、そして htmlspecialchars()esc_html() の混在など、統一性のない対策方針が果たして正しいのか判断がつかない事ではないでしょうか?

私には、混在(要は、省略されたパラメータの扱い)が直ちに問題となる例題は示せませんし、実際、大丈夫なのかもしれません。しかし WordPress という系の中では、コアチームが熟成させてきた関数群を使うべきだと思っています(例えば esc_html() の内部では 厳密性の高い htmlspecialchars() の使い方 がされている)。

追記:徳丸さんの 『例えば、PHPを避ける』以降PHPはどれだけ安全になったか | 徳丸浩の日記 の第3パラメータについて詳しく解説されていました。

入力の検証と出力のエスケープ、およびモデルとビューの分離

「見過ごし」を防ぐ上で大事なことは、「モデル」における「入力の検証」と、「ビュー」における「出力のエスケープ」と言った具合に、見通しのよい設計と実装をする事だと思います(注:ここでは MVC の話をしているつもりはありません)

ちょっと厳しい言い方ですが、先のコードはこれら2つが混沌としているため、「対策漏れ」が起きて当然と言われても仕方ありません。

Codex の Validating Sanitizing and Escaping User DataData Validation日本語版)には、XSS や SQLi、 DT など、様々な攻撃を阻止するための入出力に関する検証と無害化の関数群がリストアップされています。

ただし一部、日本で語られている考え方やネーミングと、多少の違いと混乱があるので要注意です。例えば、不要なタグやコードを削除する「入力の検証(クリーニング)」の関数群が sanitize_*() というネーミングになっていたり、「出力のサニタイジング」にエスケープ処理を施す関数群 esc_*()sanitize_text_field() が混在していたり、といった具合です。

いずれにしても、見通しの良い設計と共に、これらの関数群をコードの文脈に合わせて適切に使うことが肝要でしょう。

最後に蛇足ですが、IPA セキュア・プログラミング講座 にある「スクリプトが動作する箇所」の図と WordPress の esc_*() 関数との対応を取ってみました。

図を描いてみて分かったのですが、 expression 攻撃を阻止する関数は無さそうなので、外から入力されたデータを style 属性に注入するのは避けた方が良さそうですネ。

スクリプトが動作する箇所と対応するWordPress関数

スクリプトが動作する箇所と対応するWordPress関数

HD FLV Player に見る「見過ごし」の例

FLASH PLAYER PLUGIN の古いバージョンと PHP バージョン 5.3.4 以前との組み合わせには、任意のファイルがダウンロードできるという脆弱性が存在します。

Sucuri の報告 には、以下の様な download.php のコードが掲載されています。

$finename = $_GET['f'];
header('Content-disposition: attachment; filename=' . basename($filename));
readfile($filename);

実際にはこれほど単純ではなく、 $finename に対して、絶対パスの追加と拡張子の検証が行われています。しかし、ディレクトリ・トラバーサルの検証(validate_file() で可能です)や NULL バイト攻撃 の検証が見過ごされたため、一撃で破られるシングル・フェイルとなってしまいました。

また download.php が WordPress とは無関係に単独で実行出来るようになっていたという点も問題でしょう。wp-load.php を読み込んだり、 admin-ajax.phpadmin-post.php 経由で nonce や権限を検証するなど、 WordPress の仕組みを使うべきだったのではないか、と思います。

先の htmlspecialchars() にも言える事ですが、僅かな処理時間の増加をケチったんだとすると、その結果、脆弱性を作り込んでしまっては、本末転倒と言えるのではないでしょうか?

2015年5月23日 追記

PHP 5.3.4 以前には、ファイル・システム系の関数が NULL バイトの攻撃に弱いという問題 がありましたが、5.3.4 で チェックが強化 されました。

手元の環境で調べてみた結果は以下の通りです。

Ver. $file = "/etc/passwd¥0.php";
is_file( $file ) is_readable( $file )
5.2.14 true true
5.4.4 false false

当然このまま include( $file ) とかすると、パスワードがバッと表示されちゃいます。WordPress は PHP 5.2.4(推奨は 5.4 以上)から使える事になっているので、拡張子を調べるだけじゃダメで、 str_replace( "¥0", "", $file ) などによる NULL バイトの無効化が必須というワケです。

この辺りは PHP と Web アプリケーションのセキュリティについてのメモ がとても参考になるので、一読を激しく推奨します。

権限昇格(PE

WordPress における nonce を一言で表せば、「今、このページにアクセスしているユーザーだけが知りえる秘密の情報」です。詳しくは拙作の WordPressプラグインのコーディングでありがちな10の間違いと設計時に考慮すべきこと初歩からわかるWordPressのnonceでAjaxをセキュアに実装する方法 を参照してください。

でもその nonce だって漏れるんです。漏れると、単なるユーザーが管理者の権限にまで昇格しちゃうんです。次は、そんな脆弱性の紹介です。

UpdraftPlus に見る「思い込み」の盲点

登録ユーザーのダッシュボード上で wp-admin/?action=hogehoge にアクセスすると hogehoge_handler() にフックされるようにしたとします。

add_action( 'admin_action_hogehoge', 'hogehoge_handler' );

このハンドラは wp-content/plugin/hogehoge/admin.php で定義され、「何か」する目的で、ブラウザに次の様な出力をするものとします。

function hogehoge_handler() {
    ...
    ?>
    ...
    <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce('hogehoge-secret-nonce');?>">
    ...
    <?php
}

この登録ユーザー向け秘密情報のタネである 'hogehoge-secret-nonce' が、管理者向けのタネと共通で使われていたりすると、ちょっと心配な事になります。いわゆる「秘密情報の漏洩」ですネ。

さらに別の管理者向けハンドラでは、 check_admin_referer('hogehoge-secret-nonce') による nonce の検証を行っただけで管理者権限の検証が漏れたりすると、いわゆる「権限昇格」が起きてしまうのです。

UpdraftPlus が正にこのパターンにハマってしまいました。また同様の脆弱性は、あの有名な WPtouch でも 報告されています

これらも一種の「思い込み」と言えるでしょう。

CSRF

CSRF が起きる原因は単純です。

例えば管理画面で「変更を保存」のボタンを押すと、内部で保存処理をして新しい画面に遷移しますが、遷移前の画面に nonce が埋め込まれていなかったり、保存処理の前に nonce がチェックされていなかったりすると、即 CSRF となります。

この場合、保存処理の所で管理者の権限を検証するだけでは CSRF は防げません。管理者が自らのクッキーを持ったまま悪意のあるリンクを踏んで起きる脆弱性だからです。

「管理者かどうかチェックしているから大丈夫」という「思い込み」があったのでしょうか、 Sucuri のデータベース からは、有名どころの W3 Total Cache を始め、この脆弱性を持ったプラグインが数多く検索されます。

オプションの保存処理であれば、 nonce の生成・検証と管理者権限の検証の両方を確実に行ってくれる Settings API を積極的に使うのが吉でしょう。

2018年2月18日 追記

WordPress での CSRF 対策 – nonce ってナンスか? がとても分かり易かったので、一読推奨です 😀 。

ファイル・インクルージョン(FI

FI は、ファイルシステム系の関数に検証が十分でない入力を与える事で起きる脆弱性です。2014年9月には、 file_get_contents() を介して 任意のファイルがダウンロードできてしまう という Slider RevolutionLFI が報告されました。

Slider Revolution に見る「思い込み」の盲点

管理者向けのある機能に対し、admin-ajax.php を経由した次の様な攻撃で、任意のファイルが漏洩します(本サイトにも未だに時々やってきます)。

http://victim.com/wp-admin/admin-ajax.php?action=revslider_show_image&img=../wp-config.php
Slider Revolutionの脆弱性により漏洩したwp-config.php

Slider Revolutionの脆弱性により漏洩したwp-config.php

Slider Revolution では、管理者用クラスの中で admin-ajax.php に対するアクション・ハンドラが登録されます。しかし同ハンドラでは、以下の様な検証が全く行われないまま file_get_content() を呼び出していました。

  • お約束の nonce を検証していない
  • 実行しようとしているユーザーの権限を検証、制限していない
  • ディレクトリー・トラバーサルの検証やパスの限定など、入力の検証と無害化をしていない

問題の構造は冒頭の SQLi と同じですネ。 admin-ajax.php のネーミングに由来する「管理者向け」という「思い込み」が原因です *。想定が「管理者」だとしても、何らかの脆弱性で権限昇格だってあり得るんだから、他の検証を省略できるってことにはならないのです!

* 断定根拠: 有料販売もしているプラグインですョ、上記の様な基本的な検証が全く実装されていないなんて、あり得ないじゃないですか!

まとめ

今回分析した案件の多くが、admin の名前が付く関数やアクションフックに対する「思い込み」によるものと推察(一部は断定)しました。また数の多い XSS では、「見過ごし」が起き難い設計が重要との認識を新たにしました。

心構えとしては、常に攻撃側/非攻撃側という2つの側面から、コードの1行1行に「盲点がないか」を問いかけるってことでしょうか。

とどのつまり、性善説じゃダメで、次のような性悪説を元に開発に臨むというのが個人的な結論です。

  • 入力は汚染されている
  • DB は改ざんされている
  • 秘密の情報は漏洩する
  • 管理者権限は回避、奪取される
  • 攻撃は想定外の経路から来る
  • 実際に今、ページにアクセスしているワケではない

攻撃者はさらにプラットフォーム(OS やら PHP、時には WordPress コア)自体に内在する脆弱性とを組み合わせてきます。

これらを全て防ぐのって大変なことですが、こと WordPress という エコシステム に関して言えば、防御に必要な仕組みはコア開発の方々が揃えてくれているので、後は「使いこなし」ってことで、この記事を〆たいと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません