TL;DR 概要
- Sonarは、WordPressコアにおいて認証されていないブラインドSSRF脆弱性を発見しました。これにより攻撃者は、認証なしで任意の内部または外部URLに対してサーバーにHTTPリクエストを発行させることができます。
- この脆弱性は、URLを十分に検証せずに処理するWordPressの機能を悪用し、ネットワークの偵察やファイアウォールの背後にある内部サービスへの潜在的なアクセスを可能にします。
- ブラインドSSRFは、クラウド環境では特に危険で、インスタンスメタデータエンドポイントに到達し、機密情報を取得する可能性があります。
- WordPress管理者は、セキュリティパッチを直ちに適用するべきです。この発見は、成熟した広く監査されたコードベースでも未発見の脆弱性クラスが含まれる可能性があることを示しています。
WordPressは世界で最も人気のあるコンテンツ管理システムで、全ウェブサイトの40%以上で使用されています。この広範な採用により、脅威アクターやセキュリティ研究者にとって、セキュリティ問題を報告することで報酬を得られる公開バグバウンティプログラムを通じて、主要なターゲットとなっています。
脆弱性ブローカーも、WordPressインスタンスを乗っ取ることができる未修正の脆弱性を取得することに非常に興味を持っており、時には重要なものに対して最大30万ドルを提供することもあります。そのため、WordPressは広範にレビューされたコードベースを持っており、研究者が簡単に見つけられるものは期待されていません。このターゲットに関する以前の研究では、セキュリティ問題を発見するために広範な専門知識と努力が必要でした。
このブログ記事では、WordPressのピンバックの実装における驚くほど単純な脆弱性について説明します。WordPressの場合、この脆弱性の影響はほとんどのユーザーにとって低いですが、関連する脆弱なコードパターンは非常に興味深いものであり、ほとんどのウェブアプリケーションにも存在する可能性が高いです。このブログ記事の目的は、このパターンについて教育し、意識を高めることです。
開示
この脆弱性は1月21日にWordPressに報告されましたが、まだ修正はありません。WordPressインスタンスに適用する可能性のある修正については、パッチセクションを参照してください。
未修正の脆弱性の詳細を公開するのは初めてであり、この決定は軽くは行われませんでした。この問題は、最初に約6年前の2017年1月に別の研究者によって報告され、その後も多くの人々によって報告されてきました。私たちの報告とさらなる調査の後、今日取り上げるのと同じ動作を文書化した複数の公開ブログ記事を特定することもできました。
そのままでは影響が低く、以前に公開されており、サードパーティソフトウェアの追加の脆弱性と連鎖する必要があるため、このリリースがWordPressユーザーを危険にさらすことはなく、インスタンスを強化するのに役立つと考えています。
影響
他の脆弱なサービスに依存せずに、この動作を利用して脆弱なインスタンスを乗っ取る方法を一般的に特定することはできませんでした。
例えば、最近のConfluence OGNLインジェクション、@orange_8361によって発見されたJenkinsの壮大なリモートコード実行、またはAssetNoteによって文書化された他のチェーンの1つを使用して、影響を受けた組織の内部ネットワークで他の脆弱性の悪用を容易にする可能性があります。
技術的詳細
ピンバック機能における脆弱な構造の使用
ピンバックは、他の「友人」ブログが特定の記事を参照したときにブログの著者に通知し表示する方法です。コメントと一緒に表示され、自由に受け入れたり拒否したりできます。内部では、ブログはリンクの存在を確認するために互いにHTTPリクエストを行う必要があります。訪問者もこのメカニズムをトリガーすることができます。
この機能は、攻撃者が数千のブログに対して単一の被害者サーバーでピンバックを確認するように悪意を持って要求することで、分散型サービス拒否攻撃を実行できるため、広く批判されています。ピンバックは、個人ブログにおけるソーシャルおよびコミュニティ機能の重要性から、WordPressインスタンスでデフォルトで有効になっています。ただし、これらのリクエストが同じサーバーまたはローカルネットワークセグメントにホストされている他の内部サービスに送信されることは期待されていません。
ピンバック機能は、WordPressのXML-RPC APIで公開されています。これは、クライアントが引数と共に呼び出す関数を選択できるXMLドキュメントを期待するAPIエンドポイントです。
実装されているメソッドの1つはpingback.pingで、引数pagelinkedfromとpagelinkedtoを期待しています。最初のものは2番目のものを参照する記事のアドレスです。
pagelinkedtoは、ここでのローカルインスタンスの既存の記事を指す必要がありますhttp://blog.tld/?p=1、pagelinkedfromはpagelinkedtoへのリンクを含むべき外部URLを指します。
以下は、このエンドポイントへのリクエストがどのように見えるかを示しています:
クリップボードにコピー
POST /xmlrpc.php HTTP/1.1
Host: blog.tld
[...]
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param>
<value><string>http://evil.tld</string></value>
</param>
<param>
<value><string>http://blog.tld/?p=1</string></value>
</param>
</params>
</methodCall>URL検証の実装
WordPressコアメソッドwp_http_validate_url()は、ユーザー提供のURLに対していくつかのチェックを行い、悪用のリスクを減らします。例えば:
- 宛先にユーザー名とパスワードを含めることはできません;
- ホスト名には次の文字を含めることはできません: #:?[]
- ドメイン名は127.0.0.1、192.168.*などのローカルまたはプライベートIPアドレスを指してはいけません。
- URLの宛先ポートは80、443、または8080のいずれかでなければなりません。
3番目のステップでは、URLにドメイン名が含まれている場合(例: http://foo.bar.tld)、ドメイン名の解決が必要になることがあります。この場合、リモートサーバーのIPアドレスはURLを解析することで取得されます[1]、その後、非公開IP範囲を除外するために検証される前に解決されます[2]:
src/wp-includes/http.php
$parsed_url = parse_url( $url ); // [1]
// [...]
$ip = gethostbyname( $host ); // [2]
if ( $ip === $host ) {
// Error condition for gethostbyname().
return false;
}
// IP validation happens here
}
// [...]検証コードは正しく実装されているように見え、URLは信頼されたものと見なされます。次に何が起こるのでしょうか?
HTTPクライアントの実装
URLを検証した後、利用可能なPHP機能に基づいて、2つのHTTPクライアントがピンバックリクエストを処理できます: Requests_Transport_cURLとRequests_Transport_fsockopenです。これらはどちらも、WordPressの傘下で独立して開発されたRequestsライブラリの一部です。
後者の実装を見てみましょう。その名前から、PHPストリームAPIを使用していることがわかります。トランスポートレベルで動作し、クライアントはHTTPリクエストを手動で作成する必要があります。URLは再びparse_url()を使用して解析され、そのホスト部分がPHPストリームAPIと互換性のある宛先を作成するために使用されます(例: tcp://host:port):
wp-includes/Requests/Transport/fsockopen.php
public function request($url, $headers = array(), $data = array(), $options = array()) {
// [...]
$url_parts = parse_url($url);
// [...]
$host = $url_parts['host'];
else {
$remote_socket = 'tcp://' . $host;
}
// [...]
$remote_socket .= ':' . $url_parts['port'];さらに先に進むと、この宛先はstream_socket_client()を使用して新しいストリームを作成するために使用され、HTTPリクエストが作成されて書き込まれます:
wp-includes/Requests/Transport/fsockopen.php
$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
// [...]
$out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']);
// [...]
if (!isset($case_insensitive_headers['Host'])) {
$out .= sprintf('Host: %s', $url_parts['host']);
// [...]
}
// [...]
fwrite($socket, $out);ご覧のとおり、このプロセスには別のDNS解決が含まれており、stream_socket_client()がパケットを送信するためにホストのIPを特定できるようにしています。
他のHTTPクライアントであるcURLの動作も非常に似ており、ここでは取り上げません。
脆弱性
この構造には問題があります: HTTPクライアントはリクエストを送信するためにURLを再解析し、ホスト名を再解決する必要があります。その間に、攻撃者がドメインを変更して、以前に検証されたものとは異なるアドレスを指すようにすることができます!
このバグクラスは、Time-of-Check-Time-of-Useとも呼ばれます。リソースが検証されますが、その後、実際に使用される前に変更される可能性があります。これは、サーバーサイドリクエストフォージェリ(SSRF)に対する緩和策でよく見られる脆弱性です。この脆弱なコードパターンに基づいたチャレンジを、Code Security Advent Calendar 2021でリリースしました。
<div class="table"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Can you spot the vulnerability? <a href="https://twitter.com/hashtag/codeadvent2021?src=hash&ref_src=twsrc%5Etfw">#codeadvent2021</a> <a href="https://twitter.com/hashtag/csharp?src=hash&ref_src=twsrc%5Etfw">#csharp</a> <br/><br/>SSRF vulnerabilities are so 2020! <a href="https://t.co/y9CSxdc5MH">pic.twitter.com/y9CSxdc5MH</a></p>— Sonar (@SonarSource) <a href="https://twitter.com/SonarSource/status/1468248939379847168?ref_src=twsrc%5Etfw">December 7, 2021</a></blockquote> <script src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script> </div>これらの連続したステップがどのように見えるかを、以下の図でまとめました:

悪用シナリオ
意図しないポートに到達したり、POSTリクエストを実行したりすることを可能にするパーサーの差分バグを見つけることを期待してコードを監査しましたが、成功しませんでした。初期のURL検証ステップは、それらの悪用を防ぐのに十分な制限があります。前述のように、攻撃者はこの動作を他の脆弱性と連鎖させる必要があります。
パッチ
この公開時点で利用可能な公開パッチは知られていません。上記の詳細は、開示プロセス中に共有された中間パッチに基づいています。
このような脆弱性に対処するには、HTTPリクエストを実行するまで検証されたデータを保持する必要があります。検証ステップの後に破棄または変換されるべきではありません。
WordPressのメンテナは、このパスをたどり、wp_http_validate_url()に2番目のオプションの引数を導入しました。このパラメータは参照によって渡され、WordPressが検証を行ったIPアドレスを含みます。最終的なコードは、古いバージョンのPHPに対応するために少し冗長ですが、主なアイデアはここにあります。
一時的な回避策として、システム管理者はXMLRPCエンドポイントのハンドラーpingback.pingを削除することをお勧めします。これを行う1つの方法は、使用中のテーマのfunctions.phpを更新して、次の呼び出しを導入することです:
add_filter('xmlrpc_methods', function($methods) {
unset($methods['pingback.ping']);
return $methods;
});また、ウェブサーバーレベルでxmlrpc.phpへのアクセスをブロックすることも可能です。

