Land #4945, @wchen-r7's login scanner for Symantec web gateway
commit
1226b3656f
|
@ -0,0 +1,149 @@
|
||||||
|
|
||||||
|
require 'metasploit/framework/login_scanner/http'
|
||||||
|
|
||||||
|
module Metasploit
|
||||||
|
module Framework
|
||||||
|
module LoginScanner
|
||||||
|
|
||||||
|
class SymantecWebGateway < HTTP
|
||||||
|
|
||||||
|
DEFAULT_PORT = 443
|
||||||
|
PRIVATE_TYPES = [ :password ]
|
||||||
|
LOGIN_STATUS = Metasploit::Model::Login::Status # Shorter name
|
||||||
|
|
||||||
|
|
||||||
|
# Checks if the target is Symantec Web Gateway. The login module should call this.
|
||||||
|
#
|
||||||
|
# @return [Boolean] TrueClass if target is SWG, otherwise FalseClass
|
||||||
|
def check_setup
|
||||||
|
login_uri = normalize_uri("#{uri}/spywall/login.php")
|
||||||
|
res = send_request({'uri'=> login_uri})
|
||||||
|
|
||||||
|
if res && res.body.include?('Symantec Web Gateway')
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Sends a HTTP request with Rex
|
||||||
|
#
|
||||||
|
# @param (see Rex::Proto::Http::Request#request_raw)
|
||||||
|
# @raise [Rex::ConnectionError] Something has gone wrong while sending the HTTP request
|
||||||
|
# @return [Rex::Proto::Http::Response] The HTTP response
|
||||||
|
def send_request(opts)
|
||||||
|
res = nil
|
||||||
|
cli = Rex::Proto::Http::Client.new(host, port,
|
||||||
|
{
|
||||||
|
'Msf' => framework,
|
||||||
|
'MsfExploit' => framework_module
|
||||||
|
},
|
||||||
|
ssl,
|
||||||
|
ssl_version,
|
||||||
|
proxies
|
||||||
|
)
|
||||||
|
configure_http_client(cli)
|
||||||
|
begin
|
||||||
|
cli.connect
|
||||||
|
req = cli.request_cgi(opts)
|
||||||
|
res = cli.send_recv(req)
|
||||||
|
rescue ::Errno::EPIPE, ::Timeout::Error => e
|
||||||
|
# We are trying to mimic the same type of exception rescuing in
|
||||||
|
# Msf::Exploit::Remote::HttpClient. But instead of returning nil, we'll consistently
|
||||||
|
# raise Rex::ConnectionError so the #attempt_login can return the error message back
|
||||||
|
# to the login module.
|
||||||
|
raise Rex::ConnectionError, e.message
|
||||||
|
ensure
|
||||||
|
cli.close
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the latest sid from Symantec Web Gateway.
|
||||||
|
#
|
||||||
|
# @returns [String] The PHP Session ID for Symantec Web Gateway login
|
||||||
|
def get_last_sid
|
||||||
|
@last_sid ||= lambda {
|
||||||
|
# We don't have a session ID. Well, let's grab one right quick from the login page.
|
||||||
|
# This should probably only happen once (initially).
|
||||||
|
login_uri = normalize_uri("#{uri}/spywall/login.php")
|
||||||
|
res = send_request({'uri' => login_uri})
|
||||||
|
|
||||||
|
return '' unless res
|
||||||
|
|
||||||
|
cookies = res.get_cookies
|
||||||
|
@last_sid = cookies.scan(/(PHPSESSID=\w+);*/).flatten[0] || ''
|
||||||
|
}.call
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Actually doing the login. Called by #attempt_login
|
||||||
|
#
|
||||||
|
# @param username [String] The username to try
|
||||||
|
# @param password [String] The password to try
|
||||||
|
# @return [Hash]
|
||||||
|
# * :status [Metasploit::Model::Login::Status]
|
||||||
|
# * :proof [String] the HTTP response body
|
||||||
|
def get_login_state(username, password)
|
||||||
|
# Prep the data needed for login
|
||||||
|
sid = get_last_sid
|
||||||
|
protocol = ssl ? 'https' : 'http'
|
||||||
|
peer = "#{host}:#{port}"
|
||||||
|
login_uri = normalize_uri("#{uri}/spywall/login.php")
|
||||||
|
|
||||||
|
res = send_request({
|
||||||
|
'uri' => login_uri,
|
||||||
|
'method' => 'POST',
|
||||||
|
'cookie' => sid,
|
||||||
|
'headers' => {
|
||||||
|
'Referer' => "#{protocol}://#{peer}/#{login_uri}"
|
||||||
|
},
|
||||||
|
'vars_post' => {
|
||||||
|
'USERNAME' => username,
|
||||||
|
'PASSWORD' => password,
|
||||||
|
'loginBtn' => 'Login' # Found in the HTML form
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
unless res
|
||||||
|
return {:status => LOGIN_STATUS::UNABLE_TO_CONNECT, :proof => res.to_s}
|
||||||
|
end
|
||||||
|
|
||||||
|
# After login, the application should give us a new SID
|
||||||
|
cookies = res.get_cookies
|
||||||
|
sid = cookies.scan(/(PHPSESSID=\w+);*/).flatten[0] || ''
|
||||||
|
@last_sid = sid # Update our SID
|
||||||
|
|
||||||
|
if res.headers['Location'].to_s.include?('executive_summary.php') && !sid.blank?
|
||||||
|
return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.to_s}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:status => LOGIN_STATUS::INCORRECT, :proof => res.to_s}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Attempts to login to Symantec Web Gateway. This is called first.
|
||||||
|
#
|
||||||
|
# @param credential [Metasploit::Framework::Credential] The credential object
|
||||||
|
# @return [Result] A Result object indicating success or failure
|
||||||
|
def attempt_login(credential)
|
||||||
|
result_opts = { credential: credential }
|
||||||
|
|
||||||
|
begin
|
||||||
|
result_opts.merge!(get_login_state(credential.public, credential.private))
|
||||||
|
rescue ::Rex::ConnectionError => e
|
||||||
|
# Something went wrong during login. 'e' knows what's up.
|
||||||
|
result_opts.merge!(status: LOGIN_STATUS::UNABLE_TO_CONNECT, proof: e.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
Result.new(result_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -540,6 +540,13 @@ module Auxiliary::AuthBrute
|
||||||
::IO.select(nil,nil,nil,sleep_time) unless sleep_time == 0
|
::IO.select(nil,nil,nil,sleep_time) unless sleep_time == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# See #print_brute
|
||||||
|
def vprint_brute(opts={})
|
||||||
|
if datastore['VERBOSE']
|
||||||
|
print_brute(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Provides a consistant way to display messages about AuthBrute-mixed modules.
|
# Provides a consistant way to display messages about AuthBrute-mixed modules.
|
||||||
# Acceptable opts are fairly self-explanitory, but :level can be tricky.
|
# Acceptable opts are fairly self-explanitory, but :level can be tricky.
|
||||||
#
|
#
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
##
|
||||||
|
# This module requires Metasploit: http://metasploit.com/download
|
||||||
|
# Current source: https://github.com/rapid7/metasploit-framework
|
||||||
|
##
|
||||||
|
|
||||||
|
require 'msf/core'
|
||||||
|
require 'metasploit/framework/login_scanner/symantec_web_gateway'
|
||||||
|
require 'metasploit/framework/credential_collection'
|
||||||
|
|
||||||
|
class Metasploit3 < Msf::Auxiliary
|
||||||
|
|
||||||
|
include Msf::Exploit::Remote::HttpClient
|
||||||
|
include Msf::Auxiliary::AuthBrute
|
||||||
|
include Msf::Auxiliary::Report
|
||||||
|
include Msf::Auxiliary::Scanner
|
||||||
|
|
||||||
|
def initialize(info={})
|
||||||
|
super(update_info(info,
|
||||||
|
'Name' => 'Symantec Web Gateway Login Utility',
|
||||||
|
'Description' => %q{
|
||||||
|
This module will attempt to authenticate to a Symantec Web Gateway
|
||||||
|
},
|
||||||
|
'Author' => [ 'sinn3r' ],
|
||||||
|
'License' => MSF_LICENSE,
|
||||||
|
'DefaultOptions' =>
|
||||||
|
{
|
||||||
|
'RPORT' => 443,
|
||||||
|
'SSL' => true,
|
||||||
|
'SSLVersion' => 'TLS1'
|
||||||
|
}
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Initializes CredentialCollection and SymantecWebGateway
|
||||||
|
def init(ip)
|
||||||
|
@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']
|
||||||
|
)
|
||||||
|
|
||||||
|
@scanner = Metasploit::Framework::LoginScanner::SymantecWebGateway.new(
|
||||||
|
configure_http_login_scanner(
|
||||||
|
host: ip,
|
||||||
|
port: datastore['RPORT'],
|
||||||
|
cred_details: @cred_collection,
|
||||||
|
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||||
|
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
|
||||||
|
connection_timeout: 5
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Reports a good login credential
|
||||||
|
def do_report(ip, port, result)
|
||||||
|
service_data = {
|
||||||
|
address: ip,
|
||||||
|
port: port,
|
||||||
|
service_name: 'http',
|
||||||
|
protocol: 'tcp',
|
||||||
|
workspace_id: myworkspace_id
|
||||||
|
}
|
||||||
|
|
||||||
|
credential_data = {
|
||||||
|
module_fullname: self.fullname,
|
||||||
|
origin_type: :service,
|
||||||
|
private_data: result.credential.private,
|
||||||
|
private_type: :password,
|
||||||
|
username: result.credential.public,
|
||||||
|
}.merge(service_data)
|
||||||
|
|
||||||
|
login_data = {
|
||||||
|
core: create_credential(credential_data),
|
||||||
|
last_attempted_at: DateTime.now,
|
||||||
|
status: result.status,
|
||||||
|
proof: result.proof
|
||||||
|
}.merge(service_data)
|
||||||
|
|
||||||
|
create_credential_login(login_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Attempts to login
|
||||||
|
def bruteforce(ip)
|
||||||
|
@scanner.scan! do |result|
|
||||||
|
case result.status
|
||||||
|
when Metasploit::Model::Login::Status::SUCCESSFUL
|
||||||
|
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
|
||||||
|
do_report(ip, rport, result)
|
||||||
|
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||||
|
vprint_brute :level => :verror, :ip => ip, :msg => result.proof
|
||||||
|
invalidate_login(
|
||||||
|
address: ip,
|
||||||
|
port: rport,
|
||||||
|
protocol: 'tcp',
|
||||||
|
public: result.credential.public,
|
||||||
|
private: result.credential.private,
|
||||||
|
realm_key: result.credential.realm_key,
|
||||||
|
realm_value: result.credential.realm,
|
||||||
|
status: result.status,
|
||||||
|
proof: result.proof
|
||||||
|
)
|
||||||
|
when Metasploit::Model::Login::Status::INCORRECT
|
||||||
|
vprint_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
|
||||||
|
invalidate_login(
|
||||||
|
address: ip,
|
||||||
|
port: rport,
|
||||||
|
protocol: 'tcp',
|
||||||
|
public: result.credential.public,
|
||||||
|
private: result.credential.private,
|
||||||
|
realm_key: result.credential.realm_key,
|
||||||
|
realm_value: result.credential.realm,
|
||||||
|
status: result.status,
|
||||||
|
proof: result.proof
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Start here
|
||||||
|
def run_host(ip)
|
||||||
|
init(ip)
|
||||||
|
unless @scanner.check_setup
|
||||||
|
print_brute :level => :error, :ip => ip, :msg => 'Target is not Symantec Web Gateway'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
bruteforce(ip)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,146 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'metasploit/framework/login_scanner/symantec_web_gateway'
|
||||||
|
|
||||||
|
describe Metasploit::Framework::LoginScanner::SymantecWebGateway do
|
||||||
|
|
||||||
|
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
|
||||||
|
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
|
||||||
|
|
||||||
|
let(:session_id) do
|
||||||
|
'PHPSESSID=FAKESESSIONID;'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:username) do
|
||||||
|
'username'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:good_password) do
|
||||||
|
'good_password'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:bad_password) do
|
||||||
|
'bad_password'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:successful_auth_response) do
|
||||||
|
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||||
|
res.headers['Location'] = 'executive_summary.php'
|
||||||
|
res.headers['Set-Cookie'] = 'PHPSESSID=NEWSESSIONID;'
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:fail_auth_response) do
|
||||||
|
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||||
|
res.headers['Set-Cookie'] = 'PHPSESSID=NEWSESSIONID;'
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
described_class.new
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:response) do
|
||||||
|
Rex::Proto::Http::Response.new(200, 'OK')
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:request_cgi).with(any_args)
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv).with(any_args).and_return(response)
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:set_config).with(any_args)
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:close)
|
||||||
|
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#check_setup' do
|
||||||
|
let(:swg_html_response) do
|
||||||
|
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||||
|
res.body = 'Symantec Web Gateway'
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when target is Symantec Web Gateway' do
|
||||||
|
let(:response) { swg_html_response }
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.check_setup).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when target is not Symantec Web Gateway' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.check_setup).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#send_request' do
|
||||||
|
context 'when a valid request is sent' do
|
||||||
|
it 'returns a response object' do
|
||||||
|
expect(subject.send_request({'uri'=>'/'})).to be_kind_of(Rex::Proto::Http::Response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_last_sid' do
|
||||||
|
let(:response) do
|
||||||
|
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||||
|
res.headers['Set-Cookie'] = session_id
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is no session ID' do
|
||||||
|
it 'returns a new session ID' do
|
||||||
|
expect(subject.get_last_sid).to include('PHPSESSID')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is already a session ID' do
|
||||||
|
it 'returns the current session ID' do
|
||||||
|
# Prepend like there's already one
|
||||||
|
subject.instance_variable_set(:@last_sid, 'PHPSESSID=PRESETSID;')
|
||||||
|
expect(subject.get_last_sid).to include('PRESETSID')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_login_state' do
|
||||||
|
context 'when the credential is valid' do
|
||||||
|
let(:response) { successful_auth_response }
|
||||||
|
it 'returns a hash indicating a successful login' do
|
||||||
|
successful_status = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||||
|
expect(subject.get_login_state(username, good_password)[:status]).to eq(successful_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the creential is invalid' do
|
||||||
|
let(:response) { fail_auth_response }
|
||||||
|
it 'returns a hash indicating an incorrect cred' do
|
||||||
|
incorrect_status = Metasploit::Model::Login::Status::INCORRECT
|
||||||
|
expect(subject.get_login_state(username, good_password)[:status]).to eq(incorrect_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#attempt_login' do
|
||||||
|
context 'when the credential is valid' do
|
||||||
|
let(:response) { successful_auth_response }
|
||||||
|
|
||||||
|
it 'returns a Result object indicating a successful login' do
|
||||||
|
cred_obj = Metasploit::Framework::Credential.new(public: username, private: good_password)
|
||||||
|
result = subject.attempt_login(cred_obj)
|
||||||
|
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
|
||||||
|
expect(result.status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the credential is invalid' do
|
||||||
|
let(:response) { fail_auth_response }
|
||||||
|
it 'returns a Result object indicating an incorrect cred' do
|
||||||
|
cred_obj = Metasploit::Framework::Credential.new(public: username, private: bad_password)
|
||||||
|
result = subject.attempt_login(cred_obj)
|
||||||
|
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
|
||||||
|
expect(result.status).to eq(Metasploit::Model::Login::Status::INCORRECT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue