Land #3699, @dmaloney-r7's ipboard login refactor

bug/bundler_fix
jvazquez-r7 2014-09-15 08:29:42 -05:00
commit 7d4c4c3658
No known key found for this signature in database
GPG Key ID: 38D99152B9352D83
7 changed files with 296 additions and 106 deletions

View File

@ -20,6 +20,8 @@ module Metasploit
host, port, {}, ssl, ssl_version
)
http_client = config_client(http_client)
result_opts = {
credential: credential,
host: host,

View File

@ -29,6 +29,15 @@ module Metasploit
# @return [String] HTTP method, e.g. "GET", "POST"
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 :method,
@ -82,6 +91,9 @@ module Metasploit
host, port, {}, ssl, ssl_version,
nil, credential.public, credential.private
)
http_client = config_client(http_client)
if credential.realm
http_client.set_config('domain' => credential.realm)
end
@ -108,6 +120,14 @@ module Metasploit
private
def config_client(client)
client.set_config(
'vhost' => vhost || host,
'agent' => user_agent
)
client
end
# This method sets the sane defaults for things
# like timeouts and TCP evasion options
def set_sane_defaults

View File

@ -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

View File

@ -82,6 +82,8 @@ class Metasploit3 < Msf::Auxiliary
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|

View File

@ -140,6 +140,8 @@ class Metasploit3 < Msf::Auxiliary
cred_details: cred_collection,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: 5,
user_agent: datastore['UserAgent'],
vhost: datastore['VHOST']
)
msg = scanner.check_setup

View File

@ -1,5 +1,6 @@
require 'msf/core'
require 'metasploit/framework/login_scanner/ipboard'
class Metasploit3 < Msf::Auxiliary
@ -25,115 +26,51 @@ class Metasploit3 < Msf::Auxiliary
end
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|
do_login(user, pass, ip)
scanner = Metasploit::Framework::LoginScanner::IPBoard.new(
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
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&section=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

View File

@ -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