RubyやRubyのOSSの脆弱性を見つけた話
この記事はRuby Advent Calendar 2018 - Qiitaの23日目です。
今年はRubyやOSSの脆弱性をいろいろ探していたので、その感想を。
まずはRubyから
Ruby (Cruby)
昨年公開された↑の@mametterさんの記事を見て、ちょっと調べてみようと思い今年のはじめから調査を開始。
はじめに見つけたものがWEBrickのhttp header injectionで、これは影響度低いのでは…?と思いながら報告してみたのですが結果は重複(CVE-2017-17742: WEBrick における HTTP レスポンス偽装の脆弱性について)。逆にこれぐらいでも脆弱性として扱われるかと思い、いろいろ調べてみることにしました。手法としては過去にRubyに報告された脆弱性を確認し、泥臭くドキュメントにあるメソッドをひとつずつ動作を試し続けました。
Tmpでのディレクトリトラバーサル
CVE-2018-6914: Tempfile および Tmpdir でのディレクトリトラバーサルを伴う意図しないファイルまたはディレクトリ作成の脆弱性について
hackerone.com
これって脆弱性なのと時々突っ込まれるやつです。ですよね、と自分でも思います。
これは同じ問題であったDir.mktmpdir
のほうがわかりやすくて
[vagrant@localhost ~]$ ls . [vagrant@localhost ~]$ irb irb(main):001:0> require 'tmpdir' => true irb(main):002:0> Dir.mktmpdir(['../../home/vagrant/', 'gold']) => "/tmp/../../home/vagrant/20180104-6544-1atequhgold" irb(main):003:0> Dir.mktmpdir('../../home/vagrant/silver') => "/tmp/../../home/vagrant/silver20180104-6544-1822fif" irb(main):004:0> `ls` => "20180104-6544-1atequhgold\nsilver20180104-6544-1822fif\n"
とするとシステムが提供するtmp以外の場所に作られたうえに、Rubyも自動で削除しないのでテンポラリじゃないという問題がありました。PHPのサーバなんかが一緒に動いてるとまずそうですね。
あと、ドキュメント上ではパスではなくprefixとされているのでユーザが安全と思ってしまいそうということで 、Pythonでも似たような話がされていますね。 https://bugs.python.org/issue35278
この辺の話をどこかに書こうと思っていたんでしたが忘れてました。
DirでのNUL文字の扱いの問題
CVE-2018-8780: Dir において NUL 文字挿入により意図しないディレクトリにアクセスされうる脆弱性について hackerone.com 以前FileでNUL文字の問題があったので、一応調べてみたらDirのメソッドの一部に残っていたもの。
TmpfileとDirの問題は書いてしまうことがありそうな気がするのですが、いまだ影響しているところを見たことがないですね…。
UNIX ドメインソケットでのNUL文字
CVE-2018-8779: UNIX ドメインソケットにおいて NUL 文字挿入により意図しないソケットにアクセスされうる脆弱性について hackerone.com NUL文字二つ目。UNIX ドメインソケットは普段使わないので、これが脆弱性として扱われるかよくわからなかったのですがとりあえず報告してみたら通りました。ここに任意の文字列が入る時点ですでに不味そうなので実際の影響は殆どなさそうです。
Ruby 2.6での挙動変更
Dir[]
でのNUL文字について
知らない人も結構多そうなんですがDir[]
とDir.glob()
ではNUL文字が区切り文字になります。
$ irb irb(main):001:0> Dir["/tmp\0/home"] => ["/tmp", "/home"] irb(main):002:0> Dir["/tmp"] => ["/tmp"] irb(main):003:0> Dir["/tmp/home"] => []
このため固定した文字列 + ユーザ入力の文字列
の文字列を渡すと全く別の場所が探索される可能性があります。
これはドキュメントに書かれている挙動であり仕様です。*1
singleton method Dir.[] (Ruby 2.6.0)
[PARAM] pattern: パターンを文字列で指定します。 パターンを "\0" で区切って 1 度に複数のパターンを指定することもで きます。 パターンの区切りには "\0" のみ指定できます。 配列を指定することで複数のパターンを指定できます。
しかし、対策しているコードは今のところ見たことがありません。
これはちょっと使う側として辛いので、twitterでusaさんにRuby3で廃止できないか提案してみたところ、チケットを作ってくださって採用されました。Feature #14643: Remove problematic separator '\0' of Dir.glob and Dir.[] - Ruby trunk - Ruby Issue Tracking System
今月リリースのRuby 2.6からNUL文字が渡されると警告が出るようになります。(ruby/NEWS at v2_6_0_rc2 · ruby/ruby · GitHub)
Rubygems
rubygems
2月のリリースのものと似たものを報告していて重複に。
3/28に公開されたRubyの脆弱性情報についてのポエム的解説 - pixiv insideと見解が違うのが、サーバ側だけでなく普通の利用者も危険なはずという点です。gemのinstallとかunpackで問題がでるので。
65534倍効率的なブルートフォース
rubygems.orgでAPIキーの確認時に渡されるパラメータの型をチェックしておらず、配列を投げてやるとそれがそのままモデルのfindに渡されるため、どれか一つでも正しいものがあれば認証が通ってしまっていました。rubygems.orgではrack-attackが入ってたんですが、これを使うと1つのリクエストで効率的にブルートフォースができるのですり抜けます。型のチェックがゆるい言語で、認証系を独自に実装しているようなところではたまにありそうな問題です。
どれぐらい渡せるか限界を探ると、65534倍まで効率的なブルートフォースが可能でそれ以上の値はエラーとなりました。この場合はAPIキーがSecureRandom.hex(32)
で生成されていたので、65534倍効率的な程度ではほぼ影響はなかったと思われます。SecureRandom.hex(8)
ぐらいではまずかったかもしれません。
Rails 5.2.2からは渡されるパラメータが多すぎるときにbindしなくなったらしいので、より大きな配列を投げられるかもしれませんがまだ確認していません。
minitarでのKernel.open*2
input,outputにKernel.open
が使われていたのでissueで書いてみたところ意図的なものとのこと。ただ、gemを使う側が気にしてにないと危ないのは確かなのでreamdeに追記されました。
geminabox
Avoid XSS vulnerability on /reindex · geminabox/geminabox@50da77f · GitHub エラーからのXSS。異常系からのXSSはたまに見かけますね。
Discourse
hackerone.com いくつか報告を出して通ったのですが、公開扱いにはなっていないので詳細については省略。 報告に書いた説明がいくつか変だったり調査不足だったところがあったので、もっとちゃんと書けばもっと報奨金もらえたのではというのが反省点。
Heroku
bugcrowd.com 会社で公開しているOSSにも報奨金を設定している企業はいくつかありまして、Herokuもそのうちの一つです。 RubyのOSSのものにいくつか出しました。これも公開されていないので詳細は省略。
Rails
pgでのNUL文字
これはCVEが出てないので知らない人も多そうです。 内容としてはこれもまたNUL文字で、文字列中にNUL文字があるとそこで切られたクエリが送信されていました。
> Article.where("title = :title", {title: "test title\0suffix"}) Article Load (0.4ms) SELECT "articles".* FROM "articles" WHERE (title = 'test title') LIMIT $1 [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Article id: 1, title: "test title", text: "dummy", created_at: "2018-08-13 13:31:37", updated_at: "2018-08-13 13:31:37">]>
RailsであればNUL文字は保存ができないので影響があるのは検索するときだけなのですが、文字列をブラックリストやsuffixでバリデーションしてるとすり抜けます。これは書いてる場所がたまにあると思います。*3
Rails 4.xではpg1.xに対応してないようなので、まだRails5に上げていない場合は頑張って諸々のバージョンを上げるかパッチでなんとかするかしか……
ちゃんと調べてなかったのでRailsのプログラムに報告してしまったのですが、正確にはpg gemの問題でした。ですが、pgのチームに連絡してもらった上で報奨金ももらえました。
Active StorageでのXSS
Rails5.2で追加されたActive Storageではファイルアップロードを実装するときにおこる問題をいろいろ踏んでいました。結構頑張って調べたんですがこれも重複。
報奨金
RubyのOSSでの報奨金は合計で$7,584となりました。
感想
Rubyに詳しくなった
Rubyに限らない話なんですが脆弱性調査をしていると、マニアックな挙動を調べるモチベーションが高くなります。ドキュメントのメソッドの挙動をひたすら確認していったのですが、途中これは一体何の修行なんだろうなどと思っていました。どんな物があるのか去年よりだいぶ詳しくなったと思います。 と言いつつ未だにmapの引数の順番で迷いますが。*4
脆弱性としての判定
普段はweb周りのXSSなどを調べていてそれ以外の用途には疎いため、脆弱性に当たるかどうかの判定に悩む場面が何度かありました。仕様なのかバグなのか微妙だけども影響あるならば報告出しておいたほうがいいか、という思考で迷う場合にはとりあえず報告していました。そのほうがシステムの安全性としては高くなるのでしょうがないかなとも思います。報告を受ける側の負荷は高そうですが……
自分が使うものを安全にする
クリティカルな脆弱性を見つけられないのはバグハンターとしては辛いものですが、利用者としては幸いです。 YAML.safe_yamlをなんとかバイバスできないかひたすら探していたり、rubygems.orgでRCEできないかなど結構調べていたのですが見つけることができず、悔しい一方利用者としては安心感が増します。
CakePHPの脆弱性を見つけた話(あるいはCakePHP Advent Calendar 2017 n日目)
年末にCakePHP 2.x系のSQL injectionを見つけたので報告してました。
詳細
内容としてはこちらの記事で書かれているようなCakePHPでよくあるSQL injectionです。
postCondition
を使うとリクエストのbodyの形からCakePHPのfindでconditionsに使う形変換されるのですが、その際にキーの値がエスケープされるかというとそうでもない状態であったということです。
これだけでしたらCakePHPの仕様であって使い方の問題ともとれるのですが、ドキュメントを見ると脆弱性が発生する形で記載されておりそれは不味いだろうということで報告しました。
public function index() { $conditions = $this->postConditions($this->request->data); $orders = $this->Order->find('all', compact('conditions')); $this->set('orders', $orders);
CakePHPの脆弱性のハンドリングは GitHub - cakephp/cakephp: CakePHP: The Rapid Development Framework for PHP - Official Repositoryで示されていたので、それに沿ってメーリングリストにメールを送って修正の方針などでやり取りしました。12/9に最初のメールを送って、修正がリリースされたのは18日です。*1
Securityコンポーネントが設定されている場合の挙動を確認していなかったのでここ数日確認しているのですが、設定している限りは多分大丈夫そうです。*2 とはいえ修正自体はシンプルなので基本的にはバージョンを上げたほうが早いでしょう。
CakePHP Advent Calendar 2017
この脆弱性は[CakePHP3] データ漏洩していませんか?RequestHandlerの危険性 - Qiitaの記事の脆弱性を確認のためCakePHPのコードを確認していたときに見つけものでした。なぜかこのアドベントカレンダーでは脆弱性の話が多かったような気がします。
- 7日目 [CakePHP3] データ漏洩していませんか?RequestHandlerの危険性 - Qiita
- 10日目 CakePHPのセキュリティー診断への対応まとめ - Qiita
- 12日目 CakePHPではDBカラムのSQLインジェクションに注意! - Code Day's Night
*1:その間にid:ichikaway さんのピンポイントな記事が公開されたのでドキッとしました
*2:少し怪しいところは見つけたのですが、存在しないテーブルがwhere文に入ってエラーになるのを回避する方法が思い浮かばない……
XSSがある場合のCSRF対策としてクリックジャッキングを使う(没)案
最近話題になっているクロスサイトリクエストフォージェリの話を。
CSRF対策はXSSがある場合には効果的ではないと言われます。javascriptが実行できればhtml中のtokenもとれますし、リクエストも自由に送信できるので対策を回避できるためですね。
XSSがあっても万が一防げるようにするため、クリティカルな箇所ではパスワードなどユーザしか知らない情報を使って確認しているサイトが多いでしょうか。しかしユーザに何度もパスワードを入力を促すと、XSSによって入力フォームが作られたときに疑われずに入力されてしまうのではないかという懸念もあります。*1
javascriptから直接触ることができない領域を使えばXSSがあったとしてもなんとかなるはずです。思いつくのはhttponlyがついたクッキーの値や別オリジンでしょうか。httponlyがついたクッキーは基本的にリクエスト時に送信されているのでCSRF対策にはなりません。他にもFlashとかでなにかあるかもしれませんが、まあ置いておきます。
ここでは別オリジンを使った方法を考えます。最も単純なのはユーザがボタンを押したときに同時に別オリジンのボタンを押させることです。そうクリックジャッキングです。
クリックジャッキングについて
詳細はこのあたりで。
大体の場合はレスポンスヘッダにX-Frame-Options
を使ってiframeに読み込まれるのを防ぎます。
CSRF対策案
CSRFを防ぎたいhttps://example.com
と、別オリジンとなるhttps://prevent-csrf.example.com
を用意します。htmlの雰囲気としてはこんな形で。
// https://example.com の画面 ... <form> <input type="token" value="何かしらのcsrf_token" /> <button type="submit">XSSがあってもCSRFを防ぎたい処理のボタン</button> </form> <iframe src="https://prevent-csrf.example.com/prevent" style="...."> <form action ="/prevent"> <input type="referrer" value="読み込みサイトのURL" /> <input type="token" value="何かしらのcsrf_token_2" /> <button type="submit">元のドメインのボタンと同じ位置に表示されるボタン</button> </form> </iframe>
ボタンが押されたときにそれぞれのドメインに向けてほぼ同じタイミングでリクエストが飛ぶようにし、それをサーバ側で検証することでユーザ操作によるものか、XSSか何かを使って操作されたものかどうかを判断します。
ボタンが押されたときに同時にリクエストされることを想定していますが、ちゃんと動くかどうかは微妙なのでhttps://example.com
側はjavascriptで制御してタイミングを遅らせるかSingle Page Applicationのような画面遷移にしたほうが良いでしょうか。
問題点
アクセスビリティ
マウスイベントはクリックジャッキングによってiframeの埋め込み先に貫通しますが、tabキーでのフォーカスは一緒に遷移はしませんし、エンターキーでのフォームの送信もされません。スクリーンリーダーとの組み合わせも悪いでしょう。ユーザに対して、"マウスでクリックして"と説明をつけることである程度回避できるかもしれませんが、使いづらそうです。
CSRF対策
例のhttps://prevent-csrf.example.com
側にもCSRFとXSS対策も必要です。CSRFが行われると結局元のドメインにもCSRFができてしまうためです。この用途にしか使わないドメインとしておいて、さらにCSPなど入れてガチガチにするとこんな感じでしょうか。
- 用途を限定して他の目的に使わない
- httpsのみにし、さらにHSTSをかける
- CSPでXSSをブロックする
- X-Frame-Optionsで
ALLOW-FROM
を使ってiframeとして読み込み元を限定する*2
XSS対策の一部として全くjavascriptを使わない実装にするか、javascirptを使ってカスタムヘッダなどをつけて送信することでCSRF対策にするかは迷うところですね。
https://prevent-csrf.example.com
を攻撃者が自分で開く(or サーバから自動で叩く)こともありえるので、*.example.com
に対してhttponlyとsecureフラグをつけたトークンを更に別に発行して、サーバ側で同じユーザかどうかを判断する必要もあります。サブドメインであればこのあたりはやりやすいですが、そうでない場合はOAuthとかリダイレクトでトークンを渡してやる必要があって、更にその仕組の安全性が…となっていくのでひたすら面倒ですね。
CSRF対策としての機能を目指しているのに、更にCSRF対策をしているのは何と言うか、何でしょうね。
クリックジャッキング対策(追記22:12)
元のオリジンにクリックジャッキング対策が必要なのはもちろんですが、XSSによってjavascriptが実行された時ユーザのマウスに追随する形で、https://prevent-csrf.example.com
側のiframeを移動させることも可能です。そのため多少面倒ですが、XSSとクリックジャッキングの組み合わせによってCSRFができてしまう可能性があります。window.screenX
を使って、iframeが読み込まれている位置を特定できるかもしれませんが確認できていません。
まとめ
*1:この辺どっちがいいか資料などがあれば誰か教えてください。
*2:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Browser_compatibility によるとChromeではALLOW-FROMが使えないようです
ChatWorkでのStored XSS
はじめに
この脆弱性は BugBounty.jp を通して報告しました。利用規約(第10条の5)によれば法人会員の許可があれば公開可能のようだったので、脆弱性を報告した際に公開可能かも問い合わせ、承諾を得ています。現在はこの脆弱性は修正されています。
詳細
チャットワークではURLらしきものが入力されると、自動的にリンクになる機能があります。URLのドメインなどによって挙動がさらに変わるのですが、外部サイトのプレビュー機能の部分でStored XSSとなる部分がありました。
修正前のソースのChromeの開発者ツールで整形したものから該当箇所を抜粋してます。
... if ("www.youtube.com" === t.hostname) { if (!(o = t.href.match(/\/watch\?v=([^\/&]+)/))) return; n.type = "youtube", n.content_id = o.pop() } ... return n.type ? ' <a\n class="_previewLink timelineLinkAppend"\n data-url="' + c.urlencode(e) + '"\n data-type="' + n.type + '"\n data-user-id="' + n.user_id + '"\n data-content-id="' + n.content_id + '">' + p.Language.getLang("%%%preview%%%") + "</a>" : "" }
YoutubeのURLであれば、URL中のv=
より後ろの文字列がcontent_id
に入ります。*1
content_id
を使っている部分ではそのままHTMLの組み立てに使われています。
data-content-id="' + n.content_id + '">'
URLとして判別される文字種は限られているのですが、"
は入力可能です。そのため
https://www.youtube.com/watch?v=#"onmouseover=alert(location.host)*"
と言った文字列が入力されるとXSSとなります。
発火しないパターン
ChatWork中では空白文字はURLとしては認識されないため、たとえば以下の方法では imgの後ろに空白が入るのでそこで途切れます。
https://www.youtube.com/watch?v=#"><img onerror=alert(locaton.host)>
owaspの資料によると空白の代わりに/
も使えるようですが、それもURLからパラメータを取得する正規表現の段階で弾かれます。
ですので、
"><script>...</script>
も記述できません。、/
が書けないため</script>
を書くことができず、別の所にある</script>
までをscriptタグの中身として認識されるのでシンタックスエラーとなります。"
や'
、`で文字列にしたりしてしましたが文字列が明示的閉じられないとやはりシンタックスエラーとなりました。*2
タグを追加することができますが閉じれず、追加するタグについても属性はかけません。しかし、a
タグの属性に追加することは可能です。
イベントハンドラを使った攻撃
さて仕方がないのでイベントハンドラでなんとかします。a
タグであるため使えるイベントハンドラは限られていてonload
などは動きません。おそらくもっと使いやすいものはonmouseover
です。
そのまま使うとユーザがマウスオーバするのを誘導しないといけないので攻撃としてはちょっと難しいですね。a
タグの属性は自由に追加できたのでスタイル属性を追加してcssで装飾してやります。
本当はモーダルのように画面いっぱいにして部屋を開いた段階で回避不可能なようにしたかったのですが、CSSにあまり詳しくないため画面半分ぐらいを埋めるのが限界でした。
自由に攻撃する
イベントハンドラで攻撃が可能になりましたが、使用できる文字種に制限があるとそのまま実行できるjavascriptにも制限がかかってしまいます。 今回は()
が使えるのでevalとbase64を使って回避できました。
// btoa('alert("xss")') // > "YWxlcnQoInhzcyIp" https://www.youtube.com/watch?v=#"onmouseover=eval(atob(`YWxlcnQoInhzcyIp`))*" // -> alert("xss") が実行される。
対策
修正後に確認したところ、encodeURI
でエスケープがされていました。
こういった正規表現の考慮漏れなどに対して抜本的な対策としては、生のHTMLを組み立てるのではなく、仮想DOM系のライブラリを使うのが良いです。ですが、すでに動いているシステムを置き換えていくのはまあ難しいのはあります。(ソースを見るとReactは入っているようですが)CSPを入れるという手もありますが、事前に確認する点が多そうです。
影響
Stored XSSであり、複数ユーザが閲覧できるそれぞれの部屋に書き込みもできるため他のユーザにも影響が及びます。また、チャットワークは複数組織にまたがったチャット部屋が存在する(確かAPI関連の部屋があったはず)ため、ワームのように影響が広がる可能性がありました。*3 数年前に起きたtwitterでのワームと類似しています。
また登録しているメールアドレスを変更する際にパスワードの入力がないため、パスワードリセットの機能との組み合わせでおそらくアカウントを乗っ取れます。*4
またチャットワークは過去の自分の発言も編集できるので、過去に遡って発言を改ざんすることが可能です。
デスクトップ版のアプリではプレビュー機能がないため影響を受けないようでした。おそらくElectron製なので外部リソースを読み込まないようにしているのではないでしょうか。同様にAndroid版もプレビュー機能がないので影響を受けていません(iPhoneは持ってないのでわかりません)。
影響度が高い攻撃方法として考えられるのは以下の組み合わせです。
- ワームとして広がるようにつくる
- 攻撃によって実行している最中にパスワードの確認入力を求めるダイアログを出す
- auto completeで自動入力された場合は送信する
- ユーザが入力した場合はそれも送信する
- 裏で行う処理として
- メールアドレスを変更し、パスワードリセット機能からパスワードを変更して乗っ取る
- チャットの内容を変更
- チャットの内容を流出
- 部屋のメンバーを変更する
- パスワードを取得できた場合
- 外のサービスでの使いまわしされてないかを探る
service workerが使えるともっと幅が広がるのですが、ファイルアップロード先が別ドメインのため難しいでしょう。
ランサムウェアのような振る舞いの可能性 (追記23:41)
チャットの内容を変更したり、アカウントの乗っ取りが出来るということはランサムウェアのようにチャットの内容を対象に身代金を請求される可能性が出てきます。変更前のメールアドレスに対して、ビットコインの振込先を指定するような形になるでしょうか。
webサービスに対してのランサムウェア的行為はDDoS以外では聞いたことはないですが、チャットワークが仕事で使うツールであり、リアルタイムなやり取りを目的としたサービスであること、仮想通貨の流通量が増えてることなどを考えると割と現実味がありそうです。チャットワーク側が対策を打つか復旧させるまでの時間との勝負になるので、早期割引等があると効果的かと。特にファイルは削除されると(UI上では)元に戻せないので、その場合どうなるのかは気になるところです。
ワームが広がったときの対策
twitterとは違い過去の内容が変更できるため、チャットの内容を変更されるのが地味に一番イヤな気はします。ユーザによるものかどうかが判別が難しいので、全部の変更のログを確認して攻撃によるものかどうかを判別するか、発覚時点までのDBに戻すか、諦めるかになるでしょうか。
余談
数カ月ぶりにbugbonty.jpで報告したのですが、送信時にCVSS V3での評価が必要になっていました*5。評価の経験があまりないのでXSSの標準(5.4)をそのままに使いましたが、今回の脆弱性ですと影響範囲が大きいのでこれでいいんだろうかという気持ちにはなりました。