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

alternative smtp_enum.rb #1915

Merged
merged 2 commits into from
Jun 7, 2013
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
272 changes: 136 additions & 136 deletions modules/auxiliary/scanner/smtp/smtp_enum.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
##
# $Id: smtp_enum.rb 14774 2012-02-21 01:42:17Z rapid7 $
Copy link
Contributor

Choose a reason for hiding this comment

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

$Id$ was for Subversion. It is no longer needed or expanded in git. This entire comment block can be removed.

##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
Expand All @@ -17,6 +21,7 @@ class Metasploit3 < Msf::Auxiliary
def initialize
super(
'Name' => 'SMTP User Enumeration Utility',
'Version' => '$Revision: 14774 $',
Copy link
Contributor

Choose a reason for hiding this comment

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

$Revision$ is a Subversion expansion like $Id$. The 'Version' entry should be removed since $Revision$ is not used in git.

'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
Expand All @@ -33,7 +38,8 @@ def initialize
'Author' =>
[
'==[ Alligator Security Team ]==',
'Heyder Andrade <heyder[at]alligatorteam.org>'
'Heyder Andrade <heyder[at]alligatorteam.org>',
'nebulus'
],
'License' => MSF_LICENSE
)
Expand All @@ -45,8 +51,9 @@ def initialize
[
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
Expand All @@ -55,174 +62,167 @@ def target
"#{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}'")
Copy link
Contributor

Choose a reason for hiding this comment

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

You should never print backtraces. Log them instead, please.

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is redundant. if result is nil, not result will be true.

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

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.")
vprint_status("#{ip}:#{rport} Domain Name: #{domain}")

result, code = smtp_send("VRFY root\r\n")
vrfy = false if (code != 250)
Copy link
Contributor

Choose a reason for hiding this comment

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

this could be simplified as 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)
users_found = do_enum('EXPN', usernames) if(expn)
end

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
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
rcpt = false
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(", ")}
)
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}'")
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like you're trying to treat timeout differently. You should rescue TimeoutError and deal with it there instead. Also, you should never print backtraces. Log them instead.

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(", ")}")
def finish_host(users_found)
ip, port = target.split(':')
Copy link
Contributor

Choose a reason for hiding this comment

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

target is the concatenation of rhost and rport. Use them instead of pulling them back out of target.

if users_found and not users_found.empty?
print_good("#{target} Users found: #{users_found.sort.join(", ")}")
report_note(
:host => rhost,
:port => rport,
:type => 'smtp.mails',
:data => {:mails => @mails_found.keys.join(", ")}
:host => ip,
Copy link
Contributor

Choose a reason for hiding this comment

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

rhost. See above.

:port => port,
Copy link
Contributor

Choose a reason for hiding this comment

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

rport. See above.

:type => 'smtp.users',
:data => {:users => users_found.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
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_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
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
elsif (@coderesult == '501')
print_error "#{target} - MX domain failure for #{@domain}"
return :abort
end
}
return users
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_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

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(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
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
Expand Down