Skip to content

Commit 0678993

Browse files
committed
fix a number of issues with the existing module (slowness, false positives, false negatives, stack traces, enumering unix users on windows systems, etc)
1 parent 882c550 commit 0678993

File tree

1 file changed

+136
-136
lines changed

1 file changed

+136
-136
lines changed

modules/auxiliary/scanner/smtp/smtp_enum.rb

Lines changed: 136 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
##
2+
# $Id: smtp_enum.rb 14774 2012-02-21 01:42:17Z rapid7 $
3+
##
4+
15
##
26
# This file is part of the Metasploit Framework and may be subject to
37
# redistribution and commercial restrictions. Please see the Metasploit
@@ -17,6 +21,7 @@ class Metasploit3 < Msf::Auxiliary
1721
def initialize
1822
super(
1923
'Name' => 'SMTP User Enumeration Utility',
24+
'Version' => '$Revision: 14774 $',
2025
'Description' => %q{
2126
The SMTP service has two internal commands that allow the enumeration
2227
of users: VRFY (confirming the names of valid users) and EXPN (which
@@ -33,7 +38,8 @@ def initialize
3338
'Author' =>
3439
[
3540
'==[ Alligator Security Team ]==',
36-
'Heyder Andrade <heyder[at]alligatorteam.org>'
41+
'Heyder Andrade <heyder[at]alligatorteam.org>',
42+
'nebulus'
3743
],
3844
'License' => MSF_LICENSE
3945
)
@@ -45,8 +51,9 @@ def initialize
4551
[
4652
true, 'The file that contains a list of probable users accounts.',
4753
File.join(Msf::Config.install_root, 'data', 'wordlists', 'unix_users.txt')
48-
]
49-
)], self.class)
54+
]),
55+
OptBool.new('UNIXONLY', [ true, 'Skip Microsoft bannered servers when testing unix users', true])
56+
], self.class)
5057

5158
deregister_options('MAILTO','MAILFROM')
5259
end
@@ -55,174 +62,167 @@ def target
5562
"#{rhost}:#{rport}"
5663
end
5764

58-
def smtp_send(data=nil, con=true)
65+
def smtp_send(data=nil)
5966
begin
60-
@result=''
61-
@coderesult=''
62-
if (con)
63-
@connected=false
64-
connect
65-
select(nil,nil,nil,0.4)
66-
end
67-
@connected=true
67+
result=''
68+
code=0
6869
sock.put("#{data}")
69-
@result=sock.get_once
70-
@coderesult=@result[0..2] if @result
70+
result=sock.get_once
71+
result.chomp! if(result)
72+
code = result[0..2].to_i if result
73+
return result, code
74+
rescue Rex::ConnectionError, Errno::ECONNRESET, ::EOFError
75+
return result, code
7176
rescue ::Exception => e
72-
print_error("Error: #{e}")
73-
raise e
77+
print_error("#{target} Error smtp_send: '#{e.class}' '#{e}' '#{e.backtrace}'")
78+
return nil, 0
7479
end
7580
end
7681

7782
def run_host(ip)
78-
@users_found = {}
79-
@mails_found = {}
83+
users_found = {}
84+
result = nil # temp for storing result of SMTP request
85+
code = 0 # status code parsed from result
86+
vrfy = true # if vrfy allowed
87+
expn = true # if expn allowed
88+
rcpt = true # if rcpt allowed and useful
89+
usernames = extract_words(datastore['USER_FILE'])
90+
8091
cmd = 'HELO' + " " + "localhost" + "\r\n"
81-
smtp_send(cmd,true)
82-
print_status(banner)
83-
@domain = @result.split()[1].split(".")[1..-1].join(".")
84-
print_status("Domain Name: #{@domain}")
92+
connect
93+
result, code = smtp_send(cmd)
8594

86-
begin
87-
cmd = 'VRFY' + " " + "root" + "\r\n"
88-
smtp_send(cmd,!@connected)
89-
if (@result.match(%r{Cannot})) or (@result.match(%r{recognized}))
90-
vprint_status("VRFY command disabled")
91-
elsif (@result.match(%r{restricted}))
92-
vprint_status("VRFY command restricted")
93-
else
94-
vprint_status("VRFY command enabled")
95-
vrfy_ok=true
96-
end
95+
if(not result or result == nil)
96+
print_error("#{target} Connection but no data...skipping")
97+
return
98+
end
99+
banner.chomp! if (banner)
100+
if(banner =~ /microsoft/i and datastore['UNIXONLY'])
101+
print_status("#{target} Skipping microsoft (#{banner})")
102+
return
103+
elsif(banner)
104+
print_status("#{target} Banner: #{banner}")
97105
end
106+
107+
domain = result.split()[1]
108+
domain = 'localhost' if(domain == '' or not domain or domain.downcase == 'hello')
98109

99-
begin
100-
if (vrfy_ok)
101-
extract_words(datastore['USER_FILE']).each {|user|
102-
do_vrfy_enum(user)
103-
}
104-
else
105-
do_mail_from()
106-
extract_words(datastore['USER_FILE']).each {|user|
107-
return finish_host() if ((do_rcpt_enum(user)) == :abort)
108-
}
109-
end
110110

111-
if(@users_found.empty?)
112-
print_status("#{target} No users or e-mail addresses found.")
111+
vprint_status("#{ip}:#{rport} Domain Name: #{domain}")
112+
113+
result, code = smtp_send("VRFY root\r\n")
114+
vrfy = false if (code != 250)
115+
users_found = do_enum('VRFY', usernames) if (vrfy)
116+
117+
if(users_found.empty?)
118+
# VRFY failed, lets try EXPN
119+
result, code = smtp_send("EXPN root\r\n")
120+
expn = false if (code != 250)
121+
users_found = do_enum('EXPN', usernames) if(expn)
122+
end
123+
124+
if(users_found.empty?)
125+
# EXPN/VRFY failed, drop back to RCPT TO
126+
result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n")
127+
if(code == 250)
128+
user = Rex::Text.rand_text_alpha(8)
129+
result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n")
130+
if(code >= 250 and code <= 259)
131+
vprint_status("#{target} RCPT TO: Allowed for random user (#{user})...not reliable? #{code} '#{result}'")
132+
rcpt = false
133+
else
134+
smtp_send("RSET\r\n")
135+
users_found = do_rcpt_enum(domain, usernames)
136+
end
113137
else
114-
vprint_status("#{target} - SMTP - Trying to get valid e-mail addresses")
115-
@users_found.keys.each {|mails|
116-
return finish_host() if((do_get_mails(mails)) == :abort)
117-
}
118-
finish_host()
119-
disconnect
138+
rcpt = false
120139
end
121140
end
122-
end
123141

124-
def finish_host()
125-
if @users_found && !@users_found.empty?
126-
print_good("#{target} Users found: #{@users_found.keys.sort.join(", ")}")
127-
report_note(
128-
:host => rhost,
129-
:port => rport,
130-
:type => 'smtp.users',
131-
:data => {:users => @users_found.keys.join(", ")}
132-
)
142+
if(not vrfy and not expn and not rcpt)
143+
print_status("#{target} could not be enumerated (no EXPN, no VRFY, invalid RCPT)")
144+
return
133145
end
146+
finish_host(users_found)
147+
disconnect
148+
149+
rescue Rex::ConnectionError, Errno::ECONNRESET, Rex::ConnectionTimeout, EOFError, Errno::ENOPROTOOPT
150+
rescue ::Exception => e
151+
print_error( (e.to_str == 'execution expired') ? "Error: #{target} Execution expired" : "Error: #{target} '#{e.class}' '#{e}' '#{e.backtrace}'")
152+
end
134153

135-
if(@mails_found.nil? or @mails_found.empty?)
136-
print_status("#{target} No e-mail addresses found.")
137-
else
138-
print_good("#{target} E-mail addresses found: #{@mails_found.keys.sort.join(", ")}")
154+
def finish_host(users_found)
155+
ip, port = target.split(':')
156+
if users_found and not users_found.empty?
157+
print_good("#{target} Users found: #{users_found.sort.join(", ")}")
139158
report_note(
140-
:host => rhost,
141-
:port => rport,
142-
:type => 'smtp.mails',
143-
:data => {:mails => @mails_found.keys.join(", ")}
159+
:host => ip,
160+
:port => port,
161+
:type => 'smtp.users',
162+
:data => {:users => users_found.join(", ")}
144163
)
145164
end
146165
end
147166

148-
def do_vrfy_enum(user)
149-
cmd = 'VRFY' + " " + user + "\r\n"
150-
smtp_send(cmd,!@connected)
151-
vprint_status("#{target} - SMTP - Trying name: '#{user}'")
152-
case @coderesult.to_i
153-
when (250..259)
154-
print_good "#{target} - Found user: #{user}"
155-
@users_found[user] = :reported
156-
mail = @result.scan(%r{\<(.*)(@)(.*)\>})
157-
unless (mail.nil? || mail.empty?)
158-
@mails_found[mail.to_s] = :reported
159-
end
160-
end
167+
def kiss_and_make_up(cmd)
168+
vprint_status("#{target} SMTP server annoyed...reconnecting and saying HELO again...")
169+
disconnect
170+
connect
171+
smtp_send("HELO localhost\r\n")
172+
result, code = smtp_send("#{cmd}")
173+
result.chomp!
174+
cmd.chomp!
175+
vprint_status("#{target} - SMTP - Re-trying #{cmd} received #{code} '#{result}'")
176+
return result,code
161177
end
162178

163-
def do_mail_from()
164-
vprint_status("Trying to use to RCPT TO command")
165-
cmd = 'MAIL FROM:' + " root@" + @domain + "\r\n"
166-
smtp_send(cmd,!@connected)
167-
if (@coderesult == '501') && @domain.split(".").count > 2
168-
print_error "#{target} - MX domain failure for #{@domain}, trying #{@domain.split(/\./).slice(-2,2).join(".")}"
169-
cmd = 'MAIL FROM:' + " root@" + @domain.split(/\./).slice(-2,2).join(".") + "\r\n"
170-
smtp_send(cmd,!@connected)
171-
if (@coderesult == '501')
172-
print_error "#{target} - MX domain failure for #{@domain.split(/\./).slice(-2,2).join(".")}"
173-
return :abort
179+
def do_enum(cmd, usernames)
180+
181+
users = []
182+
usernames.each {|user|
183+
next if user.downcase == 'root'
184+
result, code = smtp_send("#{cmd} #{user}\r\n")
185+
vprint_status("#{target} - SMTP - Trying #{cmd} #{user} received #{code} '#{result}'")
186+
result, code = kiss_and_make_up("#{cmd} #{user}\r\n") if(code == 0 and result.to_s == '')
187+
if(code == 250)
188+
vprint_status("#{target} - Found user: #{user}")
189+
users.push(user)
174190
end
175-
elsif (@coderesult == '501')
176-
print_error "#{target} - MX domain failure for #{@domain}"
177-
return :abort
178-
end
191+
}
192+
return users
179193
end
180194

181-
def do_rcpt_enum(user)
182-
cmd = 'RCPT TO:' + " " + user + "\r\n"
183-
smtp_send(cmd,!@connected)
184-
vprint_status("#{target} - SMTP - Trying name: '#{user}'")
185-
case @coderesult.to_i
186-
# 550 is User unknown, which obviously isn't fatal when trying to
187-
# enumerate users, so only abort on other 500-series errors. See #4031
188-
when (500..549), (551..599)
189-
print_error "#{target} : #{@result.strip if @result} "
190-
print_error "#{target} : Enumeration not possible"
191-
return :abort
192-
when (250..259)
193-
print_good "#{target} - Found user: #{user}"
194-
@users_found[user] = :reported
195-
mail = @result.scan(%r{\<(.*)(@)(.*)\>})
196-
unless (mail.nil? || mail.empty?)
197-
@mails_found[mail.to_s] = :reported
198-
end
199-
end
200-
end
195+
def do_rcpt_enum(domain, usernames)
196+
users = []
197+
usernames.each {|user|
198+
next if user.downcase == 'root'
199+
vprint_status("#{target} - SMTP - Trying MAIL FROM: root\@#{domain} / RCPT TO: #{user}...")
200+
result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n")
201+
result, code = kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n") if(code == 0 and result.to_s == '')
202+
203+
if(code == 250)
204+
result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n")
205+
if(code == 0 and result.to_s == '')
206+
kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n")
207+
result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n")
208+
end
201209

202-
def do_get_mails(user)
203-
cmd = 'EXPN' + " " + user + "\r\n"
204-
smtp_send(cmd,!@connected)
205-
if (@coderesult == '502')
206-
print_error "#{target} - EXPN : #{@result.strip if @result}"
207-
return :abort
208-
else
209-
unless (@result.nil? || @result.empty?)
210-
mail = @result.scan(%r{\<(.*)(@)(.*)\>})
211-
unless (mail.nil? || mail.empty?)
212-
print_good "#{target} - Mail Found: #{mail}"
213-
@mails_found[mail.to_s] = :reported
210+
if(code == 250)
211+
vprint_status("#{target} - Found user: #{user}")
212+
users.push(user)
214213
end
214+
else
215+
vprint_status("#{target} MAIL FROM: #{user} NOT allowed during brute...aborting ( '#{code}' '#{result}')")
216+
break
215217
end
216-
end
218+
smtp_send("RSET\r\n")
219+
}
220+
return users
217221
end
218222

219223
def extract_words(wordfile)
220224
return [] unless wordfile && File.readable?(wordfile)
221-
begin
222-
words = File.open(wordfile, "rb") {|f| f.read}
223-
rescue
224-
return
225-
end
225+
words = File.open(wordfile, "rb") {|f| f.read}
226226
save_array = words.split(/\r?\n/)
227227
return save_array
228228
end

0 commit comments

Comments
 (0)