Land #3699, @dmaloney-r7's ipboard login refactor
commit
7d4c4c3658
|
@ -20,6 +20,8 @@ module Metasploit
|
||||||
host, port, {}, ssl, ssl_version
|
host, port, {}, ssl, ssl_version
|
||||||
)
|
)
|
||||||
|
|
||||||
|
http_client = config_client(http_client)
|
||||||
|
|
||||||
result_opts = {
|
result_opts = {
|
||||||
credential: credential,
|
credential: credential,
|
||||||
host: host,
|
host: host,
|
||||||
|
|
|
@ -29,6 +29,15 @@ module Metasploit
|
||||||
# @return [String] HTTP method, e.g. "GET", "POST"
|
# @return [String] HTTP method, e.g. "GET", "POST"
|
||||||
attr_accessor :method
|
attr_accessor :method
|
||||||
|
|
||||||
|
# @!attribute user_agent
|
||||||
|
# @return [String] the User-Agent to use for the HTTP requests
|
||||||
|
attr_accessor :user_agent
|
||||||
|
|
||||||
|
# @!attribute vhost
|
||||||
|
# @return [String] the Virtual Host name for the target Web Server
|
||||||
|
attr_accessor :vhost
|
||||||
|
|
||||||
|
|
||||||
validates :uri, presence: true, length: { minimum: 1 }
|
validates :uri, presence: true, length: { minimum: 1 }
|
||||||
|
|
||||||
validates :method,
|
validates :method,
|
||||||
|
@ -82,6 +91,9 @@ module Metasploit
|
||||||
host, port, {}, ssl, ssl_version,
|
host, port, {}, ssl, ssl_version,
|
||||||
nil, credential.public, credential.private
|
nil, credential.public, credential.private
|
||||||
)
|
)
|
||||||
|
|
||||||
|
http_client = config_client(http_client)
|
||||||
|
|
||||||
if credential.realm
|
if credential.realm
|
||||||
http_client.set_config('domain' => credential.realm)
|
http_client.set_config('domain' => credential.realm)
|
||||||
end
|
end
|
||||||
|
@ -108,6 +120,14 @@ module Metasploit
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def config_client(client)
|
||||||
|
client.set_config(
|
||||||
|
'vhost' => vhost || host,
|
||||||
|
'agent' => user_agent
|
||||||
|
)
|
||||||
|
client
|
||||||
|
end
|
||||||
|
|
||||||
# This method sets the sane defaults for things
|
# This method sets the sane defaults for things
|
||||||
# like timeouts and TCP evasion options
|
# like timeouts and TCP evasion options
|
||||||
def set_sane_defaults
|
def set_sane_defaults
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
require 'metasploit/framework/login_scanner/http'
|
||||||
|
|
||||||
|
module Metasploit
|
||||||
|
module Framework
|
||||||
|
module LoginScanner
|
||||||
|
|
||||||
|
# IP Board login scanner
|
||||||
|
class IPBoard < HTTP
|
||||||
|
|
||||||
|
# (see Base#attempt_login)
|
||||||
|
def attempt_login(credential)
|
||||||
|
http_client = Rex::Proto::Http::Client.new(
|
||||||
|
host, port, {}, ssl, ssl_version
|
||||||
|
)
|
||||||
|
|
||||||
|
http_client = config_client(http_client)
|
||||||
|
|
||||||
|
result_opts = {
|
||||||
|
credential: credential,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
protocol: 'tcp'
|
||||||
|
}
|
||||||
|
if ssl
|
||||||
|
result_opts[:service_name] = 'https'
|
||||||
|
else
|
||||||
|
result_opts[:service_name] = 'http'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
http_client.connect
|
||||||
|
|
||||||
|
nonce_request = http_client.request_cgi(
|
||||||
|
'uri' => uri,
|
||||||
|
'method' => 'GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
nonce_response = http_client.send_recv(nonce_request)
|
||||||
|
|
||||||
|
if nonce_response.body =~ /name='auth_key'\s+value='.*?((?:[a-z0-9]*))'/i
|
||||||
|
server_nonce = $1
|
||||||
|
|
||||||
|
if uri.end_with? '/'
|
||||||
|
base_uri = uri.gsub(/\/$/, '')
|
||||||
|
else
|
||||||
|
base_uri = uri
|
||||||
|
end
|
||||||
|
|
||||||
|
auth_uri = "#{base_uri}/index.php"
|
||||||
|
|
||||||
|
request = http_client.request_cgi(
|
||||||
|
'uri' => auth_uri,
|
||||||
|
'method' => 'POST',
|
||||||
|
'vars_get' => {
|
||||||
|
'app' => 'core',
|
||||||
|
'module' => 'global',
|
||||||
|
'section' => 'login',
|
||||||
|
'do' => 'process'
|
||||||
|
},
|
||||||
|
'vars_post' => {
|
||||||
|
'auth_key' => server_nonce,
|
||||||
|
'ips_username' => credential.public,
|
||||||
|
'ips_password' => credential.private
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = http_client.send_recv(request)
|
||||||
|
|
||||||
|
if response && response.get_cookies.include?('ipsconnect') && response.get_cookies.include?('coppa')
|
||||||
|
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: response)
|
||||||
|
else
|
||||||
|
result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT, proof: response)
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
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.")
|
||||||
|
end
|
||||||
|
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error
|
||||||
|
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||||
|
end
|
||||||
|
|
||||||
|
Result.new(result_opts)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# (see Base#set_sane_defaults)
|
||||||
|
def set_sane_defaults
|
||||||
|
self.uri = "/forum/" if self.uri.nil?
|
||||||
|
@method = "POST".freeze
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# The method *must* be "POST", so don't let the user change it
|
||||||
|
# @raise [RuntimeError]
|
||||||
|
def method=(_)
|
||||||
|
raise RuntimeError, "Method must be POST for IPBoard"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -82,6 +82,8 @@ class Metasploit3 < Msf::Auxiliary
|
||||||
cred_details: cred_collection,
|
cred_details: cred_collection,
|
||||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||||
connection_timeout: 5,
|
connection_timeout: 5,
|
||||||
|
user_agent: datastore['UserAgent'],
|
||||||
|
vhost: datastore['VHOST']
|
||||||
)
|
)
|
||||||
|
|
||||||
scanner.scan! do |result|
|
scanner.scan! do |result|
|
||||||
|
|
|
@ -140,6 +140,8 @@ class Metasploit3 < Msf::Auxiliary
|
||||||
cred_details: cred_collection,
|
cred_details: cred_collection,
|
||||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||||
connection_timeout: 5,
|
connection_timeout: 5,
|
||||||
|
user_agent: datastore['UserAgent'],
|
||||||
|
vhost: datastore['VHOST']
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = scanner.check_setup
|
msg = scanner.check_setup
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
require 'msf/core'
|
require 'msf/core'
|
||||||
|
require 'metasploit/framework/login_scanner/ipboard'
|
||||||
|
|
||||||
class Metasploit3 < Msf::Auxiliary
|
class Metasploit3 < Msf::Auxiliary
|
||||||
|
|
||||||
|
@ -25,115 +26,51 @@ class Metasploit3 < Msf::Auxiliary
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_host(ip)
|
def run_host(ip)
|
||||||
connect
|
cred_collection = Metasploit::Framework::CredentialCollection.new(
|
||||||
|
blank_passwords: datastore['BLANK_PASSWORDS'],
|
||||||
|
pass_file: datastore['PASS_FILE'],
|
||||||
|
password: datastore['PASSWORD'],
|
||||||
|
user_file: datastore['USER_FILE'],
|
||||||
|
userpass_file: datastore['USERPASS_FILE'],
|
||||||
|
username: datastore['USERNAME'],
|
||||||
|
user_as_pass: datastore['USER_AS_PASS'],
|
||||||
|
)
|
||||||
|
|
||||||
each_user_pass do |user, pass|
|
scanner = Metasploit::Framework::LoginScanner::IPBoard.new(
|
||||||
do_login(user, pass, ip)
|
host: ip,
|
||||||
|
port: rport,
|
||||||
|
uri: normalize_uri(target_uri.path),
|
||||||
|
proxies: datastore["PROXIES"],
|
||||||
|
cred_details: cred_collection,
|
||||||
|
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||||
|
connection_timeout: 5,
|
||||||
|
user_agent: datastore['UserAgent'],
|
||||||
|
vhost: datastore['VHOST']
|
||||||
|
)
|
||||||
|
|
||||||
|
scanner.scan! do |result|
|
||||||
|
credential_data = result.to_h
|
||||||
|
credential_data.merge!(
|
||||||
|
module_fullname: self.fullname,
|
||||||
|
workspace_id: myworkspace_id
|
||||||
|
)
|
||||||
|
case result.status
|
||||||
|
when Metasploit::Model::Login::Status::SUCCESSFUL
|
||||||
|
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
|
||||||
|
credential_core = create_credential(credential_data)
|
||||||
|
credential_data[:core] = credential_core
|
||||||
|
create_credential_login(credential_data)
|
||||||
|
:next_user
|
||||||
|
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||||
|
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
|
||||||
|
invalidate_login(credential_data)
|
||||||
|
:abort
|
||||||
|
when Metasploit::Model::Login::Status::INCORRECT
|
||||||
|
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
|
||||||
|
invalidate_login(credential_data)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def do_login(user, pass, ip)
|
|
||||||
begin
|
|
||||||
print_status "Connecting to target, searching for IP Board server nonce..."
|
|
||||||
|
|
||||||
# Perform the initial request and find the server nonce, which is required to log
|
|
||||||
# into IP Board
|
|
||||||
res = send_request_cgi({
|
|
||||||
'uri' => normalize_uri(target_uri.path),
|
|
||||||
'method' => 'GET'
|
|
||||||
}, 10)
|
|
||||||
|
|
||||||
unless res
|
|
||||||
print_error "No response when trying to connect to #{vhost}"
|
|
||||||
return :connection_error
|
|
||||||
end
|
|
||||||
|
|
||||||
# Grab the key from within the body, or alert that it can't be found and exit out
|
|
||||||
if res.body =~ /name='auth_key'\s+value='.*?((?:[a-z0-9]*))'/i
|
|
||||||
server_nonce = $1
|
|
||||||
print_status "Server nonce found, attempting to log in..."
|
|
||||||
else
|
|
||||||
print_error "Server nonce not present, potentially not an IP Board install or bad URI."
|
|
||||||
print_error "Skipping #{vhost}.."
|
|
||||||
return :abort
|
|
||||||
end
|
|
||||||
|
|
||||||
# With the server nonce found, try to log into IP Board with the user provided creds
|
|
||||||
res2 = send_request_cgi({
|
|
||||||
'uri' => normalize_uri(target_uri.path, "index.php?app=core&module=global§ion=login&do=process"),
|
|
||||||
'method' => 'POST',
|
|
||||||
'vars_post' => {
|
|
||||||
'auth_key' => server_nonce,
|
|
||||||
'ips_username' => user,
|
|
||||||
'ips_password' => pass
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Default value of no creds found
|
|
||||||
valid_creds = false
|
|
||||||
|
|
||||||
# Iterate over header response. If the server is setting the ipsconnect and coppa cookie
|
|
||||||
# then we were able to log in successfully. If they are not set, invalid credentials were
|
|
||||||
# provided.
|
|
||||||
|
|
||||||
if res2.get_cookies.include?('ipsconnect') && res2.get_cookies.include?('coppa')
|
|
||||||
valid_creds = true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inform the user if the user supplied credentials were valid or not
|
|
||||||
if valid_creds
|
|
||||||
print_good "Username: #{user} and Password: #{pass} are valid credentials!"
|
|
||||||
register_creds(user, pass, ip)
|
|
||||||
return :next_user
|
|
||||||
else
|
|
||||||
vprint_error "Username: #{user} and Password: #{pass} are invalid credentials!"
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue ::Timeout::Error
|
|
||||||
print_error "Connection timed out while attempting to reach #{vhost}!"
|
|
||||||
return :connection_error
|
|
||||||
|
|
||||||
rescue ::Errno::EPIPE
|
|
||||||
print_error "Broken pipe error when connecting to #{vhost}!"
|
|
||||||
return :connection_error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def register_creds(username, password, ipaddr)
|
|
||||||
# Build service information
|
|
||||||
service_data = {
|
|
||||||
address: ipaddr,
|
|
||||||
port: datastore['RPORT'],
|
|
||||||
service_name: 'http',
|
|
||||||
protocol: 'tcp',
|
|
||||||
workspace_id: myworkspace_id
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build credential information
|
|
||||||
credential_data = {
|
|
||||||
origin_type: :service,
|
|
||||||
module_fullname: self.fullname,
|
|
||||||
private_data: password,
|
|
||||||
private_type: :password,
|
|
||||||
username: username,
|
|
||||||
workspace_id: myworkspace_id
|
|
||||||
}
|
|
||||||
|
|
||||||
credential_data.merge!(service_data)
|
|
||||||
credential_core = create_credential(credential_data)
|
|
||||||
|
|
||||||
# Assemble the options hash for creating the Metasploit::Credential::Login object
|
|
||||||
login_data = {
|
|
||||||
access_level: "user",
|
|
||||||
core: credential_core,
|
|
||||||
last_attempted_at: DateTime.now,
|
|
||||||
status: Metasploit::Model::Login::Status::SUCCESSFUL,
|
|
||||||
workspace_id: myworkspace_id
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data.merge!(service_data)
|
|
||||||
create_credential_login(login_data)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'metasploit/framework/login_scanner/ipboard'
|
||||||
|
|
||||||
|
describe Metasploit::Framework::LoginScanner::IPBoard do
|
||||||
|
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
|
||||||
|
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
|
||||||
|
it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP'
|
||||||
|
|
||||||
|
context "#attempt_login" do
|
||||||
|
|
||||||
|
let(:username) { 'admin' }
|
||||||
|
let(:password) { 'password' }
|
||||||
|
let(:server_nonce) { 'nonce' }
|
||||||
|
|
||||||
|
let(:creds) do
|
||||||
|
Metasploit::Framework::Credential.new(
|
||||||
|
paired: true,
|
||||||
|
public: username,
|
||||||
|
private: password
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:invalid_creds) do
|
||||||
|
Metasploit::Framework::Credential.new(
|
||||||
|
paired: true,
|
||||||
|
public: 'username',
|
||||||
|
private: 'novalid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when Rex::Proto::Http::Client#connect raises Rex::ConnectionError" do
|
||||||
|
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Rex::ConnectionError)
|
||||||
|
expect(subject.attempt_login(creds).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when Rex::Proto::Http::Client#connect raises Timeout::Error" do
|
||||||
|
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Timeout::Error)
|
||||||
|
expect(subject.attempt_login(creds).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when Rex::Proto::Http::Client#connect raises EOFError" do
|
||||||
|
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(EOFError)
|
||||||
|
expect(subject.attempt_login(creds).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when invalid IPBoard application" do
|
||||||
|
let(:not_found_warning) { 'Server nonce not present, potentially not an IP Board install or bad URI.' }
|
||||||
|
before :each do
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv) do |cli, req|
|
||||||
|
Rex::Proto::Http::Response.new(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do
|
||||||
|
expect(subject.attempt_login(creds).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns proof warning about nonce not found' do
|
||||||
|
expect(subject.attempt_login(creds).proof).to eq(not_found_warning)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when valid IPBoard application" do
|
||||||
|
before :each do
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv) do |cli, req|
|
||||||
|
|
||||||
|
if req.opts['uri'] && req.opts['uri'].include?('index.php') &&
|
||||||
|
req.opts['vars_get'] &&
|
||||||
|
req.opts['vars_get']['app'] &&
|
||||||
|
req.opts['vars_get']['app'] == 'core' &&
|
||||||
|
req.opts['vars_get']['module'] &&
|
||||||
|
req.opts['vars_get']['module'] == 'global' &&
|
||||||
|
req.opts['vars_get']['section'] &&
|
||||||
|
req.opts['vars_get']['section'] == 'login' &&
|
||||||
|
req.opts['vars_get']['do'] &&
|
||||||
|
req.opts['vars_get']['do'] == 'process' &&
|
||||||
|
req.opts['vars_post'] &&
|
||||||
|
req.opts['vars_post']['auth_key'] &&
|
||||||
|
req.opts['vars_post']['auth_key'] == server_nonce &&
|
||||||
|
req.opts['vars_post']['ips_username'] &&
|
||||||
|
req.opts['vars_post']['ips_username'] == username &&
|
||||||
|
req.opts['vars_post']['ips_password'] &&
|
||||||
|
req.opts['vars_post']['ips_password'] == password
|
||||||
|
res = Rex::Proto::Http::Response.new(200)
|
||||||
|
res.headers['Set-Cookie'] = 'ipsconnect=ipsconnect_value;Path=/;,coppa=coppa_value;Path=/;'
|
||||||
|
elsif req.opts['uri'] && req.opts['uri'].include?('index.php') && req.opts['method'] == 'POST'
|
||||||
|
res = Rex::Proto::Http::Response.new(404)
|
||||||
|
else
|
||||||
|
res = Rex::Proto::Http::Response.new(200)
|
||||||
|
res.body = "name='auth_key' value='#{server_nonce}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when valid login" do
|
||||||
|
it 'returns status Metasploit::Model::Login::Status::SUCCESSFUL' do
|
||||||
|
expect(subject.attempt_login(creds).status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when invalid login" do
|
||||||
|
it 'returns status Metasploit::Model::Login::Status::INCORRECT' do
|
||||||
|
expect(subject.attempt_login(invalid_creds).status).to eq(Metasploit::Model::Login::Status::INCORRECT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in New Issue