Skip to content

Commit e810acd

Browse files
committed
Land rapid7#3748, @wchen-r7's HP System Management Homepage LoginScanner Upgrade
2 parents 69d0fad + 67c0ee6 commit e810acd

File tree

3 files changed

+297
-57
lines changed

3 files changed

+297
-57
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
require 'metasploit/framework/login_scanner/http'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
# HP System Management login scanner tested on v6.3.1.24 upto v7.2.1.3 and 7.4
9+
class Smh < HTTP
10+
11+
DEFAULT_PORT = 4848
12+
PRIVATE_TYPES = [ :password ]
13+
CAN_GET_SESSION = true
14+
15+
16+
# (see Base#attempt_login)
17+
def attempt_login(credential)
18+
result_opts = {
19+
credential: credential
20+
}
21+
22+
req_opts = {
23+
'method' => 'POST',
24+
'uri' => '/proxy/ssllogin',
25+
'vars_post' => {
26+
'redirecturl' => '',
27+
'redirectquerystring' => '',
28+
'user' => credential.public,
29+
'password' => credential.private
30+
}
31+
}
32+
33+
res = nil
34+
35+
begin
36+
cli = Rex::Proto::Http::Client.new(host, port, {}, ssl, ssl_version)
37+
cli.connect
38+
req = cli.request_cgi(req_opts)
39+
res = cli.send_recv(req)
40+
41+
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, ::EOFError, ::Timeout::Error
42+
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
43+
return Result.new(result_opts)
44+
end
45+
46+
if res && res.headers['CpqElm-Login'].to_s =~ /success/
47+
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL)
48+
else
49+
result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT)
50+
end
51+
52+
Result.new(result_opts)
53+
end
54+
55+
end
56+
end
57+
end
58+
end

modules/auxiliary/scanner/http/hp_sys_mgmt_login.rb

Lines changed: 149 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
##
55

66
require 'msf/core'
7+
require 'metasploit/framework/login_scanner/smh'
78

89
class Metasploit3 < Msf::Auxiliary
910

@@ -21,81 +22,172 @@ def initialize(info={})
2122
},
2223
'License' => MSF_LICENSE,
2324
'Author' => [ 'sinn3r' ],
24-
'DefaultOptions' => { 'SSL' => true }
25+
'DefaultOptions' =>
26+
{
27+
'SSL' => true,
28+
'RPORT' => 2381,
29+
'USERPASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_userpass.txt"),
30+
'USER_FILE' => File.join(Msf::Config.data_directory, "wordlists", "unix_users.txt"),
31+
'PASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "unix_passwords.txt")
32+
}
2533
))
34+
end
2635

27-
register_options(
28-
[
29-
Opt::RPORT(2381),
30-
OptPath.new('USERPASS_FILE', [ false, "File containing users and passwords separated by space, one pair per line",
31-
File.join(Msf::Config.data_directory, "wordlists", "http_default_userpass.txt") ]),
32-
OptPath.new('USER_FILE', [ false, "File containing users, one per line",
33-
File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt") ]),
34-
OptPath.new('PASS_FILE', [ false, "File containing passwords, one per line",
35-
File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt") ]),
36-
], self.class)
36+
def get_version(res)
37+
if res
38+
return res.body.scan(/smhversion = "HP System Management Homepage v([\d\.]+)"/i).flatten[0] || ''
39+
end
40+
41+
''
42+
end
43+
44+
def is_version_tested?(version)
45+
# As of Sep 4 2014, version 7.4 is the latest and that's the last one we've tested
46+
if Gem::Version.new(version) < Gem::Version.new('7.5')
47+
return true
48+
end
49+
50+
false
3751
end
3852

39-
def anonymous_access?
40-
res = send_request_raw({'uri' => '/'})
53+
def get_system_name(res)
54+
if res
55+
return res.body.scan(/fullsystemname = "(.+)"/i).flatten[0] || ''
56+
end
57+
58+
''
59+
end
60+
61+
def anonymous_access?(res)
4162
return true if res and res.body =~ /username = "hpsmh_anonymous"/
4263
false
4364
end
4465

45-
def do_login(user, pass)
46-
begin
47-
res = send_request_cgi({
48-
'method' => 'POST',
49-
'uri' => '/proxy/ssllogin',
50-
'vars_post' => {
51-
'redirecturl' => '',
52-
'redirectquerystring' => '',
53-
'user' => user,
54-
'password' => pass
55-
}
56-
})
66+
def init_loginscanner(ip)
67+
@cred_collection = Metasploit::Framework::CredentialCollection.new(
68+
blank_passwords: datastore['BLANK_PASSWORDS'],
69+
pass_file: datastore['PASS_FILE'],
70+
password: datastore['PASSWORD'],
71+
user_file: datastore['USER_FILE'],
72+
userpass_file: datastore['USERPASS_FILE'],
73+
username: datastore['USERNAME'],
74+
user_as_pass: datastore['USER_AS_PASS']
75+
)
5776

58-
if not res
59-
vprint_error("#{peer} - Connection timed out")
60-
return :abort
61-
end
62-
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED
63-
vprint_error("#{peer} - Failed to response")
64-
return :abort
65-
end
77+
@scanner = Metasploit::Framework::LoginScanner::Smh.new(
78+
host: ip,
79+
port: rport,
80+
uri: datastore['URI'],
81+
proxies: datastore["PROXIES"],
82+
cred_details: @cred_collection,
83+
stop_on_success: datastore['STOP_ON_SUCCESS'],
84+
connection_timeout: 5
85+
)
86+
87+
@scanner.ssl = datastore['SSL']
88+
@scanner.ssl_version = datastore['SSLVERSION']
89+
end
90+
91+
def do_report(ip, port, result)
92+
service_data = {
93+
address: ip,
94+
port: port,
95+
service_name: 'http',
96+
protocol: 'tcp',
97+
workspace_id: myworkspace_id
98+
}
99+
100+
credential_data = {
101+
module_fullname: self.fullname,
102+
origin_type: :service,
103+
private_data: result.credential.private,
104+
private_type: :password,
105+
username: result.credential.public,
106+
}.merge(service_data)
107+
108+
credential_core = create_credential(credential_data)
66109

67-
if res.headers['CpqElm-Login'].to_s =~ /success/
68-
print_good("#{peer} - Successful login: '#{user}:#{pass}'")
69-
report_auth_info({
70-
:host => rhost,
71-
:port => rport,
72-
:sname => 'https',
73-
:user => user,
74-
:pass => pass,
75-
:proof => "CpqElm-Login: #{res.headers['CpqElm-Login']}"
76-
})
77-
78-
return :next_user
110+
login_data = {
111+
core: credential_core,
112+
last_attempted_at: DateTime.now,
113+
status: result.status
114+
}.merge(service_data)
115+
116+
create_credential_login(login_data)
117+
end
118+
119+
def bruteforce(ip)
120+
@scanner.scan! do |result|
121+
case result.status
122+
when Metasploit::Model::Login::Status::SUCCESSFUL
123+
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
124+
do_report(ip, rport, result)
125+
:next_user
126+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
127+
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
128+
invalidate_login(
129+
address: ip,
130+
port: rport,
131+
protocol: 'tcp',
132+
public: result.credential.public,
133+
private: result.credential.private,
134+
realm_key: result.credential.realm_key,
135+
realm_value: result.credential.realm,
136+
status: result.status
137+
)
138+
:abort
139+
when Metasploit::Model::Login::Status::INCORRECT
140+
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
141+
invalidate_login(
142+
address: ip,
143+
port: rport,
144+
protocol: 'tcp',
145+
public: result.credential.public,
146+
private: result.credential.private,
147+
realm_key: result.credential.realm_key,
148+
realm_value: result.credential.realm,
149+
status: result.status
150+
)
151+
end
79152
end
80153
end
81154

82155

83156
def run_host(ip)
84-
if anonymous_access?
85-
print_status("#{peer} - No login necessary. Server allows anonymous access.")
86-
return
157+
res = send_request_cgi({
158+
'uri' => '/cpqlogin.htm',
159+
'method' => 'GET',
160+
'vars_get' => {
161+
'RedirectUrl' => '/cpqlogin',
162+
'RedirectQueryString' => ''
163+
}
164+
})
165+
166+
version = get_version(res)
167+
unless version.blank?
168+
print_status("#{peer} - Version detected: #{version}")
169+
unless is_version_tested?(version)
170+
print_warning("#{peer} - You're running the module against a version we have not tested")
171+
end
172+
end
173+
174+
sys_name = get_system_name(res)
175+
unless sys_name.blank?
176+
print_status("#{peer} - System name detected: #{sys_name}")
177+
report_note(
178+
:host => ip,
179+
:type => "system.name",
180+
:data => sys_name
181+
)
87182
end
88183

89-
each_user_pass { |user, pass|
90-
# Actually respect the BLANK_PASSWORDS option
91-
next if not datastore['BLANK_PASSWORDS'] and pass.blank?
184+
if anonymous_access?(res)
185+
print_good("#{peer} - No login necessary. Server allows anonymous access.")
186+
return
187+
end
92188

93-
vprint_status("#{peer} - Trying: '#{user}:#{pass}'")
94-
do_login(user, pass)
95-
}
189+
init_loginscanner(ip)
190+
bruteforce(ip)
96191
end
97192
end
98193

99-
=begin
100-
Tested: v6.3.1.24 upto v7.2.1.3
101-
=end
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
2+
require 'spec_helper'
3+
require 'metasploit/framework/login_scanner/smh'
4+
5+
describe Metasploit::Framework::LoginScanner::Smh do
6+
7+
subject(:smh_cli) { described_class.new }
8+
9+
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
10+
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
11+
12+
context "#attempt_login" do
13+
14+
let(:username) { 'admin' }
15+
let(:password) { 'password' }
16+
17+
let(:cred) do
18+
Metasploit::Framework::Credential.new(
19+
paired: true,
20+
public: username,
21+
private: password
22+
)
23+
end
24+
25+
let(:invalid_cred) do
26+
Metasploit::Framework::Credential.new(
27+
paired: true,
28+
public: 'username',
29+
private: 'novalid'
30+
)
31+
end
32+
33+
context "when Rex::Proto::Http::Client#connect raises Rex::ConnectionError" do
34+
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
35+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Rex::ConnectionError)
36+
expect(smh_cli.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
37+
end
38+
end
39+
40+
context "when Rex::Proto::Http::Client#connect raises Timeout::Error" do
41+
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
42+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Timeout::Error)
43+
expect(smh_cli.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
44+
end
45+
end
46+
47+
context "when Rex::Proto::Http::Client#connect raises EOFError" do
48+
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
49+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(EOFError)
50+
expect(smh_cli.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
51+
end
52+
end
53+
54+
context "when valid HP System Management application" do
55+
before :each do
56+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv) do |cli, req|
57+
58+
if req.opts['uri'] && req.opts['uri'].include?('/proxy/ssllogin') &&
59+
req.opts['vars_post'] &&
60+
req.opts['vars_post']['user'] &&
61+
req.opts['vars_post']['user'] == username &&
62+
req.opts['vars_post']['password'] &&
63+
req.opts['vars_post']['password'] == password
64+
res = Rex::Proto::Http::Response.new(200)
65+
res.headers['CpqElm-Login'] = 'success'
66+
res
67+
else
68+
res = Rex::Proto::Http::Response.new(404)
69+
end
70+
71+
res
72+
end
73+
end
74+
75+
context "when valid login" do
76+
it 'returns status Metasploit::Model::Login::Status::SUCCESSFUL' do
77+
expect(smh_cli.attempt_login(cred).status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
78+
end
79+
end
80+
81+
context "when invalid login" do
82+
it 'returns status Metasploit::Model::Login::Status::INCORRECT' do
83+
expect(smh_cli.attempt_login(invalid_cred).status).to eq(Metasploit::Model::Login::Status::INCORRECT)
84+
end
85+
end
86+
87+
end
88+
end
89+
90+
end

0 commit comments

Comments
 (0)