entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

pixiv Bug Bounty Program 2018

こんにちは、セキュリティエンジニアのkoboです。ピクシブでは2016年より脆弱性報奨金制度を運用していますが、2018年度に入ってから報奨金の増額や新しいプラットフォームへの参入など、これまでに増して注力しています。本記事では、最近のピクシブの脆弱性報奨金制度の動向と実際に報告された脆弱性の例を紹介していきます。

pixiv Bug Bounty Programの概要

  • 期間: 2016/04〜
  • 支払い済み報奨金総額: 300万円程度
  • 報告総数: 294件

ピクシブでは2年半ほどに渡って脆弱性報奨金制度を実施してきましたが、2018年に入ってから脆弱性報告の件数、クオリティ向上の為に2つの重要な変更を行いました。

  1. 報奨金の増額
    • 脆弱性を報告するハッカーに対してこれまでよりも高いインセンティブを提供することで報告を促すため
  2. HackerOneへの参入
    • 世界最大のバグバウンティプラットフォームの利用により、BugBounty.jpではリーチできないハッカーをアトラクトするため

1.については特に説明は不要だと思いますが、2.は既に別プラットフォームを使用している状態からの移行としては珍しい事例だと思います。現在ピクシブはBugBounty.jpとHackerOneの2つを並行して利用していますが、後の項目にてその理由と、得られた効果について解説します。

報奨金について

2016年4月の開始当時、報奨金の上限は5万円でした。これは当初様子見で小さめに設定された金額で、同年内には10万円に引き上げられました。その後、2018年の前半頃になってから報告数を増やす目的と、業界標準の報奨金に近づけるために20万円、30万円と増額しました。これでもまだ業界標準とピクシブのサービス規模に照らし合わせるとやや低めだと思いますが、一度に大きく増額するのではなく少しずつ増額していくという進め方をしています。

ピクシブではこれまで300万円弱程度の報奨金を支払っていますが、そのおよそ半分に当たる140万円ほどが増額およびHackerOne利用開始後の2018年7月以降に報告された脆弱性に対して支払われています。 これは脆弱性報奨金制度の維持にかかる費用が上がったということではなく、これまでのインセンティブや注目度では十分に調査してもらえていなかった範囲についての調査が進み、より難易度の高い脆弱性が発見されるようになった結果と考えています。実際に、増額後にこれまで潜んでいた深刻な脆弱性が報告された例も数件ありました。

f:id:pxv:20210414103556p:plain

報告数

報奨金と同様、報告数も増額 & HackerOne登録後に増加しました。

BugBounty.jp vs. HackerOne

2018年7月より、ピクシブはプライベートプログラムとしてHackerOneの使用を開始しました。これにはいくつかの狙いがあります。

1. 世界最大のバグバウンティプラットフォームであり、最大のハッカーユーザーベースにリーチできる

HackerOneの利用を開始した最も主な理由はユーザーベースの大きさでした。 ピクシブのバグバウンティプログラムが開始してから2年経過した2018年4月頃、脆弱性報告件数が伸び悩んでいた理由として (1) 報奨金が不十分 (2) 認知度が低い という2つが考えられました。報奨金については先述したように増額を実施し、認知度については将来的に完全に移行することも視野に入れてHackerOneに登録することにしました。HackerOneはバグバウンティプラットフォームの中で最も多くの企業やOSSが報奨金制度を実施している分プラットフォームへの注目度が高く、ハッカーにとっても報告実績や評価が集約・可視化される場所にもなっており、マイナーなプラットフォームや独自プラットフォームで実施する場合よりも積極的なハッカーが多い印象です。

また、多くのプログラムは最初プライベートプログラムとして開始し、十分な数のハッカーをinviteしても安定して運用できるようになってからパブリックになりますが、プライベートプログラムでは直接指名したり、一定以上の評価を得ているハッカーに絞って自動的にinvitationを送ることができるのもアトラクトに役立つ機能です。

2. 「ノイズ」となる無効な報告を削減するためのReputationなどの仕組みの存在

脆弱性スキャナの結果をそのまま貼っただけの無効な報告や、そもそも正しくない報告は多くのバグバウンティプログラムに共通の「ノイズ」です。脆弱性報告はするだけならタダでも、運が良ければそれでお金がもらえることもあるので雑な報告や虚偽の報告をする人もいます。HackerOneは、そのハッカーが正当な報告をした割合を表すReputationという評価システムを通じてこれを減らしています。これは実際のところ機能しているようで、BugBounty.jpと比べて無効な報告は少なくなっています。また、ハッカーとのコミュニケーションの中で、一部のハッカーはこのReputationを気にすることがあり、彼らがHackerOneにおける実績を自らのポートフォリオの一部のようなものと考えていることが窺えます。

報奨金の対象にならなかった報告の割合がBugBounty.jpでは51.5% (重複 + 対象外 + 無効)、HackerOneでは40.9% (Informative + not-applicable + duplicate) と、BugBounty.jpの方が10ポイントほど報奨金対象外の報告が多かったです。

f:id:pxv:20210414103644p:plain f:id:pxv:20210414103654p:plain

3. (比較的)優れたUIやSlack、GitHub連携などといったワークフロー効率化のための機能の存在

BugBounty.jpは脆弱性報告のやり取りと報奨金手配のためのベーシックな機能のみ提供している一方で、HackerOneはSlackやGitHubなどといったサービスとの連携機能など、業務効率向上のための機能を提供しています。

BugBounty.jpのメリット

ここまでHackerOneのメリットを挙げてきましたが、BugBounty.jpにも以下のようなメリットがあります。概観としては日本のサービスが小さく始めるのに敷居が低く、始めやすいと言えます。

  1. コストが安い

    • HackerOneのコストはプログラム開始時に払うプラットフォーム利用費 + 支払った報奨金に応じてかかる手数料ですが、このプラットフォーム利用費だけでいい値段します。一方BugBounty.jpではこれがかからず、基本的には支払った報奨金に応じてかかる手数料のみがコストなので、安価にプログラムを開始することができます。
  2. 日本語サポートが充実、日本人ハッカーが多い

    • BugBounty.jpの運営元が日本の企業であるため、プログラム開始までのコミュニケーションはもちろん、開始後のやり取りや「フルトリアージサービス」も日本語で行うことができます。HackerOneではこういったコミュニケーションは全て英語で行う必要があります。
    • 日本人ハッカーが多いため、多くの報告が日本語で行われます。HackerOneでは非日本語話者のハッカーが多いため、英語によるコミュニケーションが必然多くなります。また、海外向けにグローバライズしていないサービスでは日本人以外にはそもそもサービスの主旨や機能の使い方がわからず、脆弱性のテストを実施してもらえないでしょう。

現在ピクシブでは、報告数、質の両面でHackerOneの使用を開始した恩恵を受けることができていると考えています。これに伴ってBugBounty.jp側のプログラムをクローズすることも考えましたが、特に問題ない限りは両方残しておくことにしました。現在のところまだHackerOne側ではプライベート運用を続けていますが、パブリックな脆弱性報告用の窓口も常に用意しておくことは重要だと考えています。

報告例

実際に報告された脆弱性の中から面白かったものをいくつか紹介します! ここで紹介する脆弱性は既に調査、修正済みです。

OAuth Connection CSRF Leading to Account Takeover

これは、脆弱性自体は通常大きな問題ではないものの、我々特有の事情で危険な脆弱性になっていたパターンです。

「OAuth連携開始エンドポイント」にCSRF脆弱性があった場合、どういった被害が想定されるでしょうか。ここで言う「OAuth連携開始エンドポイント」とは、他SNS (例: Twitter)と自サービス (pixiv) のアカウントを紐づけ、他SNS側の認証で自サービスにログイン可能にする処理を開始するエンドポイントです。通常、こういったエンドポイントは他SNSのOAuth認可用のエンドポイントへのリダイレクトを行います。

通常、ユーザーは自分自身のアカウントでのみTwitterなどのSNSにログインしているため、OAuth連携のエンドポイントにCSRFがあってもその自分自身のアカウントに対して連携が行われるだけで嫌がらせ以上の被害はないと考えられます。

しかし、pixivがOAuth連携を許可しているサービスの一つ、Weiboは例外でした。Weiboは、驚くべきことに https://login.sina.com.cn/sso/login.php?username=<ユーザー名>&password=<パスワード> のようにGETのクエリパラメータにIDとパスワードを渡してログインする機能を提供しています。これを利用すると、攻撃者のID/パスワードを含んだログインURLを踏ませることで攻撃者の用意したWeiboアカウントにログインさせるCSRFが可能になります。

この仕様(?)と先述したOAuth連携のCSRFを組み合わせると、以下のような攻撃が成立します

  1. 被害者に https://login.sina.com.cn/sso/login.php?username=*attackers_email*&password=*attackers_password* を踏ませる (被害者が攻撃者のWeiboアカウントにログインさせられる)
  2. 被害者にpixivのOAuth連携開始エンドポイント https://www.pixiv.net/g_auth.php?mode=connect&provider=sina を踏ませる (被害者のpixivアカウントが攻撃者のWeiboアカウントに連携される)
  3. 被害者のpixivアカウントが攻撃者のWeiboアカウントに連携され、攻撃者はWeibo連携で被害者のpixivアカウントにログイン可能になる。

この脆弱性は、pixivのOAuth連携開始エンドポイントに通常のformと同様のCSRF対策を施すことで修正しました。

OAuth連携する先のサイトでログインしているアカウントはそのユーザー本人のものである、というのは妥当な前提に思えますが、一部のサービスではそれが必ずしも正しくないという事例でした。GoogleやTwitter、FacebookなどのOAuth認可サーバーを提供している主要なサービスでは恐らくこの問題はありませんが、その前提のもとにCSRF対策をしないのは安全とは言えない、というのがこの報告から得られた教訓でした。

Stored DOM-Based XSS via Vue.js Template

典型的なVue.jsのDOM-Based XSSですが、これも個人的に「バグバウンティって面白い!」と思う面白い事案だったので紹介します。

Vue.jsを使用しているページにて、VueのMustacheテンプレート記法を用いたDOM-Based XSSが存在しました。

XSSのペイロードは以下です。

{{alert(/XSS/)}}

Vue.jsのMustache記法を注入するだけのシンプルなXSSです。XSS対策のセオリー通り <>”’& をエスケープしていても防げません。

これの修正方法は色々ありますが、我々はそもそもMustache記法を使用する必要が無かったので、この機能自体を無効にすることにしました。以下はCoffeeScriptのコードです。

window.nullDelimitersMixin = {
  delimiters: [{ replace: -> undefined }, { replace: -> undefined }]
}

new Vue
    el: '#js-shop'
    mixins: [nullDelimitersMixin]
...略...

この変更によりdelimiters (Mustache記法の開始 {{ および終端 }} ) に undefined が設定され、XSSが修正されたように思えます。実際に、元のペイロードである {{alert(/XSS/)}} ではXSSが発火しないようになっていました。

しかし、実際にはここで渡した undefined はVue.js内の正規表現を組み立てる箇所で型変換が起きます。

https://github.com/vuejs/vue/blob/8f04135dbaa5f5f0500d42c0968beba8043f5363/src/compiler/parser/text-parser.js#L10-L12

const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g

const buildRegex = cached(delimiters => {
  const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})

おわかりいただけただろうか? delimitersにnullやundefinedのような値をセットすればMustacheを無効化できると考えての修正でしたが、実際には先程のundefinedはStringとの +'undefined' というStringに変換され、最終的に生成される正規表現は /undefined((?:.|\n)+?)undefined/g になります。つまり、 {{alert(/XSS/)}} ではalertが発火しなくても、 undefinedalert(/XSS/)undefined でalertが発火するのです!

この問題は、一度リリースされた後に社内のコードレビューで気づき、結果的には undefined の代わりに何にもマッチしない正規表現を指定することで修正されました。

https://github.com/vuejs/vue/issues/4223#issuecomment-294864675

修正のバグ自体も面白かったですが、最も面白かったのはその後この脆弱性の報告者と技術イベントでお話した時でした。 この脆弱性の話をすると「undefined の問題については気付いており、『修正した』と言われた後にもう一度報告するつもりだった」とのことでした。自分達はソースコードが見える状態でレビューをして気づくことができましたが、JSがminifyされて届くproduction環境でこれに気付けるとは…… 畏敬の念を抱くばかりでした。

おわりに

ピクシブのサービスに脆弱性を発見された場合は以下からご報告ください! https://bugbounty.jp/program/0602f8c6f136dbbd92fbb909

参考

20191219012710
kobo
2018年4月入社。セキュリティエンジニアとしてサービスやコーポレートのセキュリティに取り組む。