Skip to content

Commit 0302437

Browse files
committed
Land rapid7#1915, smtp user enumeration enhancements
2 parents 4edceea + 8cf5b54 commit 0302437

File tree

1 file changed

+128
-138
lines changed

1 file changed

+128
-138
lines changed

modules/auxiliary/scanner/smtp/smtp_enum.rb

Lines changed: 128 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def initialize
3333
'Author' =>
3434
[
3535
'==[ Alligator Security Team ]==',
36-
'Heyder Andrade <heyder[at]alligatorteam.org>'
36+
'Heyder Andrade <heyder[at]alligatorteam.org>',
37+
'nebulus'
3738
],
3839
'License' => MSF_LICENSE
3940
)
@@ -45,184 +46,173 @@ def initialize
4546
[
4647
true, 'The file that contains a list of probable users accounts.',
4748
File.join(Msf::Config.install_root, 'data', 'wordlists', 'unix_users.txt')
48-
]
49-
)], self.class)
49+
]),
50+
OptBool.new('UNIXONLY', [ true, 'Skip Microsoft bannered servers when testing unix users', true])
51+
], self.class)
5052

5153
deregister_options('MAILTO','MAILFROM')
5254
end
5355

54-
def target
55-
"#{rhost}:#{rport}"
56-
end
57-
58-
def smtp_send(data=nil, con=true)
56+
def smtp_send(data=nil)
5957
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
58+
result=''
59+
code=0
6860
sock.put("#{data}")
69-
@result=sock.get_once
70-
@coderesult=@result[0..2] if @result
61+
result=sock.get_once
62+
result.chomp! if(result)
63+
code = result[0..2].to_i if result
64+
return result, code
65+
rescue Rex::ConnectionError, Errno::ECONNRESET, ::EOFError
66+
return result, code
7167
rescue ::Exception => e
72-
print_error("Error: #{e}")
73-
raise e
68+
print_error("#{rhost}:#{rport} Error smtp_send: '#{e.class}' '#{e}'")
69+
return nil, 0
7470
end
7571
end
7672

7773
def run_host(ip)
78-
@users_found = {}
79-
@mails_found = {}
74+
users_found = {}
75+
result = nil # temp for storing result of SMTP request
76+
code = 0 # status code parsed from result
77+
vrfy = true # if vrfy allowed
78+
expn = true # if expn allowed
79+
rcpt = true # if rcpt allowed and useful
80+
usernames = extract_words(datastore['USER_FILE'])
81+
8082
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}")
83+
connect
84+
result, code = smtp_send(cmd)
8585

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
86+
if(not result)
87+
print_error("#{rhost}:#{rport} Connection but no data...skipping")
88+
return
9789
end
90+
banner.chomp! if (banner)
91+
if(banner =~ /microsoft/i and datastore['UNIXONLY'])
92+
print_status("#{rhost}:#{rport} Skipping microsoft (#{banner})")
93+
return
94+
elsif(banner)
95+
print_status("#{rhost}:#{rport} Banner: #{banner}")
96+
end
97+
98+
domain = result.split()[1]
99+
domain = 'localhost' if(domain == '' or not domain or domain.downcase == 'hello')
98100

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
110101

111-
if(@users_found.empty?)
112-
print_status("#{target} No users or e-mail addresses found.")
102+
vprint_status("#{ip}:#{rport} Domain Name: #{domain}")
103+
104+
result, code = smtp_send("VRFY root\r\n")
105+
vrfy = (code == 250)
106+
users_found = do_enum('VRFY', usernames) if (vrfy)
107+
108+
if(users_found.empty?)
109+
# VRFY failed, lets try EXPN
110+
result, code = smtp_send("EXPN root\r\n")
111+
expn = (code == 250)
112+
users_found = do_enum('EXPN', usernames) if(expn)
113+
end
114+
115+
if(users_found.empty?)
116+
# EXPN/VRFY failed, drop back to RCPT TO
117+
result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n")
118+
if(code == 250)
119+
user = Rex::Text.rand_text_alpha(8)
120+
result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n")
121+
if(code >= 250 and code <= 259)
122+
vprint_status("#{rhost}:#{rport} RCPT TO: Allowed for random user (#{user})...not reliable? #{code} '#{result}'")
123+
rcpt = false
124+
else
125+
smtp_send("RSET\r\n")
126+
users_found = do_rcpt_enum(domain, usernames)
127+
end
113128
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
129+
rcpt = false
120130
end
121131
end
122-
end
123132

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-
)
133+
if(not vrfy and not expn and not rcpt)
134+
print_status("#{rhost}:#{rport} could not be enumerated (no EXPN, no VRFY, invalid RCPT)")
135+
return
133136
end
137+
finish_host(users_found)
138+
disconnect
139+
140+
rescue Rex::ConnectionError, Errno::ECONNRESET, Rex::ConnectionTimeout, EOFError, Errno::ENOPROTOOPT
141+
rescue ::Exception => e
142+
print_error("Error: #{rhost}:#{rport} '#{e.class}' '#{e}'")
143+
end
134144

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(", ")}")
145+
def finish_host(users_found)
146+
if users_found and not users_found.empty?
147+
print_good("#{rhost}:#{rport} Users found: #{users_found.sort.join(", ")}")
139148
report_note(
140149
:host => rhost,
141150
:port => rport,
142-
:type => 'smtp.mails',
143-
:data => {:mails => @mails_found.keys.join(", ")}
151+
:type => 'smtp.users',
152+
:data => {:users => users_found.join(", ")}
144153
)
145154
end
146155
end
147156

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
157+
def kiss_and_make_up(cmd)
158+
vprint_status("#{rhost}:#{rport} SMTP server annoyed...reconnecting and saying HELO again...")
159+
disconnect
160+
connect
161+
smtp_send("HELO localhost\r\n")
162+
result, code = smtp_send("#{cmd}")
163+
result.chomp!
164+
cmd.chomp!
165+
vprint_status("#{rhost}:#{rport} - SMTP - Re-trying #{cmd} received #{code} '#{result}'")
166+
return result,code
161167
end
162168

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
169+
def do_enum(cmd, usernames)
170+
171+
users = []
172+
usernames.each {|user|
173+
next if user.downcase == 'root'
174+
result, code = smtp_send("#{cmd} #{user}\r\n")
175+
vprint_status("#{rhost}:#{rport} - SMTP - Trying #{cmd} #{user} received #{code} '#{result}'")
176+
result, code = kiss_and_make_up("#{cmd} #{user}\r\n") if(code == 0 and result.to_s == '')
177+
if(code == 250)
178+
vprint_status("#{rhost}:#{rport} - Found user: #{user}")
179+
users.push(user)
174180
end
175-
elsif (@coderesult == '501')
176-
print_error "#{target} - MX domain failure for #{@domain}"
177-
return :abort
178-
end
181+
}
182+
return users
179183
end
180184

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
185+
def do_rcpt_enum(domain, usernames)
186+
users = []
187+
usernames.each {|user|
188+
next if user.downcase == 'root'
189+
vprint_status("#{rhost}:#{rport} - SMTP - Trying MAIL FROM: root\@#{domain} / RCPT TO: #{user}...")
190+
result, code = smtp_send("MAIL FROM: root\@#{domain}\r\n")
191+
result, code = kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n") if(code == 0 and result.to_s == '')
192+
193+
if(code == 250)
194+
result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n")
195+
if(code == 0 and result.to_s == '')
196+
kiss_and_make_up("MAIL FROM: root\@#{domain}\r\n")
197+
result, code = smtp_send("RCPT TO: #{user}\@#{domain}\r\n")
198+
end
201199

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
200+
if(code == 250)
201+
vprint_status("#{rhost}:#{rport} - Found user: #{user}")
202+
users.push(user)
214203
end
204+
else
205+
vprint_status("#{rhost}:#{rport} MAIL FROM: #{user} NOT allowed during brute...aborting ( '#{code}' '#{result}')")
206+
break
215207
end
216-
end
208+
smtp_send("RSET\r\n")
209+
}
210+
return users
217211
end
218212

219213
def extract_words(wordfile)
220214
return [] unless wordfile && File.readable?(wordfile)
221-
begin
222-
words = File.open(wordfile, "rb") {|f| f.read}
223-
rescue
224-
return
225-
end
215+
words = File.open(wordfile, "rb") {|f| f.read}
226216
save_array = words.split(/\r?\n/)
227217
return save_array
228218
end

0 commit comments

Comments
 (0)