Skip to content

Commit 284ef5b

Browse files
committed
Land rapid7#5112, Nessus REST Login Module
2 parents 3313dac + 0adc558 commit 284ef5b

File tree

3 files changed

+356
-0
lines changed

3 files changed

+356
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
2+
require 'metasploit/framework/login_scanner/http'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
class Nessus < HTTP
9+
10+
DEFAULT_PORT = 8834
11+
PRIVATE_TYPES = [ :password ]
12+
LIKELY_SERVICE_NAMES = [ 'nessus' ]
13+
LOGIN_STATUS = Metasploit::Model::Login::Status # Shorter name
14+
15+
16+
# Checks if the target is a Tenable Nessus server.
17+
#
18+
# @return [Boolean] TrueClass if target is Nessus server, otherwise FalseClass
19+
def check_setup
20+
login_uri = "/server/properties"
21+
res = send_request({'uri'=> login_uri})
22+
if res && res.body.include?('Nessus')
23+
return true
24+
end
25+
26+
false
27+
end
28+
29+
# Actually doing the login. Called by #attempt_login
30+
#
31+
# @param username [String] The username to try
32+
# @param password [String] The password to try
33+
# @return [Hash]
34+
# * :status [Metasploit::Model::Login::Status]
35+
# * :proof [String] the HTTP response body
36+
def get_login_state(username, password)
37+
login_uri = "#{uri}"
38+
39+
res = send_request({
40+
'uri' => login_uri,
41+
'method' => 'POST',
42+
'vars_post' => {
43+
'username' => username,
44+
'password' => password
45+
}
46+
})
47+
48+
unless res
49+
return {:status => LOGIN_STATUS::UNABLE_TO_CONNECT, :proof => res.to_s}
50+
end
51+
if res.code == 200 && res.body =~ /token/
52+
return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.body.to_s}
53+
end
54+
55+
{:status => LOGIN_STATUS::INCORRECT, :proof => res.to_s}
56+
end
57+
58+
59+
# Attempts to login to Nessus.
60+
#
61+
# @param credential [Metasploit::Framework::Credential] The credential object
62+
# @return [Result] A Result object indicating success or failure
63+
def attempt_login(credential)
64+
result_opts = {
65+
credential: credential,
66+
status: Metasploit::Model::Login::Status::INCORRECT,
67+
proof: nil,
68+
host: host,
69+
port: port,
70+
protocol: 'tcp'
71+
}
72+
73+
begin
74+
result_opts.merge!(get_login_state(credential.public, credential.private))
75+
rescue ::Rex::ConnectionError => e
76+
# Something went wrong during login. 'e' knows what's up.
77+
result_opts.merge!(status: LOGIN_STATUS::UNABLE_TO_CONNECT, proof: e.message)
78+
end
79+
80+
Result.new(result_opts)
81+
end
82+
83+
def set_sane_defaults
84+
super
85+
# nessus_rest_login has the same default in TARGETURI, but rspec doesn't check nessus_rest_login
86+
# so we have to set the default here, too.
87+
self.uri = '/session'
88+
end
89+
90+
end
91+
end
92+
end
93+
end
94+
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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 'metasploit/framework/login_scanner/nessus'
8+
require 'metasploit/framework/credential_collection'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Auxiliary::AuthBrute
14+
include Msf::Auxiliary::Report
15+
include Msf::Auxiliary::Scanner
16+
17+
def initialize(info={})
18+
super(update_info(info,
19+
'Name' => 'Nessus RPC Interface Login Utility',
20+
'Description' => %q{
21+
This module will attempt to authenticate to a Nessus server RPC interface.
22+
},
23+
'Author' => [ 'void_in' ],
24+
'License' => MSF_LICENSE
25+
))
26+
register_options(
27+
[
28+
Opt::RPORT(8834),
29+
OptString.new('TARGETURI', [ true, 'The path to the Nessus server login API', '/session']),
30+
OptBool.new('SSL', [true, 'Negotiate SSL for outgoing connections', true]),
31+
OptEnum.new('SSLVersion', [false, 'Specify the version of SSL that should be used', 'TLS1', ['SSL2', 'SSL3', 'TLS1']])
32+
], self.class)
33+
end
34+
35+
36+
# Initializes CredentialCollection and Nessus Scanner
37+
def init(ip)
38+
@cred_collection = Metasploit::Framework::CredentialCollection.new(
39+
blank_passwords: datastore['BLANK_PASSWORDS'],
40+
pass_file: datastore['PASS_FILE'],
41+
password: datastore['PASSWORD'],
42+
user_file: datastore['USER_FILE'],
43+
userpass_file: datastore['USERPASS_FILE'],
44+
username: datastore['USERNAME'],
45+
user_as_pass: datastore['USER_AS_PASS']
46+
)
47+
48+
@scanner = Metasploit::Framework::LoginScanner::Nessus.new(
49+
host: ip,
50+
port: datastore['RPORT'],
51+
uri: datastore['TARGETURI'],
52+
cred_details: @cred_collection,
53+
stop_on_success: datastore['STOP_ON_SUCCESS'],
54+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
55+
connection_timeout: 5
56+
)
57+
@scanner.ssl = datastore['SSL']
58+
@scanner.ssl_version = datastore['SSLVERSION']
59+
end
60+
61+
62+
# Reports a good login credential
63+
def do_report(ip, port, result)
64+
service_data = {
65+
address: ip,
66+
port: port,
67+
service_name: 'http',
68+
protocol: 'tcp',
69+
workspace_id: myworkspace_id
70+
}
71+
72+
credential_data = {
73+
module_fullname: self.fullname,
74+
origin_type: :service,
75+
private_data: result.credential.private,
76+
private_type: :password,
77+
username: result.credential.public,
78+
}.merge(service_data)
79+
80+
login_data = {
81+
core: create_credential(credential_data),
82+
last_attempted_at: DateTime.now,
83+
status: result.status,
84+
proof: result.proof
85+
}.merge(service_data)
86+
87+
create_credential_login(login_data)
88+
end
89+
90+
91+
# Attempts to login
92+
def bruteforce(ip)
93+
@scanner.scan! do |result|
94+
case result.status
95+
when Metasploit::Model::Login::Status::SUCCESSFUL
96+
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
97+
do_report(ip, rport, result)
98+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
99+
vprint_brute :level => :verror, :ip => ip, :msg => result.proof
100+
invalidate_login(
101+
address: ip,
102+
port: rport,
103+
protocol: 'tcp',
104+
public: result.credential.public,
105+
private: result.credential.private,
106+
realm_key: result.credential.realm_key,
107+
realm_value: result.credential.realm,
108+
status: result.status,
109+
proof: result.proof
110+
)
111+
when Metasploit::Model::Login::Status::INCORRECT
112+
vprint_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
113+
invalidate_login(
114+
address: ip,
115+
port: rport,
116+
protocol: 'tcp',
117+
public: result.credential.public,
118+
private: result.credential.private,
119+
realm_key: result.credential.realm_key,
120+
realm_value: result.credential.realm,
121+
status: result.status,
122+
proof: result.proof
123+
)
124+
end
125+
end
126+
end
127+
128+
129+
# Start here
130+
def run_host(ip)
131+
init(ip)
132+
unless @scanner.check_setup
133+
print_brute :level => :error, :ip => ip, :msg => 'Target is not a Tenable Nessus server'
134+
return
135+
end
136+
137+
bruteforce(ip)
138+
end
139+
140+
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
require 'spec_helper'
2+
require 'metasploit/framework/login_scanner/nessus'
3+
4+
describe Metasploit::Framework::LoginScanner::Nessus do
5+
6+
subject(:http_scanner) { described_class.new }
7+
8+
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
9+
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
10+
11+
let(:username) do
12+
'username'
13+
end
14+
15+
let(:good_password) do
16+
'good_password'
17+
end
18+
19+
let(:bad_password) do
20+
'bad_password'
21+
end
22+
23+
let(:successful_auth_response) do
24+
res = Rex::Proto::Http::Response.new(200, 'OK')
25+
res.body = 'token'
26+
res
27+
end
28+
29+
let(:fail_auth_response) do
30+
Rex::Proto::Http::Response.new(401, 'Unauthorized')
31+
end
32+
33+
let(:response) do
34+
Rex::Proto::Http::Response.new(200, 'OK')
35+
end
36+
37+
before(:each) do
38+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:request_cgi).with(any_args)
39+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv).with(any_args).and_return(response)
40+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:set_config).with(any_args)
41+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:close)
42+
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect)
43+
end
44+
45+
describe '#check_setup' do
46+
let(:msp_html_response) do
47+
res = Rex::Proto::Http::Response.new(200, 'OK')
48+
res.body = 'Nessus'
49+
res
50+
end
51+
52+
context 'when target is Nessus' do
53+
let(:response) { msp_html_response }
54+
it 'returns true' do
55+
expect(http_scanner.check_setup).to be_truthy
56+
end
57+
end
58+
59+
context 'when target is not Nessus' do
60+
it 'returns false' do
61+
expect(http_scanner.check_setup).to be_falsey
62+
end
63+
end
64+
end
65+
66+
describe '#get_login_state' do
67+
it 'sends a login request to /session' do
68+
allow(http_scanner).to receive(:send_request).with(hash_including('uri'=>'/session')).and_return(response)
69+
http_scanner.get_login_state(username, good_password)
70+
end
71+
72+
it 'sends a login request containing the username and password' do
73+
expected_hash = {
74+
'vars_post' => {
75+
"username" => username,
76+
"password" => good_password
77+
}
78+
}
79+
allow(http_scanner).to receive(:send_request).with(hash_including(expected_hash)).and_return(response)
80+
http_scanner.get_login_state(username, good_password)
81+
end
82+
83+
context 'when the credential is valid' do
84+
let(:response) { successful_auth_response }
85+
it 'returns a hash indicating a successful login' do
86+
successful_status = Metasploit::Model::Login::Status::SUCCESSFUL
87+
expect(http_scanner.get_login_state(username, good_password)[:status]).to eq(successful_status)
88+
end
89+
end
90+
91+
context 'when the creential is invalid' do
92+
let(:response) { fail_auth_response }
93+
it 'returns a hash indicating an incorrect cred' do
94+
incorrect_status = Metasploit::Model::Login::Status::INCORRECT
95+
expect(http_scanner.get_login_state(username, good_password)[:status]).to eq(incorrect_status)
96+
end
97+
end
98+
end
99+
100+
describe '#attempt_login' do
101+
context 'when the credential is valid' do
102+
let(:response) { successful_auth_response }
103+
104+
it 'returns a Result object indicating a successful login' do
105+
cred_obj = Metasploit::Framework::Credential.new(public: username, private: good_password)
106+
result = http_scanner.attempt_login(cred_obj)
107+
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
108+
expect(result.status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
109+
end
110+
end
111+
112+
context 'when the credential is invalid' do
113+
let(:response) { fail_auth_response }
114+
it 'returns a Result object indicating an incorrect cred' do
115+
cred_obj = Metasploit::Framework::Credential.new(public: username, private: bad_password)
116+
result = http_scanner.attempt_login(cred_obj)
117+
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
118+
expect(result.status).to eq(Metasploit::Model::Login::Status::INCORRECT)
119+
end
120+
end
121+
end
122+
end

0 commit comments

Comments
 (0)