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

2016年12月30日金曜日

PHPのescapeshellcmdを巡る冒険はmail関数を経てCVE-2016-10033に至った

エグゼクティブサマリ

2011年始めに徳丸がescapeshellcmdの危険性を指摘したが、この問題はmail関数のadditional_parameters経由で攻撃可能であることが2013年末に指摘された。その後2016年末に、PHPMailerの脆弱性CVE-2016-10033として現実のものとなった

経緯


escapeshellcmdはなぜ危険か

経緯のところで説明したように、escapeshellcmdはコマンド文字列全体をエスケープするための関数です。
サンプルとして以下のプログラムを考えます。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i $mail";
system($cmd);
このプログラムは外部から指定されたメールアドレスに対して空のメールを送信するものです(実用上の意味はありませんが説明を簡単にするためですのでご容赦ください)。…が、一見して分かるようにOSコマンドインジェクション脆弱性があります。例えば、メールアドレスとして下記のメールアドレスを指定します。
sample@example.jp; cat /etc/passwd
実行されるコマンドは下記となり、メール送信に続いて、cat /etc/passwd が実行されます。
/usr/sbin/sendmail sample@example.jp; cat /etc/passwd
このような攻撃を防ぐために、escapeshellcmdは下記のように用います。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i $mail";
$cmd = escapeshellcmd($cmd);
system($cmd);
この場合、実行されるコマンドは下記となり、OSコマンドインジェクションは避けられます。
/usr/sbin/sendmail -i sample@example.jp\; cat /etc/passwd
しかし、cat /etc/passwdという余計なオプションがついてしまいますので、$mailをダブルクォートで囲むことも一案です。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i \"$mail\"";
$cmd = escapeshellcmd($cmd);
system($cmd);
実行結果は下記となります。
/usr/sbin/sendmail -i "sample@example.jp\; cat /etc/passwd"
今度は ; cat /etc/passwdも含めてメールアドレスの一部となり、メール送信はエラーになりますが、OSコマンドインジェクションもオプション追加も避けられています。
しかし、このような使い方を想定すると、ダブルクォート「"」はエスケープしてはいけないことになり、実際エスケープされていません。このため、escapeshellcmdのマニュアルには以下のように記載されています。
' および " は、対になっていない場合にのみエスケープされます
ということは、メールアドレスとして下記を指定した場合、
-OQueueDirectory=/tmp" "-X/tmp/exploit.php" "sample@example.jp
実行されるコマンドは下記となり、パラメータインジェクション攻撃が成立します。
/usr/sbin/sendmail -i "-OQueueDirectory=/tmp" "-X/tmp/exploit.php" "sample@example.jp"
この攻撃が成立する理由は、前述のとおり、escapeshellcmdの「' および " は、対になっていない場合にのみエスケープされます」という仕様によるものであり、これはescapeshellcmdのユースケースを考えると不可避であり、仕様の欠陥といえるものです。このため、OSコマンドインジェクション対策には、escapeshellcmdの利用を避け、escapeshellarg関数を用いる必要があります。
CVE-2016-10033の本質は、mail関数とmb_send_mail関数が、OSコマンドインジェクション対策として内部的にescapeshellcmd関数を呼び出しているために、潜在的にパラメータインジェクションに対して脆弱である点をついたものです。

CVE-2016-10033 PoCの巧妙さ

実際のCVE-2016-10033は上記の説明とは少し違います。mail関数は、宛先をメールヘッダから取得する -t オプションが指定されていますが、PHPMailerはSender(エンベロープFrom)の設定するため、sendmailコマンドの-fオプションをmail関数の第5パラメータadditional_parameters経由で指定しています。典型的なsendmail呼び出しは下記となります。
/usr/sbin/sendmail -t -i -fsample@example.jp
ご覧のように-fオプション全体やメールアドレスはダブルクォートで囲まれていません。しかし、これを突破するのは容易ではありません。なぜなら、
  • PHPMailerはSenderプロパティに対してRFC準拠のメールアドレスであることを確認している
  • sendmail呼び出しの前にescapeshellcmdによるエスケープ処理がなされている
  • sendmailコマンドのパラメータチェックを回避する必要がある
この3点を同時に満足するメールアドレスを見つける必要があるからです。私が先に示したPoCでは以下のメールアドレスを使いました。
"a \"  -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp
このメールアドレスは、RFC5321等を満たしています。escapeshellcmdを通ったあとのsendmailコマンド呼び出しは下記となります。
/usr/sbin/sendmail -t -i  -f"a \\" -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp
"は偶数個あるためエスケープされませんが、バックスラッシュ「\」はエスケープされています。この不整合により、/bin/sh経由で呼び出されたsendmailコマンドが受け取るパラータは下記となります。
-t
-i
-fa \
-OQueueDirectory=/tmp
-X/tmp/exploit.php
\a@example.jp
「a \」と「\a@example.jp」はメールアドレスが必要なパラメータであり、RFCには違反していますが、sendmailは一旦これらをエラーとせずメール配送を開始するため、攻撃が成立してしまいます。

mail関数はどうすればいいか

mail関数の今のマニュアルには、以下のように書かれています。
escapeshellcmd() が自動的に適用されるため、 インターネット RFC でメールアドレスとして許可さているいくつかの文字を使用することができません。 mail() はそうした文字を許可しないため、プログラム中でそうした文字の使用が必須である場合、 メール送信の代替手段(フレームワークやライブラリの使用など)が推奨されます。
PHP: mail - Manual より引用
つまり、RFCよりも制限を厳しくしたバリデーションを施さないと危険だということですが、この状態はよろしくないと考えます。現に、PHPMailerだけでなく、Swift Mailerにも同種の指摘(CVE-2016-10074)がなされていますが、PoCが同じなので、mail関数の仕様による問題と考えられます。

これを改善するには、additional_parametersとして現在の文字列パラータに加えて、配列を許すようにする案が考えられます。配列としてパラメータを一つずつ指定できれば、escapeshellarg関数により安全にエスケープ処理が行えるからです。

まとめ

2011年はじめにescapeshellcmdの問題点を指摘した後、2016年末にPHPMaierの脆弱性CVE-2016-10033という重大な問題として顕在化した流れを報告しました。PHPは長い歴史の中で安全性を高めており、この記事や、このスライドにまとめたことがありますが、まだ危険な関数は残っています。今回の問題は、escapeshellcmd、mail、mb_send_mail関数の仕様が問題でした。
今回の問題を機に、さしあたりmail関数とmb_send_mail関数の仕様が見直されることを期待しています。

2016年12月29日木曜日

PHPのmail関数、mb_send_mail関数のマニュアルに警告が追記されていた

昨日の記事PHPMailerの脆弱性CVE-2016-10033について解析したにて、PHPMailerの脆弱性CVE-2016-10033の原因はmail関数が内部で呼んでいるescapeshellcmd関数の仕様が原因であると指摘しました。そして、mail関数の危険性については、小邨孝明さんが2013年12月23日の記事にて指摘していました。
mb_send_mail関数(mail関数も同様)ですが、第5引数(additional_parameter)にユーザの入力を使用する場合は注意が必要です。mb_send_mail関数の第5引数は、内部でescapeshellcmd(内部関数名:php_escape_shell_cmd)によって引数の文字列全体がエスケープされます。

escapeshellcmd() は、以前に徳丸さんから OS コマンドインジェクションの防止には不適切であると指摘されています。ユーザの入力値を十分にチェックしておかないと、sendmailコマンドに任意のコマンドライン引数を渡されてしまうことになります。
mb_send_mail(),mail()で第5引数を設定する際の注意点 より引用
私はこの記事が書かれて直ぐに読んでいましたが、残念ながら多くの方が知るとこではなかったように思います。しかし、今日になって、小邨さんの注意喚起がPHPのマニュアルに追記されていることに気が付きました。


additional_parameters(オプション)
パラメータ additional_parameters は、 追加のフラグをコマンドラインオプションとしてメール送信プログラムに渡す際に使用可能です。 メール送信プログラムは、設定オプション sendmail_path により設定されます。例えば、 sendmail を使用する際に -f オプションを使って エンベロープの sender アドレスを設定する際に使用できます。
このパラメータはコマンドの実行を防止するために内部的に escapeshellcmd() によってエスケープされます。 escapeshellcmd() はコマンドの実行を防止しますが、 別のパラメータを追加することは許してしまいます。セキュリティ上の理由から、 シェルコマンドへの望ましくないパラメータの追加を避けるために、 ユーザーはこのパラメータを適切に処理することが推奨されます。
escapeshellcmd() が自動的に適用されるため、 インターネット RFC でメールアドレスとして許可さているいくつかの文字を使用することができません。 mail() はそうした文字を許可しないため、プログラム中でそうした文字の使用が必須である場合、 メール送信の代替手段(フレームワークやライブラリの使用など)が推奨されます。
PHP: mail - Manual より引用
私は、CVE-2016-10033の主原因はPHPMailerというよりもPHPのmail関数側にあると考えていましたが、上記のようにマニュアル上で「免責」されていたことになります。
それでは、いつごろこの注意書きが追加されたのだろうとarchive.orgで確認したところ、下記のような結果でした。
したがって、2014年1月6日から同年2月9日のどこかのタイミングでこの注意書きが追記されたことになります。いずれにせよ、小邨さんの記事からほどなく、PHP本家のマニュアルに注意書きが追記されたことになります。

これはこれで立派なことですが、PHP利用者は常にマニュアルの変更を見張っているわけではないので、もう少し注意喚起を工夫できなかっただろうかと感じました。

フォロワー