more posts

Discord 1-click XSS to RCE

Electron Isolation Overview

Discord's desktop app is made with Electron. The (modern) Electron security model focuses on having the browser sandbox, the renderer process, and the main process. each are isolated from each other as separate processes so require some interface to communicate, like so:

    main world (scripts inside the site)
      ↑                            |
      |       contextBridge        |
      |                            ↓
 isolated world (limited node as preload)
      ↑ ipcMain                    |
      |                            |
      |                ipcRenderer ↓
           main process (node)

ipcMain and ipcRenderer are event-based ways to communicate between the renderer and main process. The renderer doesn't have access to all capabilities and it's generally recommended to use it largely for such communication for security.

contextBridge exposes functions in the preload (isolated world) to the main world in the window object.

Discord Native Modules

Discord have their own native modules system with their updater, allowing separate modules for their purposes. The main modules they have are:

These are exposed to the main world (the Discord webapp loaded) via their own exposed context bridge, DiscordNative. Specifically, DiscordNative.nativeModules.requireModule(name):

function requireModule(name) {
  if (!/^discord_[a-z0-9_-]+$/.test(name) && name !== 'erlpack') {
    throw new Error('"' + String(name) + '" is not a whitelisted native module');
  }

  return require(name);
}

They use Node's require, which looks scary but the input is explicitly checked to begin with discord_ or being erlpack. The paths (module.paths) which can be required are carefully controlled earlier on. However, what if we could add our own file in one of the paths which we could then require?

Discord Native File Saving

Discord have an API in their context bridge for saving files with a UI prompt for where with DiscordNative.fileManager.saveWithDialog, this is why we need 1-click, as the user must click save themselves. We can supply our own contents, filename, and default directory. The default directory is appended to the Downloads folder for the running user. However, it isn't sanitized or checked for path traversal, so we can give it ../../../../../ (etc) to escape to the root dir.

Another context bridge API, DiscordNative.fileManager.getModulePath(), gives us the path straight to an allowed path by the native module requirer. So we can combine them for a full path into the directory we want.

Bringing it all together

Combining these separate functions and knowledge, we can make our exploit:

Reporting