From 067899341e40351a5c8f0752e75327a579e63fb9 Mon Sep 17 00:00:00 2001 From: Thomas Ring Date: Thu, 6 Jun 2013 13:26:04 -0500 Subject: [PATCH 1/2] fix a number of issues with the existing module (slowness, false positives, false negatives, stack traces, enumering unix users on windows systems, etc) --- modules/auxiliary/scanner/smtp/smtp_enum.rb | 306 ++++++++++---------- 1 file changed, 153 insertions(+), 153 deletions(-) diff --git a/modules/auxiliary/scanner/smtp/smtp_enum.rb b/modules/auxiliary/scanner/smtp/smtp_enum.rb index dfc449ab9a..8a3adcf6e9 100644 --- a/modules/auxiliary/scanner/smtp/smtp_enum.rb +++ b/modules/auxiliary/scanner/smtp/smtp_enum.rb @@ -1,3 +1,7 @@ +## +# $Id: smtp_enum.rb 14774 2012-02-21 01:42:17Z rapid7 $ +## + ## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit @@ -17,6 +21,7 @@ class Metasploit3 < Msf::Auxiliary def initialize super( 'Name' => 'SMTP User Enumeration Utility', + 'Version' => '$Revision: 14774 $', 'Description' => %q{ The SMTP service has two internal commands that allow the enumeration of users: VRFY (confirming the names of valid users) and EXPN (which @@ -33,7 +38,8 @@ class Metasploit3 < Msf::Auxiliary 'Author' => [ '==[ Alligator Security Team ]==', - 'Heyder Andrade ' + 'Heyder Andrade ', + 'nebulus' ], 'License' => MSF_LICENSE ) @@ -45,8 +51,9 @@ class Metasploit3 < Msf::Auxiliary [ true, 'The file that contains a list of probable users accounts.', File.join(Msf::Config.install_root, 'data', 'wordlists', 'unix_users.txt') - ] - )], self.class) + ]), + OptBool.new('UNIXONLY', [ true, 'Skip Microsoft bannered servers when testing unix users', true]) + ], self.class) deregister_options('MAILTO','MAILFROM') end @@ -55,174 +62,167 @@ class Metasploit3 < Msf::Auxiliary "#{rhost}:#{rport}" end - def smtp_send(data=nil, con=true) + def smtp_send(data=nil) begin - @result='' - @coderesult='' - if (con) - @connected=false - connect - select(nil,nil,nil,0.4) - end - @connected=true + result='' + code=0 sock.put("#{data}") - @result=sock.get_once - @coderesult=@result[0..2] if @result + result=sock.get_once + result.chomp! if(result) + code = result[0..2].to_i if result + return result, code + rescue Rex::ConnectionError, Errno::ECONNRESET, ::EOFError + return result, code rescue ::Exception => e - print_error("Error: #{e}") - raise e + print_error("#{target} Error smtp_send: '#{e.class}' '#{e}' '#{e.backtrace}'") + return nil, 0 end end def run_host(ip) - @users_found = {} - @mails_found = {} + users_found = {} + result = nil # temp for storing result of SMTP request + code = 0 # status code parsed from result + vrfy = true # if vrfy allowed + expn = true # if expn allowed + rcpt = true # if rcpt allowed and useful + usernames = extract_words(datastore['USER_FILE']) + cmd = 'HELO' + " " + "localhost" + "\r\n" - smtp_send(cmd,true) - print_status(banner) - @domain = @result.split()[1].split(".")[1..-1].join(".") - print_status("Domain Name: #{@domain}") + connect + result, code = smtp_send(cmd) - begin - cmd = 'VRFY' + " " + "root" + "\r\n" - smtp_send(cmd,!@connected) - if (@result.match(%r{Cannot})) or (@result.match(%r{recognized})) - vprint_status("VRFY command disabled") - elsif (@result.match(%r{restricted})) - vprint_status("VRFY command restricted") - else - vprint_status("VRFY command enabled") - vrfy_ok=true - end + if(not result or result == nil) + print_error("#{target} Connection but no data...skipping") + return + end + banner.chomp! if (banner) + if(banner =~ /microsoft/i and datastore['UNIXONLY']) + print_status("#{target} Skipping microsoft (#{banner})") + return + elsif(banner) + print_status("#{target} Banner: #{banner}") + end + + domain = result.split()[1] + domain = 'localhost' if(domain == '' or not domain or domain.downcase == 'hello') + + + vprint_status("#{ip}:#{rport} Domain Name: #{domain}") + + result, code = smtp_send("VRFY root\r\n") + vrfy = false if (code != 250) + users_found = do_enum('VRFY', usernames) if (vrfy) + + if(users_found.empty?) + # VRFY failed, lets try EXPN + result, code = smtp_send("EXPN root\r\n") + expn = false if (code != 250) + users_found = do_enum('EXPN', usernames) if(expn) end - begin - if (vrfy_ok) - extract_words(datastore['USER_FILE']).each {|user| - do_vrfy_enum(user) - } - else - do_mail_from() - extract_words(datastore['USER_FILE']).each {|user| - return finish_host() if ((do_rcpt_enum(user)) == :abort) - } - end - - if(@users_found.empty?) - print_status("#{target} No users or e-mail addresses found.") - else - vprint_status("#{target} - SMTP - Trying to get valid e-mail addresses") - @users_found.keys.each {|mails| - return finish_host() if((do_get_mails(mails)) == :abort) - } - finish_host() - disconnect - end - end - end - - def finish_host() - if @users_found && !@users_found.empty? - print_good("#{target} Users found: #{@users_found.keys.sort.join(", ")}") - report_note( - :host => rhost, - :port => rport, - :type => 'smtp.users', - :data => {:users => @users_found.keys.join(", ")} - ) - end - - if(@mails_found.nil? or @mails_found.empty?) - print_status("#{target} No e-mail addresses found.") - else - print_good("#{target} E-mail addresses found: #{@mails_found.keys.sort.join(", ")}") - report_note( - :host => rhost, - :port => rport, - :type => 'smtp.mails', - :data => {:mails => @mails_found.keys.join(", ")} - ) - end - end - - def do_vrfy_enum(user) - cmd = 'VRFY' + " " + user + "\r\n" - smtp_send(cmd,!@connected) - vprint_status("#{target} - SMTP - Trying name: '#{user}'") - case @coderesult.to_i - when (250..259) - print_good "#{target} - Found user: #{user}" - @users_found[user] = :reported - mail = @result.scan(%r{\<(.*)(@)(.*)\>}) - unless (mail.nil? || mail.empty?) - @mails_found[mail.to_s] = :reported - end - end - end - - def do_mail_from() - vprint_status("Trying to use to RCPT TO command") - cmd = 'MAIL FROM:' + " root@" + @domain + "\r\n" - smtp_send(cmd,!@connected) - if (@coderesult == '501') && @domain.split(".").count > 2 - print_error "#{target} - MX domain failure for #{@domain}, trying #{@domain.split(/\./).slice(-2,2).join(".")}" - cmd = 'MAIL FROM:' + " root@" + @domain.split(/\./).slice(-2,2).join(".") + "\r\n" - smtp_send(cmd,!@connected) - if (@coderesult == '501') - print_error "#{target} - MX domain failure for #{@domain.split(/\./).slice(-2,2).join(".")}" - return :abort - end - elsif (@coderesult == '501') - print_error "#{target} - MX domain failure for #{@domain}" - return :abort - end - end - - def do_rcpt_enum(user) - cmd = 'RCPT TO:' + " " + user + "\r\n" - smtp_send(cmd,!@connected) - vprint_status("#{target} - SMTP - Trying name: '#{user}'") - case @coderesult.to_i - # 550 is User unknown, which obviously isn't fatal when trying to - # enumerate users, so only abort on other 500-series errors. See #4031 - when (500..549), (551..599) - print_error "#{target} : #{@result.strip if @result} " - print_error "#{target} : Enumeration not possible" - return :abort - when (250..259) - print_good "#{target} - Found user: #{user}" - @users_found[user] = :reported - mail = @result.scan(%r{\<(.*)(@)(.*)\>}) - unless (mail.nil? || mail.empty?) - @mails_found[mail.to_s] = :reported - end - end - end - - def do_get_mails(user) - cmd = 'EXPN' + " " + user + "\r\n" - smtp_send(cmd,!@connected) - if (@coderesult == '502') - print_error "#{target} - EXPN : #{@result.strip if @result}" - return :abort - else - unless (@result.nil? || @result.empty?) - mail = @result.scan(%r{\<(.*)(@)(.*)\>}) - unless (mail.nil? || mail.empty?) - print_good "#{target} - Mail Found: #{mail}" - @mails_found[mail.to_s] = :reported + if(users_found.empty?) + # EXPN/VRFY failed, drop back to RCPT TO + result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n") + if(code == 250) + user = Rex::Text.rand_text_alpha(8) + result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n") + if(code >= 250 and code <= 259) + vprint_status("#{target} RCPT TO: Allowed for random user (#{user})...not reliable? #{code} '#{result}'") + rcpt = false + else + smtp_send("RSET\r\n") + users_found = do_rcpt_enum(domain, usernames) end + else + rcpt = false end end + + if(not vrfy and not expn and not rcpt) + print_status("#{target} could not be enumerated (no EXPN, no VRFY, invalid RCPT)") + return + end + finish_host(users_found) + disconnect + + rescue Rex::ConnectionError, Errno::ECONNRESET, Rex::ConnectionTimeout, EOFError, Errno::ENOPROTOOPT + rescue ::Exception => e + print_error( (e.to_str == 'execution expired') ? "Error: #{target} Execution expired" : "Error: #{target} '#{e.class}' '#{e}' '#{e.backtrace}'") + end + + def finish_host(users_found) + ip, port = target.split(':') + if users_found and not users_found.empty? + print_good("#{target} Users found: #{users_found.sort.join(", ")}") + report_note( + :host => ip, + :port => port, + :type => 'smtp.users', + :data => {:users => users_found.join(", ")} + ) + end + end + + def kiss_and_make_up(cmd) + vprint_status("#{target} SMTP server annoyed...reconnecting and saying HELO again...") + disconnect + connect + smtp_send("HELO localhost\r\n") + result, code = smtp_send("#{cmd}") + result.chomp! + cmd.chomp! + vprint_status("#{target} - SMTP - Re-trying #{cmd} received #{code} '#{result}'") + return result,code + end + + def do_enum(cmd, usernames) + + users = [] + usernames.each {|user| + next if user.downcase == 'root' + result, code = smtp_send("#{cmd} #{user}\r\n") + vprint_status("#{target} - SMTP - Trying #{cmd} #{user} received #{code} '#{result}'") + result, code = kiss_and_make_up("#{cmd} #{user}\r\n") if(code == 0 and result.to_s == '') + if(code == 250) + vprint_status("#{target} - Found user: #{user}") + users.push(user) + end + } + return users + end + + def do_rcpt_enum(domain, usernames) + users = [] + usernames.each {|user| + next if user.downcase == 'root' + vprint_status("#{target} - SMTP - Trying MAIL FROM: root\@#{domain} / RCPT TO: #{user}...") + result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n") + result, code = kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n") if(code == 0 and result.to_s == '') + + if(code == 250) + result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n") + if(code == 0 and result.to_s == '') + kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n") + result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n") + end + + if(code == 250) + vprint_status("#{target} - Found user: #{user}") + users.push(user) + end + else + vprint_status("#{target} MAIL FROM: #{user} NOT allowed during brute...aborting ( '#{code}' '#{result}')") + break + end + smtp_send("RSET\r\n") + } + return users end def extract_words(wordfile) return [] unless wordfile && File.readable?(wordfile) - begin - words = File.open(wordfile, "rb") {|f| f.read} - rescue - return - end + words = File.open(wordfile, "rb") {|f| f.read} save_array = words.split(/\r?\n/) return save_array end From 8cf5b548c37af68228b3b958b8d7e295b17a11db Mon Sep 17 00:00:00 2001 From: Thomas Ring Date: Thu, 6 Jun 2013 14:23:25 -0500 Subject: [PATCH 2/2] make recommended changes --- modules/auxiliary/scanner/smtp/smtp_enum.rb | 50 +++++++++------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/modules/auxiliary/scanner/smtp/smtp_enum.rb b/modules/auxiliary/scanner/smtp/smtp_enum.rb index 8a3adcf6e9..c8e26389b8 100644 --- a/modules/auxiliary/scanner/smtp/smtp_enum.rb +++ b/modules/auxiliary/scanner/smtp/smtp_enum.rb @@ -1,7 +1,3 @@ -## -# $Id: smtp_enum.rb 14774 2012-02-21 01:42:17Z rapid7 $ -## - ## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit @@ -21,7 +17,6 @@ class Metasploit3 < Msf::Auxiliary def initialize super( 'Name' => 'SMTP User Enumeration Utility', - 'Version' => '$Revision: 14774 $', 'Description' => %q{ The SMTP service has two internal commands that allow the enumeration of users: VRFY (confirming the names of valid users) and EXPN (which @@ -58,10 +53,6 @@ class Metasploit3 < Msf::Auxiliary deregister_options('MAILTO','MAILFROM') end - def target - "#{rhost}:#{rport}" - end - def smtp_send(data=nil) begin result='' @@ -74,7 +65,7 @@ class Metasploit3 < Msf::Auxiliary rescue Rex::ConnectionError, Errno::ECONNRESET, ::EOFError return result, code rescue ::Exception => e - print_error("#{target} Error smtp_send: '#{e.class}' '#{e}' '#{e.backtrace}'") + print_error("#{rhost}:#{rport} Error smtp_send: '#{e.class}' '#{e}'") return nil, 0 end end @@ -92,16 +83,16 @@ class Metasploit3 < Msf::Auxiliary connect result, code = smtp_send(cmd) - if(not result or result == nil) - print_error("#{target} Connection but no data...skipping") + if(not result) + print_error("#{rhost}:#{rport} Connection but no data...skipping") return end banner.chomp! if (banner) if(banner =~ /microsoft/i and datastore['UNIXONLY']) - print_status("#{target} Skipping microsoft (#{banner})") + print_status("#{rhost}:#{rport} Skipping microsoft (#{banner})") return elsif(banner) - print_status("#{target} Banner: #{banner}") + print_status("#{rhost}:#{rport} Banner: #{banner}") end domain = result.split()[1] @@ -111,13 +102,13 @@ class Metasploit3 < Msf::Auxiliary vprint_status("#{ip}:#{rport} Domain Name: #{domain}") result, code = smtp_send("VRFY root\r\n") - vrfy = false if (code != 250) + vrfy = (code == 250) users_found = do_enum('VRFY', usernames) if (vrfy) if(users_found.empty?) # VRFY failed, lets try EXPN result, code = smtp_send("EXPN root\r\n") - expn = false if (code != 250) + expn = (code == 250) users_found = do_enum('EXPN', usernames) if(expn) end @@ -128,7 +119,7 @@ class Metasploit3 < Msf::Auxiliary user = Rex::Text.rand_text_alpha(8) result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n") if(code >= 250 and code <= 259) - vprint_status("#{target} RCPT TO: Allowed for random user (#{user})...not reliable? #{code} '#{result}'") + vprint_status("#{rhost}:#{rport} RCPT TO: Allowed for random user (#{user})...not reliable? #{code} '#{result}'") rcpt = false else smtp_send("RSET\r\n") @@ -140,7 +131,7 @@ class Metasploit3 < Msf::Auxiliary end if(not vrfy and not expn and not rcpt) - print_status("#{target} could not be enumerated (no EXPN, no VRFY, invalid RCPT)") + print_status("#{rhost}:#{rport} could not be enumerated (no EXPN, no VRFY, invalid RCPT)") return end finish_host(users_found) @@ -148,16 +139,15 @@ class Metasploit3 < Msf::Auxiliary rescue Rex::ConnectionError, Errno::ECONNRESET, Rex::ConnectionTimeout, EOFError, Errno::ENOPROTOOPT rescue ::Exception => e - print_error( (e.to_str == 'execution expired') ? "Error: #{target} Execution expired" : "Error: #{target} '#{e.class}' '#{e}' '#{e.backtrace}'") + print_error("Error: #{rhost}:#{rport} '#{e.class}' '#{e}'") end def finish_host(users_found) - ip, port = target.split(':') if users_found and not users_found.empty? - print_good("#{target} Users found: #{users_found.sort.join(", ")}") + print_good("#{rhost}:#{rport} Users found: #{users_found.sort.join(", ")}") report_note( - :host => ip, - :port => port, + :host => rhost, + :port => rport, :type => 'smtp.users', :data => {:users => users_found.join(", ")} ) @@ -165,14 +155,14 @@ class Metasploit3 < Msf::Auxiliary end def kiss_and_make_up(cmd) - vprint_status("#{target} SMTP server annoyed...reconnecting and saying HELO again...") + vprint_status("#{rhost}:#{rport} SMTP server annoyed...reconnecting and saying HELO again...") disconnect connect smtp_send("HELO localhost\r\n") result, code = smtp_send("#{cmd}") result.chomp! cmd.chomp! - vprint_status("#{target} - SMTP - Re-trying #{cmd} received #{code} '#{result}'") + vprint_status("#{rhost}:#{rport} - SMTP - Re-trying #{cmd} received #{code} '#{result}'") return result,code end @@ -182,10 +172,10 @@ class Metasploit3 < Msf::Auxiliary usernames.each {|user| next if user.downcase == 'root' result, code = smtp_send("#{cmd} #{user}\r\n") - vprint_status("#{target} - SMTP - Trying #{cmd} #{user} received #{code} '#{result}'") + vprint_status("#{rhost}:#{rport} - SMTP - Trying #{cmd} #{user} received #{code} '#{result}'") result, code = kiss_and_make_up("#{cmd} #{user}\r\n") if(code == 0 and result.to_s == '') if(code == 250) - vprint_status("#{target} - Found user: #{user}") + vprint_status("#{rhost}:#{rport} - Found user: #{user}") users.push(user) end } @@ -196,7 +186,7 @@ class Metasploit3 < Msf::Auxiliary users = [] usernames.each {|user| next if user.downcase == 'root' - vprint_status("#{target} - SMTP - Trying MAIL FROM: root\@#{domain} / RCPT TO: #{user}...") + vprint_status("#{rhost}:#{rport} - SMTP - Trying MAIL FROM: root\@#{domain} / RCPT TO: #{user}...") result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n") result, code = kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n") if(code == 0 and result.to_s == '') @@ -208,11 +198,11 @@ class Metasploit3 < Msf::Auxiliary end if(code == 250) - vprint_status("#{target} - Found user: #{user}") + vprint_status("#{rhost}:#{rport} - Found user: #{user}") users.push(user) end else - vprint_status("#{target} MAIL FROM: #{user} NOT allowed during brute...aborting ( '#{code}' '#{result}')") + vprint_status("#{rhost}:#{rport} MAIL FROM: #{user} NOT allowed during brute...aborting ( '#{code}' '#{result}')") break end smtp_send("RSET\r\n")