## # # This plugin requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework # ## module Msf ### # # This plugin extends the Rex::Text::Table class and provides commands # that output database information for the current workspace in a wiki # friendly format # # @author Trenton Ivey # * *email:* ("trenton.ivey@example.com").gsub(/example/,"gmail") # * *github:* kn0 # * *twitter:* trentonivey ### class Plugin::Wiki < Msf::Plugin ### # # This class implements a command dispatcher that provides commands to # output database information in a wiki friendly format. # ### class WikiCommandDispatcher include Msf::Ui::Console::CommandDispatcher # # The dispatcher's name. # def name 'Wiki' end # # Returns the hash of commands supported by the wiki dispatcher. # def commands { 'dokuwiki' => 'Outputs data from the current workspace in dokuwiki markup.', 'mediawiki' => 'Outputs data from the current workspace in mediawiki markup.' } end # # Outputs database entries as Dokuwiki formatted text by passing the # arguments to the wiki method with a wiki_type of 'dokuwiki' # @param [Array] args the arguments passed when the command is # called # @see #wiki # def cmd_dokuwiki(*args) wiki('dokuwiki', *args) end # # Outputs database entries as Mediawiki formatted text by passing the # arguments to the wiki method with a wiki_type of 'mediawiki' # @param [Array] args the arguments passed when the command is # called # @see #wiki # def cmd_mediawiki(*args) wiki('mediawiki', *args) end # # This method parses arguments passed from the wiki output commands # and then formats and displays or saves text according to the # provided wiki type # # @param [String] wiki_type selects the wiki markup lanuguage output to # use, it can be: # * dokuwiki # * mediawiki # # @param [Array] args the arguments passed when the command is # called # def wiki(wiki_type, *args) # Create a table options hash tbl_opts = {} # Set some default options for the table hash tbl_opts[:hosts] = [] tbl_opts[:links] = false tbl_opts[:wiki_type] = wiki_type tbl_opts[:heading_size] = 5 case wiki_type when 'dokuwiki' tbl_opts[:namespace] = 'notes:targets:hosts:' else tbl_opts[:namespace] = '' end # Get the table we should be looking at command = args.shift if command.nil? || !['creds', 'hosts', 'loot', 'services', 'vulns'].include?(command.downcase) usage(wiki_type) return end # Parse the rest of the arguments while (arg = args.shift) case arg when '-o', '--output' tbl_opts[:file_name] = next_opt(args) when '-h', '--help' usage(wiki_type) return when '-l', '-L', '--link', '--links' tbl_opts[:links] = true when '-n', '-N', '--namespace' tbl_opts[:namespace] = next_opt(args) when '-p', '-P', '--port', '--ports' tbl_opts[:ports] = next_opts(args) tbl_opts[:ports].map!(&:to_i) when '-s', '-S', '--search' tbl_opts[:search] = next_opt(args) when '-i', '-I', '--heading-size' heading_size = next_opt(args) tbl_opts[:heading_size] = heading_size.to_i unless heading_size.nil? else # Assume it is a host rw = Rex::Socket::RangeWalker.new(arg) if rw.valid? rw.each do |ip| tbl_opts[:hosts] << ip end else print_warning "#{arg} is an invalid hostname" end end end # Output the table if respond_to? "#{command}_to_table", true table = send "#{command}_to_table", tbl_opts if table.respond_to? "to_#{wiki_type}", true if tbl_opts[:file_name] print_status("Wrote the #{command} table to a file as a #{wiki_type} formatted table") File.open(tbl_opts[:file_name], 'wb') do |f| f.write(table.send("to_#{wiki_type}")) end else print_line table.send "to_#{wiki_type}" end return end end usage(wiki_type) end # # Gets the next set of arguments when parsing command options # # *Note:* This will modify the provided argument list # # @param [Array] args the list of unparsed arguments # @return [Array] the unique list of items before the next '-' in the # provided array # def next_opts(args) opts = [] while (opt = args.shift) if opt =~ /^-/ args.unshift opt break end opts.concat(opt.split(',')) end return opts.uniq end # # Gets the next argument when parsing command options # # *Note:* This will modify the provided argument list # # @param [Array] args the list of unparsed arguments # @return [String, nil] the argument or nil if the argument starts with a '-' # def next_opt(args) return nil if args[0] =~ /^-/ args.shift end # # Outputs the help message # # @param [String] cmd_name the type of the wiki output command to display # help for # def usage(cmd_name = '') print_line "Usage: #{cmd_name} [options] [IP1 IP2,IPn]" print_line print_line 'The first argument must be the type of table to retrieve:' print_line ' creds, hosts, loot, services, vulns' print_line print_line 'OPTIONS:' print_line ' -l,--link Enables links for host addresses' print_line ' -n,--namespace Changes the default namespace for host links' print_line ' -o,--output Write output to a file' print_line ' -p,--port Only return results that relate to given ports' print_line ' -s,--search Only show results that match the provided text' print_line ' -i,--heading-size <1-6> Changes the heading size' print_line ' -h,--help Displays this menu' print_line end # # Outputs credentials in the database (within the current workspace) as a Rex table object # @param [Hash] opts # @option opts [Array] :hosts contains list of hosts used to limit results # @option opts [Array] :ports contains list of ports used to limit results # @option opts [String] :search limits results to those containing a provided string # @return [Rex::Text::Table] table containing credentials # def creds_to_table(opts = {}) tbl = Rex::Text::Table.new({ 'Columns' => ['host', 'port', 'user', 'pass', 'type', 'proof', 'active?'] }) tbl.header = 'Credentials' tbl.headeri = opts[:heading_size] framework.db.creds.each do |cred| if !(opts[:hosts].nil? || opts[:hosts].empty?) && !(opts[:hosts].include? cred.service.host.address) next end if !opts[:ports].nil? && opts[:ports].none? { |p| cred.service.port.eql? p } next end address = cred.service.host.address address = to_wikilink(address, opts[:namespace]) if opts[:links] row = [ address, cred.service.port, cred.user, cred.pass, cred.ptype, cred.proof, cred.active ] if opts[:search] tbl << row if row.any? { |r| /#{opts[:search]}/i.match r.to_s } else tbl << row end end return tbl end # # Outputs host information stored in the database (within the current # workspace) as a Rex table object # @param [Hash] opts # @option opts [Array] :hosts contains list of hosts used to limit results # @option opts [Array] :ports contains list of ports used to limit results # @option opts [String] :search limits results to those containing a provided string # @return [Rex::Text::Table] table containing credentials # def hosts_to_table(opts = {}) tbl = Rex::Text::Table.new({ 'Columns' => ['address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments'] }) tbl.header = 'Hosts' tbl.headeri = opts[:heading_size] framework.db.hosts.each do |host| if !(opts[:hosts].nil? || opts[:hosts].empty?) && !(opts[:hosts].include? host.address) next end if !opts[:ports].nil? && (host.services.map { |s| s[:port] }).none? { |p| opts[:ports].include? p } next end address = host.address address = to_wikilink(address, opts[:namespace]) if opts[:links] row = [ address, host.mac, host.name, host.os_name, host.os_flavor, host.os_sp, host.purpose, host.info, host.comments ] if opts[:search] tbl << row if row.any? { |r| /#{opts[:search]}/i.match r.to_s } else tbl << row end end return tbl end # # Outputs loot information stored in the database (within the current # workspace) as a Rex table object # @param [Hash] opts # @option opts [Array] :hosts contains list of hosts used to limit results # @option opts [Array] :ports contains list of ports used to limit results # @option opts [String] :search limits results to those containing a provided string # @return [Rex::Text::Table] table containing credentials # def loot_to_table(opts = {}) tbl = Rex::Text::Table.new({ 'Columns' => ['host', 'service', 'type', 'name', 'content', 'info', 'path'] }) tbl.header = 'Loot' tbl.headeri = opts[:heading_size] framework.db.loots.each do |loot| if !(opts[:hosts].nil? || opts[:hosts].empty?) && !(opts[:hosts].include? loot.host.address) next end if !(opts[:ports].nil? || opts[:ports].empty?) && (loot.service.nil? || loot.service.port.nil? || !opts[:ports].include?(loot.service.port)) next end if loot.service svc = (loot.service.name || "#{loot.service.port}/#{loot.service.proto}") end address = loot.host.address address = to_wikilink(address, opts[:namespace]) if opts[:links] row = [ address, svc || '', loot.ltype, loot.name, loot.content_type, loot.info, loot.path ] if opts[:search] tbl << row if row.any? { |r| /#{opts[:search]}/i.match r.to_s } else tbl << row end end return tbl end # # Outputs service information stored in the database (within the current # workspace) as a Rex table object # @param [Hash] opts # @option opts [Array] :hosts contains list of hosts used to limit results # @option opts [Array] :ports contains list of ports used to limit results # @option opts [String] :search limits results to those containing a provided string # @return [Rex::Text::Table] table containing credentials # def services_to_table(opts = {}) tbl = Rex::Text::Table.new({ 'Columns' => ['host', 'port', 'proto', 'name', 'state', 'info'] }) tbl.header = 'Services' tbl.headeri = opts[:heading_size] framework.db.services.each do |service| if !(opts[:hosts].nil? || opts[:hosts].empty?) && !(opts[:hosts].include? service.host.address) next end if !(opts[:ports].nil? || opts[:ports].empty?) && opts[:ports].none? { |p| service[:port].eql? p } next end address = service.host.address address = to_wikilink(address, opts[:namespace]) if opts[:links] row = [ address, service.port, service.proto, service.name, service.state, service.info ] if opts[:search] tbl << row if row.any? { |r| /#{opts[:search]}/i.match r.to_s } else tbl << row end end return tbl end # # Outputs vulnerability information stored in the database (within the current # workspace) as a Rex table object # @param [Hash] opts # @option opts [Array] :hosts contains list of hosts used to limit results # @option opts [Array] :ports contains list of ports used to limit results # @option opts [String] :search limits results to those containing a provided string # @return [Rex::Text::Table] table containing credentials # def vulns_to_table(opts = {}) tbl = Rex::Text::Table.new({ 'Columns' => ['Title', 'Host', 'Port', 'Info', 'Detail Count', 'Attempt Count', 'Exploited At', 'Updated At'] }) tbl.header = 'Vulns' tbl.headeri = opts[:heading_size] framework.db.vulns.each do |vuln| if !(opts[:hosts].nil? || opts[:hosts].empty?) && !(opts[:hosts].include? vuln.host.address) next end if !(opts[:ports].nil? || opts[:ports].empty?) && opts[:ports].none? { |p| vuln.service.port.eql? p } next end address = vuln.host.address address = to_wikilink(address, opts[:namespace]) if opts[:links] row = [ vuln.name, address, (vuln.service ? vuln.service.port : ''), vuln.info, vuln.vuln_detail_count, vuln.vuln_attempt_count, vuln.exploited_at, vuln.updated_at, ] if opts[:search] tbl << row if row.any? { |r| /#{opts[:search]}/i.match r.to_s } else tbl << row end end return tbl end # # Converts a value to a wiki link # @param [String] text value to convert to a link # @param [String] namespace optional namespace to set for the link # @return [String] the formatted wiki link def to_wikilink(text, namespace = '') return '[[' + namespace + text + ']]' end end # # Plugin Initialization # # # Constructs a new instance of the plugin and registers the console # dispatcher. It also extends Rex by adding the following methods: # * Rex::Text::Table.to_dokuwiki # * Rex::Text::Table.to_mediawiki # def initialize(framework, opts) super # Extend Rex::Text::Table class so it can output wiki formats add_dokuwiki_to_rex add_mediawiki_to_rex # Add the console dispatcher add_console_dispatcher(WikiCommandDispatcher) end # # The cleanup routine removes the methods added to Rex by the plugin # initialization and then removes the console dispatcher # def cleanup # Cleanup methods added to Rex::Text::Table Rex::Text::Table.class_eval { undef :to_dokuwiki } Rex::Text::Table.class_eval { undef :to_mediawiki } # Deregister the console dispatcher remove_console_dispatcher('Wiki') end # # Returns the plugin's name. # def name 'wiki' end # # This method returns a brief description of the plugin. It should be no # more than 60 characters, but there are no hard limits. # def desc 'Outputs stored database values from the current workspace into DokuWiki or MediaWiki format' end # # The following methods are added here to keep the initialize method # readable # # # Extends Rex tables to be able to create Dokuwiki tables # def add_dokuwiki_to_rex Rex::Text::Table.class_eval do def to_dokuwiki str = prefix.dup # Print the header if there is one. Use headeri to determine wiki paragraph level if header level = '=' * headeri str << level + header + level + "\n" end # Add the column names to the top of the table columns.each do |col| str << '^ ' + col.to_s + ' ' end str << "^\n" unless columns.count.eql? 0 # Fill out the rest of the table with rows rows.each do |row| row.each do |val| cell = val.to_s cell = "#{cell}" if cell.include? '|' str << '| ' + cell + ' ' end str << "|\n" unless rows.count.eql? 0 end return str end end end # # Extends Rex tables to be able to create Mediawiki tables # def add_mediawiki_to_rex Rex::Text::Table.class_eval do def to_mediawiki str = prefix.dup # Print the header if there is one. Use headeri to determine wiki # headline level. Mediawiki does headlines a bit backwards so that # the header level isn't limited. This results in the need to 'flip' # the headline length to standardize it. if header if headeri <= 6 level = '=' * (-headeri + 7) str << "#{level} #{header} #{level}" else str << header.to_s end str << "\n" end # Setup the table with some standard formatting options str << "{|class=\"wikitable\"\n" # Output formatted column names as the first row unless columns.count.eql? 0 str << '!' str << columns.join('!!') str << "\n" end # Add the rows to the table unless rows.count.eql? 0 rows.each do |row| str << "|-\n|" # Try and prevent formatting tags from causing problems bad = ['&', '<', '>', '"', "'", '/'] r = row.join('|| ') r.each_char do |c| if bad.include? c str << Rex::Text.html_encode(c) else str << c end end str << "\n" end end # Finish up the table str << '|}' return str end end end end end