Skip to content

Commit 7d4c4c3

Browse files
committed
Land rapid7#3699, @dmaloney-r7's ipboard login refactor
2 parents 6bd3675 + 8452ac1 commit 7d4c4c3

File tree

7 files changed

+294
-104
lines changed

7 files changed

+294
-104
lines changed

lib/metasploit/framework/login_scanner/axis2.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def attempt_login(credential)
2020
host, port, {}, ssl, ssl_version
2121
)
2222

23+
http_client = config_client(http_client)
24+
2325
result_opts = {
2426
credential: credential,
2527
host: host,

lib/metasploit/framework/login_scanner/http.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ class HTTP
2929
# @return [String] HTTP method, e.g. "GET", "POST"
3030
attr_accessor :method
3131

32+
# @!attribute user_agent
33+
# @return [String] the User-Agent to use for the HTTP requests
34+
attr_accessor :user_agent
35+
36+
# @!attribute vhost
37+
# @return [String] the Virtual Host name for the target Web Server
38+
attr_accessor :vhost
39+
40+
3241
validates :uri, presence: true, length: { minimum: 1 }
3342

3443
validates :method,
@@ -82,6 +91,9 @@ def attempt_login(credential)
8291
host, port, {}, ssl, ssl_version,
8392
nil, credential.public, credential.private
8493
)
94+
95+
http_client = config_client(http_client)
96+
8597
if credential.realm
8698
http_client.set_config('domain' => credential.realm)
8799
end
@@ -108,6 +120,14 @@ def attempt_login(credential)
108120

109121
private
110122

123+
def config_client(client)
124+
client.set_config(
125+
'vhost' => vhost || host,
126+
'agent' => user_agent
127+
)
128+
client
129+
end
130+
111131
# This method sets the sane defaults for things
112132
# like timeouts and TCP evasion options
113133
def set_sane_defaults
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'metasploit/framework/login_scanner/http'
2+
3+
module Metasploit
4+
module Framework
5+
module LoginScanner
6+
7+
# IP Board login scanner
8+
class IPBoard < HTTP
9+
10+
# (see Base#attempt_login)
11+
def attempt_login(credential)
12+
http_client = Rex::Proto::Http::Client.new(
13+
host, port, {}, ssl, ssl_version
14+
)
15+
16+
http_client = config_client(http_client)
17+
18+
result_opts = {
19+
credential: credential,
20+
host: host,
21+
port: port,
22+
protocol: 'tcp'
23+
}
24+
if ssl
25+
result_opts[:service_name] = 'https'
26+
else
27+
result_opts[:service_name] = 'http'
28+
end
29+
30+
begin
31+
http_client.connect
32+
33+
nonce_request = http_client.request_cgi(
34+
'uri' => uri,
35+
'method' => 'GET'
36+
)
37+
38+
nonce_response = http_client.send_recv(nonce_request)
39+
40+
if nonce_response.body =~ /name='auth_key'\s+value='.*?((?:[a-z0-9]*))'/i
41+
server_nonce = $1
42+
43+
if uri.end_with? '/'
44+
base_uri = uri.gsub(/\/$/, '')
45+
else
46+
base_uri = uri
47+
end
48+
49+
auth_uri = "#{base_uri}/index.php"
50+
51+
request = http_client.request_cgi(
52+
'uri' => auth_uri,
53+
'method' => 'POST',
54+
'vars_get' => {
55+
'app' => 'core',
56+
'module' => 'global',
57+
'section' => 'login',
58+
'do' => 'process'
59+
},
60+
'vars_post' => {
61+
'auth_key' => server_nonce,
62+
'ips_username' => credential.public,
63+
'ips_password' => credential.private
64+
}
65+
)
66+
67+
response = http_client.send_recv(request)
68+
69+
if response && response.get_cookies.include?('ipsconnect') && response.get_cookies.include?('coppa')
70+
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: response)
71+
else
72+
result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT, proof: response)
73+
end
74+
75+
else
76+
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: "Server nonce not present, potentially not an IP Board install or bad URI.")
77+
end
78+
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error
79+
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
80+
end
81+
82+
Result.new(result_opts)
83+
84+
end
85+
86+
87+
# (see Base#set_sane_defaults)
88+
def set_sane_defaults
89+
self.uri = "/forum/" if self.uri.nil?
90+
@method = "POST".freeze
91+
92+
super
93+
end
94+
95+
# The method *must* be "POST", so don't let the user change it
96+
# @raise [RuntimeError]
97+
def method=(_)
98+
raise RuntimeError, "Method must be POST for IPBoard"
99+
end
100+
101+
end
102+
end
103+
end
104+
end
105+

modules/auxiliary/scanner/http/axis_login.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def run_host(ip)
8282
cred_details: cred_collection,
8383
stop_on_success: datastore['STOP_ON_SUCCESS'],
8484
connection_timeout: 5,
85+
user_agent: datastore['UserAgent'],
86+
vhost: datastore['VHOST']
8587
)
8688

8789
scanner.scan! do |result|

modules/auxiliary/scanner/http/http_login.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ def run_host(ip)
140140
cred_details: cred_collection,
141141
stop_on_success: datastore['STOP_ON_SUCCESS'],
142142
connection_timeout: 5,
143+
user_agent: datastore['UserAgent'],
144+
vhost: datastore['VHOST']
143145
)
144146

145147
msg = scanner.check_setup
Lines changed: 41 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
require 'msf/core'
3+
require 'metasploit/framework/login_scanner/ipboard'
34

45
class Metasploit3 < Msf::Auxiliary
56

@@ -25,115 +26,51 @@ def initialize
2526
end
2627

2728
def run_host(ip)
28-
connect
29-
30-
each_user_pass do |user, pass|
31-
do_login(user, pass, ip)
32-
end
33-
end
34-
35-
def do_login(user, pass, ip)
36-
begin
37-
print_status "Connecting to target, searching for IP Board server nonce..."
38-
39-
# Perform the initial request and find the server nonce, which is required to log
40-
# into IP Board
41-
res = send_request_cgi({
42-
'uri' => normalize_uri(target_uri.path),
43-
'method' => 'GET'
44-
}, 10)
45-
46-
unless res
47-
print_error "No response when trying to connect to #{vhost}"
48-
return :connection_error
49-
end
50-
51-
# Grab the key from within the body, or alert that it can't be found and exit out
52-
if res.body =~ /name='auth_key'\s+value='.*?((?:[a-z0-9]*))'/i
53-
server_nonce = $1
54-
print_status "Server nonce found, attempting to log in..."
55-
else
56-
print_error "Server nonce not present, potentially not an IP Board install or bad URI."
57-
print_error "Skipping #{vhost}.."
58-
return :abort
59-
end
60-
61-
# With the server nonce found, try to log into IP Board with the user provided creds
62-
res2 = send_request_cgi({
63-
'uri' => normalize_uri(target_uri.path, "index.php?app=core&module=global&section=login&do=process"),
64-
'method' => 'POST',
65-
'vars_post' => {
66-
'auth_key' => server_nonce,
67-
'ips_username' => user,
68-
'ips_password' => pass
69-
}
70-
})
71-
72-
# Default value of no creds found
73-
valid_creds = false
74-
75-
# Iterate over header response. If the server is setting the ipsconnect and coppa cookie
76-
# then we were able to log in successfully. If they are not set, invalid credentials were
77-
# provided.
29+
cred_collection = Metasploit::Framework::CredentialCollection.new(
30+
blank_passwords: datastore['BLANK_PASSWORDS'],
31+
pass_file: datastore['PASS_FILE'],
32+
password: datastore['PASSWORD'],
33+
user_file: datastore['USER_FILE'],
34+
userpass_file: datastore['USERPASS_FILE'],
35+
username: datastore['USERNAME'],
36+
user_as_pass: datastore['USER_AS_PASS'],
37+
)
7838

79-
if res2.get_cookies.include?('ipsconnect') && res2.get_cookies.include?('coppa')
80-
valid_creds = true
81-
end
39+
scanner = Metasploit::Framework::LoginScanner::IPBoard.new(
40+
host: ip,
41+
port: rport,
42+
uri: normalize_uri(target_uri.path),
43+
proxies: datastore["PROXIES"],
44+
cred_details: cred_collection,
45+
stop_on_success: datastore['STOP_ON_SUCCESS'],
46+
connection_timeout: 5,
47+
user_agent: datastore['UserAgent'],
48+
vhost: datastore['VHOST']
49+
)
8250

83-
# Inform the user if the user supplied credentials were valid or not
84-
if valid_creds
85-
print_good "Username: #{user} and Password: #{pass} are valid credentials!"
86-
register_creds(user, pass, ip)
87-
return :next_user
88-
else
89-
vprint_error "Username: #{user} and Password: #{pass} are invalid credentials!"
90-
return nil
51+
scanner.scan! do |result|
52+
credential_data = result.to_h
53+
credential_data.merge!(
54+
module_fullname: self.fullname,
55+
workspace_id: myworkspace_id
56+
)
57+
case result.status
58+
when Metasploit::Model::Login::Status::SUCCESSFUL
59+
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
60+
credential_core = create_credential(credential_data)
61+
credential_data[:core] = credential_core
62+
create_credential_login(credential_data)
63+
:next_user
64+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
65+
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
66+
invalidate_login(credential_data)
67+
:abort
68+
when Metasploit::Model::Login::Status::INCORRECT
69+
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
70+
invalidate_login(credential_data)
9171
end
92-
93-
rescue ::Timeout::Error
94-
print_error "Connection timed out while attempting to reach #{vhost}!"
95-
return :connection_error
96-
97-
rescue ::Errno::EPIPE
98-
print_error "Broken pipe error when connecting to #{vhost}!"
99-
return :connection_error
10072
end
101-
end
102-
103-
def register_creds(username, password, ipaddr)
104-
# Build service information
105-
service_data = {
106-
address: ipaddr,
107-
port: datastore['RPORT'],
108-
service_name: 'http',
109-
protocol: 'tcp',
110-
workspace_id: myworkspace_id
111-
}
112-
113-
# Build credential information
114-
credential_data = {
115-
origin_type: :service,
116-
module_fullname: self.fullname,
117-
private_data: password,
118-
private_type: :password,
119-
username: username,
120-
workspace_id: myworkspace_id
121-
}
122-
123-
credential_data.merge!(service_data)
124-
credential_core = create_credential(credential_data)
125-
126-
# Assemble the options hash for creating the Metasploit::Credential::Login object
127-
login_data = {
128-
access_level: "user",
129-
core: credential_core,
130-
last_attempted_at: DateTime.now,
131-
status: Metasploit::Model::Login::Status::SUCCESSFUL,
132-
workspace_id: myworkspace_id
133-
}
13473

135-
login_data.merge!(service_data)
136-
create_credential_login(login_data)
13774
end
13875

13976
end

0 commit comments

Comments
 (0)