10万のWordPressサイトにSQLインジェクションを見つけた話

最近、10万以上のサイトで有効化されていたWordPress検索プラグインRelevanssiに、認証不要のSQLインジェクションを報告しました。このバグが特に面白かった理由は二つあります。ひとつは、数値のタームIDにしか見えない入力に余分なSQLを載せられる型の取り違えがあったこと。もうひとつは、最初のクエリの制約を回避するために、ひとつのSQLインジェクションのペイロードを別のSQLインジェクションの中に忍ばせるという手口が使えたことです。

この記事では、実際のコードパスをたどりながら、なぜこのバグが悪用可能だったのか、そしてcatstagsの検索パラメータを使ってどうやって実用的なブラインドSQLインジェクションにしたのかを説明します。

Relevanssiは、WordPress標準の検索機能をより高機能な検索エンジンに置き換えるプラグインです。関連度の高い順に並べたり、追加のフィルタリングをしたりできるのですが、今回のバグが公開クエリパラメータ経由で到達可能だったのは、その追加の検索機能のおかげでした。

脆弱な入力

RelevanssiはWordPress検索を拡張していて、検索結果を絞り込むための追加クエリパラメータをいくつか受け付けます。そのうち二つがcatstagsで、カテゴリータグで絞り込むために使われます。

カテゴリーの場合、プラグインはcatsパラメータを読み取り、カンマで分割し、その結果をタームIDとして保存します。

if ( isset( $query->query_vars['cats'] ) ) {
	$cat = $query->query_vars['cats'];
	if ( is_array( $cat ) ) {
		$cat = implode( ',', $cat );
	}
}
if ( empty( $cat ) ) {
	$cat = get_option( 'relevanssi_cat' );
}
if ( $cat ) {
	$cat         = explode( ',', $cat );
	$tax_query[] = array(
		'taxonomy' => 'category',
		'field'    => 'term_id',
		'terms'    => $cat,
		'operator' => 'IN',
	);
}

tagsパラメータも同じような形で処理されます。どちらの場合も、プラグインはタームIDを扱っているつもりで、後続のコードもその前提に依存しています。

どこでおかしくなるのか

バグが出るのはもう少し後で、Relevanssiがtaxonomy queryを処理し、ユーザー入力のターム値をterm taxonomy IDに変換しようとする部分です。

foreach ( $terms_parameter as $name ) {
	$term = get_term_by( $field_name, $name, $taxonomy );
	if ( ! $term ) {
		if ( ctype_digit( strval( $name ) ) ) {
			$numeric_terms[] = $name;
		}
	} elseif ( isset( $term->term_id ) && in_array( $field_name, array( 'slug', 'name' ), true ) ) {
		$names[] = "'" . esc_sql( $name ) . "'";
	} else {
		$numeric_terms[] = $name;
	}
}

return array(
	'numeric_terms' => implode( ',', $numeric_terms ),
	'term_in'       => implode( ',', $names ),
);

重要なのは最後のelseです。get_term_by()がタームオブジェクトを返した場合、元の値が数値として検証されないままnumeric_terms配列に追加されます。

一見すると大したことがないように見えますが、WordPressがIDによるターム検索をどう処理するかを見ると話が変わります。

if ( 'id' === $field || 'ID' === $field || 'term_id' === $field ) {
	$term = get_term( (int) $value, $taxonomy, $output, $filter );
	if ( is_wp_error( $term ) || null === $term ) {
		$term = false;
	}
	return $term;
}

検索前に値が整数へキャストされています。つまり、1fooのような文字列は検索時にはタームID 1 として扱われ、もしID 1のタームが存在すれば検索は成功します。しかしnumeric_termsに追加されるのは元の文字列である1fooです。

これは、一見もっともらしく見えるコードでもバグにつながるPHP特有の挙動のひとつです。PHP 8でも警告なしに1fooのような文字列がこっそり1になる、というのは、型に関する前提が危険であることをよく示しています。Relevanssiは、本来は汚染された文字列である元の値を、正しい数値IDとして検証済みであるかのように扱ってしまっていました。

インジェクションポイント

最終的に、それらの値はこのSQLクエリに連結されます。

if ( ! empty( $numeric_terms ) ) {
	$type    = 'term_id';
	$term_in = $numeric_terms;
}

if ( ! empty( $term_in ) ) {
	$row_taxonomy = sanitize_text_field( $row['taxonomy'] );

	$tt_q = "SELECT tt.term_taxonomy_id
			  FROM $wpdb->term_taxonomy AS tt
			  LEFT JOIN $wpdb->terms AS t ON (tt.term_id=t.term_id)
			  WHERE tt.taxonomy = '$row_taxonomy' AND t.$type IN ($term_in)";
	$term_tax_id = $wpdb->get_col( $tt_q );
}

ここでは、$term_inにカンマ区切りの数値IDしか入っていない前提になっています。しかし元の値が整数に変換されていないため、攻撃者が制御するSQLをIN (...)句に紛れ込ませることができます。

[続きを読む]

100日間の機械学習

2017年8月、初めてソフトウェア開発の仕事についたときに、あることを誓いました。

IT系仕事につくことが数年の努力の成果でした。日本語学習、プログラミングの勉強、基本情報技術者試験の準備、そして日本での就職活動:このハードルを全部乗り越えないとできなかったことです。転職した時点は新しいことばかり:新しい職種、新しい環境、初めての第二言語での仕事。慣れるまでには時間がかかることを知っていました。一方で、慣れてきたころに罠があることも知っていました。慣れてきた自分に満足してしまうと成長が止まり、なりたい開発者にはなれなくなってしまうのです。

誓ったことはこうです:仕事に慣れたら必ず学び続けます。

[続きを読む]

Fedora 25にHugoをインストール

他のLinuxディストリにも

今回の記事はFedoraリナックスに静的ウェブサイトジェネレーターHugoのインストールについてです。Hugo(発音:ヒューゴ)はOctopressJekyllと同様、コンテンツをMarkdownで記載し、タイトルなどの設定を決め、テーマを選ぶだけで簡単にウェブサイトを作れます。そしてWordpressなどと違って出来上がるサイトはデータベースを使わないため、サイトのセキュリティと速さが抜群。

Hugoは機能豊富で処理スピードが恐ろしく早い、本当に優れているツールです。ただ、現時点ではHugoのRPMがないので、FedoraなどのRPMディストリを使っている人にインストールが困難かもしれません。

私の場合は色んなインストールの仕方(非公式RPM、snap、ソースコードからコンパイル)を検討したけど、なかなか最新バージョンを簡単にインストールする方法はありませんでした。調べていたときは何度もUbuntuでできるsudo apt-get install hugoを「いいな〜」と思いました。

でもありがたいことに、結局答えが簡単でした。

[続きを読む]

第二言語でFE試験

去年の5月に基本情報技術者試験(FE試験)を合格することができました。

おめでとうございます!FE試験ってどんな試験でしたっけ?

一言でいうと、ITシステムに関する知識やスキル全般を対象とする国家試験です。

じゃ、プログラミングとかを勉強するの?

ITシステムといえばプログラミングなんだけど、システムを作るのに必要なものはプログラミング以外にもたくさんあります。なので、プログラミングが試験範囲の本の一部です。他に2進数や論理回路、CPUとメモリ、OSの仕組み、ファイルやデータベース、ネットワーク、セキュリティ、プロジェクトマネジメント、システム構成…内容がさまざまです。経営戦略や財務会計だって試験に出てきます。システム開発の部にプログラミング以外にも結構ありますね(たとえばテストのしかたとか)。

え、多くないですか?

本当に多かったですね(笑)。こういうような全部を対象とする試験が西洋になかなかありませんね。でもそのおかげでたくさん新しいものを覚えて、結構ためになったと思います。

[続きを読む]