Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the fuzzy-use plugin #19471

Merged
merged 11 commits into from
Oct 1, 2024
7 changes: 4 additions & 3 deletions docs/metasploit-framework.wiki/How-To-Use-Plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ plugin_name_command --option

The current available plugins for Metasploit can be found by running the `load -l` command, or viewing Metasploit's [plugins](https://github.com/rapid7/metasploit-framework/tree/master/plugins) directory:

| name | Description |
| Name | Description |
|------------------|-----------------------------------------------------------------------------------------------------|
| aggregator | Interacts with the external Session Aggregator |
| alias | Adds the ability to alias console commands |
Expand All @@ -30,6 +30,7 @@ The current available plugins for Metasploit can be found by running the `load -
| db_tracker | Monitors socket calls and updates the database backend |
| event_tester | Internal test tool used to verify the internal framework event subscriber logic works |
| ffautoregen | This plugin reloads and re-executes a file-format exploit module once it has changed |
| fzuse | A plugin offering a fuzzy use command |
| ips_filter | Scans all outgoing data to see if it matches a known IPS signature |
| lab | Adds the ability to manage VMs |
| libnotify | Send desktop notification with libnotify on sessions and db events |
Expand All @@ -42,12 +43,12 @@ The current available plugins for Metasploit can be found by running the `load -
| request | Make requests from within Metasploit using various protocols. |
| rssfeed | Create an RSS feed of events |
| sample | Demonstrates using framework plugins |
| session_notifier | This plugin notifies you of a new session via SMS |
| session_notifier | This plugin notifies you of a new session via SMS |
| session_tagger | Automatically interacts with new sessions to create a new remote TaggedByUser file |
| socket_logger | Log socket operations to a directory as individual files |
| sounds | Automatically plays a sound when various framework events occur |
| sqlmap | sqlmap plugin for Metasploit |
| thread | Internal test tool for testing thread usage in Metasploit |
| thread | Internal test tool for testing thread usage in Metasploit |
| token_adduser | Attempt to add an account using all connected Meterpreter session tokens |
| token_hunter | Search all active Meterpreter sessions for specific tokens |
| wiki | Outputs stored database values from the current workspace into DokuWiki or MediaWiki format |
Expand Down
13 changes: 7 additions & 6 deletions lib/msf/core/modules/metadata/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def init_store
load_metadata
end

def get_user_store
zeroSteiner marked this conversation as resolved.
Show resolved Hide resolved
store_dir = ::File.join(Msf::Config.config_directory, "store")
FileUtils.makedirs(store_dir) if !::File.exist?(store_dir)
return ::File.join(store_dir, UserMetaDataFile)
end


#######
private
#######
Expand Down Expand Up @@ -107,12 +114,6 @@ def configure_user_store
return copied
end

def get_user_store
store_dir = ::File.join(Msf::Config.config_directory, "store")
FileUtils.makedirs(store_dir) if !::File.exist?(store_dir)
return ::File.join(store_dir, UserMetaDataFile)
end

def load_cache_from_file_store
cache_map = JSON.parse(File.read(@path_to_user_metadata))
cache_map.each {|k,v|
Expand Down
150 changes: 150 additions & 0 deletions plugins/fzuse.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
require 'socket'

# this is the main routine that's executed in the grandchild process (msfconsole -> fzf -> this)
if $PROGRAM_NAME == __FILE__
exit 64 unless ARGV.length == 2

UNIXSocket.open(ARGV[0]) do |sock|
sock.write ARGV[1] + "\n"
sock.flush

puts sock.read
end
exit 0
end

module Msf
class Plugin::FuzzyUse < Msf::Plugin
class ConsoleCommandDispatcher
include Msf::Ui::Console::CommandDispatcher

FZF_THEME = {
'fg' => '-1',
'fg+' => 'white:regular:bold',
'bg' => '-1',
'bg+' => '-1',
'hl' => '-1',
'hl+' => 'red:regular:bold',
'info' => '-1',
'marker' => '-1',
'prompt' => '-1',
'spinner' => '-1',
'pointer' => 'blue:bold',
'header' => '-1',
'border' => '-1',
'label' => '-1',
'query' => '-1'
}.freeze

def initialize(driver)
super

@module_dispatcher = Msf::Ui::Console::CommandDispatcher::Modules.new(driver)
end

def name
'FuzzyUse'
end

#
# Returns the hash of commands supported by this dispatcher.
#
def commands
{
'fzuse' => 'A fuzzy use command added by the FuzzyUse plugin'
}
end

def pipe_server(socket_path)
server = UNIXServer.new(socket_path)
File.chmod(0600, socket_path)
loop do
client = server.accept
begin
unless (input_string = client.gets&.chomp).blank?
if (mod = framework.modules.create(input_string))
client.puts(Serializer::ReadableText.dump_module(mod))
end
end
rescue StandardError
end
client.close
end
rescue EOFError
ensure
server.close if server
File.delete(socket_path) if File.exist?(socket_path)
end

#
# This method handles the fzuse command.
#
def cmd_fzuse(*args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question; Why is it called fzuse instead of fzfuse 👀

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing the rationale is the fzf is considered an implementation detail, and in the future fzuse could technically be powered by anything? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with fzuse because it's fuzzy-user. fzf is named as it is because it's fuzzy-finder.

selection = nil

Dir.mktmpdir('msf-fzuse-') do |dir|
socket_path = File.join(dir, "msf-fzuse.sock")
Copy link
Contributor

@adfoster-r7 adfoster-r7 Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on using Ruby's tmp file API and creating the file in the ~/.msf4/tmp/local directory with a slightly different naming convention:

>> Tempfile.create(['msf-fsuse-', '.sock'], Msf::Config.local_directory) { |socket_file| puts socket_file.path }
/home/vagrant/.msf4/local/msf-fsuse-20240927-2373170-2tm1kk.sock

Which I think should allow us to drop the wrapper mktmpdir call here too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't work:

[6] pry(main)> Tempfile.create(['msf-fsuse-', '.sock']) { |sf| UNIXServer.new(sf) }
Errno::EADDRINUSE: Address already in use - connect(2) for /tmp/msf-fsuse-20240927-98954-b5sojv.sock
from (pry):6:in `initialize'
[7] pry(main)> Tempfile.create(['msf-fsuse-', '.sock']) { |sf| UNIXServer.new(sf.path + '.new') }
=> #<UNIXServer:/tmp/msf-fsuse-20240927-98954-of8yja.sock.new>
[8] pry(main)> 

Copy link
Contributor

@adfoster-r7 adfoster-r7 Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense 👍

So I guess this would be enough to move the file into the ~/.msf4 folder

>> Dir.mktmpdir('msf-fzuse-', Msf::Config.local_directory) { |dir| socket_path = File.join(dir, "msf-fzuse.sock"); UNIXServer.new(socket_path) }
=> #<UNIXServer:/home/vagrant/.msf4/local/msf-fzuse-20240927-2373170-wkyt2n/msf-fzuse.sock>
>> Dir.mktmpdir('msf-fzuse-', Msf::Config.local_directory) { |dir| socket_path = File.join(dir, "msf-fzuse.sock"); UNIXServer.new(socket_path) }
=> #<UNIXServer:/home/vagrant/.msf4/local/msf-fzuse-20240927-2373170-n3ya92/msf-fzuse.sock>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to put it in the config directory? The file only lasts a couple of seconds while the user is picking their module and I wasn't aware there was a precedent of placing temp files in the config directory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've had PRs that put wordlists etc into places outside of ~/.msf4 which I think we've tried to avoid in the past, and for Metasploit Pro it's unexpected for files to appear in random places IMO instead of being self-contained. It's not a strong blocker for me 👍

server_thread = Thread.new { pipe_server(socket_path) }

query = args.empty? ? '' : args.first
ruby = RbConfig::CONFIG['bindir'] + '/' + RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']

color = "--color=#{FZF_THEME.map { |key, value| "#{key}:#{value}" }.join(',')}"
Open3.popen3('fzf', '--select-1', '--query', query, '--pointer=->', color, '--preview', "'#{ruby}' '#{__FILE__}' '#{socket_path}' '{1}'", '--preview-label', "Module Information") do |stdin, stdout, stderr, wait_thr|
framework.modules.module_types.each do |module_type|
framework.modules.module_names(module_type).each do |module_name|
stdin.puts "#{module_type}/#{module_name}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker; it'd be interesting to add actions here potentially

end
end
stdin.close
selection = stdout.read
end

server_thread.kill
server_thread.join
end

return if selection.blank?

selection.strip!
@module_dispatcher.cmd_use(selection)
end
end

def initialize(framework, opts)
super

unless defined?(UNIXSocket)
# This isn't a requirement that can be fixed by installing something
print_error("The FuzzyUse plugin has loaded but the Ruby environment does not support UNIX sockets.")
return
end

missing_requirements = []
missing_requirements << 'fzf' unless Msf::Util::Helper.which('fzf')
zeroSteiner marked this conversation as resolved.
Show resolved Hide resolved

unless missing_requirements.empty?
print_error("The FuzzyUse plugin has loaded but the following requirements are missing: #{missing_requirements.join(', ')}")
print_error("Please install the missing requirements, then reload the plugin by running: `unload fzuse` and `load fzuse`.")
return
end

add_console_dispatcher(ConsoleCommandDispatcher)

print_status('FuzzyUse plugin loaded.')
end

def cleanup
remove_console_dispatcher('FuzzyUse')
end

def name
'fuzzy_use'
end

def desc
'A plugin offering a fuzzy use command'
end

end
end
Loading