ooooooo_qの日記

脆弱性の話とか

ChatWorkでのStored XSS

f:id:ooooooo_q:20171129224921p:plain

はじめに

この脆弱性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で装飾してやります。

f:id:ooooooo_q:20171129225115p:plain

本当はモーダルのように画面いっぱいにして部屋を開いた段階で回避不可能なようにしたかったのですが、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)をそのままに使いましたが、今回の脆弱性ですと影響範囲が大きいのでこれでいいんだろうかという気持ちにはなりました。

f:id:ooooooo_q:20171129234251p:plain

*1:他にもspeakerdeck.comなどもあります

*2:何か他に方法はあるかもしれません

*3:このあたりはグループごとにドメインが存在するslackと違う点ですね。

*4:実際の挙動を確認していないので、何か不思議な力でで防がれる可能性はあります

*5:Hacker Oneにも同様の機能がありますが、必須入力ではないはず