1
+ ##
2
+ # $Id: smtp_enum.rb 14774 2012-02-21 01:42:17Z rapid7 $
3
+ ##
4
+
1
5
##
2
6
# This file is part of the Metasploit Framework and may be subject to
3
7
# redistribution and commercial restrictions. Please see the Metasploit
@@ -17,6 +21,7 @@ class Metasploit3 < Msf::Auxiliary
17
21
def initialize
18
22
super (
19
23
'Name' => 'SMTP User Enumeration Utility' ,
24
+ 'Version' => '$Revision: 14774 $' ,
20
25
'Description' => %q{
21
26
The SMTP service has two internal commands that allow the enumeration
22
27
of users: VRFY (confirming the names of valid users) and EXPN (which
@@ -33,7 +38,8 @@ def initialize
33
38
'Author' =>
34
39
[
35
40
'==[ Alligator Security Team ]==' ,
36
- 'Heyder Andrade <heyder[at]alligatorteam.org>'
41
+ 'Heyder Andrade <heyder[at]alligatorteam.org>' ,
42
+ 'nebulus'
37
43
] ,
38
44
'License' => MSF_LICENSE
39
45
)
@@ -45,8 +51,9 @@ def initialize
45
51
[
46
52
true , 'The file that contains a list of probable users accounts.' ,
47
53
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 )
50
57
51
58
deregister_options ( 'MAILTO' , 'MAILFROM' )
52
59
end
@@ -55,174 +62,167 @@ def target
55
62
"#{ rhost } :#{ rport } "
56
63
end
57
64
58
- def smtp_send ( data = nil , con = true )
65
+ def smtp_send ( data = nil )
59
66
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
68
69
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
71
76
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
74
79
end
75
80
end
76
81
77
82
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
+
80
91
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 )
85
94
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 } " )
97
105
end
106
+
107
+ domain = result . split ( ) [ 1 ]
108
+ domain = 'localhost' if ( domain == '' or not domain or domain . downcase == 'hello' )
98
109
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
110
110
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
113
137
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
120
139
end
121
140
end
122
- end
123
141
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
133
145
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
134
153
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 ( ", " ) } " )
139
158
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 ( ", " ) }
144
163
)
145
164
end
146
165
end
147
166
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
161
177
end
162
178
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 )
174
190
end
175
- elsif ( @coderesult == '501' )
176
- print_error "#{ target } - MX domain failure for #{ @domain } "
177
- return :abort
178
- end
191
+ }
192
+ return users
179
193
end
180
194
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
201
209
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 )
214
213
end
214
+ else
215
+ vprint_status ( "#{ target } MAIL FROM: #{ user } NOT allowed during brute...aborting ( '#{ code } ' '#{ result } ')" )
216
+ break
215
217
end
216
- end
218
+ smtp_send ( "RSET\r \n " )
219
+ }
220
+ return users
217
221
end
218
222
219
223
def extract_words ( wordfile )
220
224
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 }
226
226
save_array = words . split ( /\r ?\n / )
227
227
return save_array
228
228
end
0 commit comments