Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support checks in relay modules #19639

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ In the following example the AUTO mode is used to issue a certificate for the MS
authenticated.

```msf
msf6 auxiliary(server/relay/esc8) > set RELAY_TARGETS 172.30.239.85
msf6 auxiliary(server/relay/esc8) > set RHOSTS 172.30.239.85
msf6 auxiliary(server/relay/esc8) > run
[*] Auxiliary module running as background job 1.
msf6 auxiliary(server/relay/esc8) >
Expand Down
29 changes: 29 additions & 0 deletions lib/msf/core/auxiliary/multiple_target_hosts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: binary -*-

module Msf

###
#
# This module provides methods for modules which intend to handle multiple hosts
# themselves through some means, e.g. scanners. This circumvents the typical
# RHOSTS -> RHOST logic offered by the framework.
#
###

module Auxiliary::MultipleTargetHosts

def has_check?
respond_to?(:check_host)
end

def check
nmod = replicant
begin
nmod.check_host(datastore['RHOST'])
rescue NoMethodError
Exploit::CheckCode::Unsupported
end
end

end
end
16 changes: 2 additions & 14 deletions lib/msf/core/auxiliary/scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Msf

module Auxiliary::Scanner

include Msf::Auxiliary::MultipleTargetHosts

class AttemptFailed < Msf::Auxiliary::Failed
end

Expand All @@ -31,20 +33,6 @@ def initialize(info = {})

end

def has_check?
respond_to?(:check_host)
end

def check
nmod = replicant
begin
nmod.check_host(datastore['RHOST'])
rescue NoMethodError
Exploit::CheckCode::Unsupported
end
end


def peer
# IPv4 addr can be 16 chars + 1 for : and + 5 for port
super.ljust(21)
Expand Down
3 changes: 2 additions & 1 deletion lib/msf/core/exploit/remote/smb/relay_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Msf
module Exploit::Remote::SMB
# This mixin provides a minimal SMB server
module RelayServer
include ::Msf::Auxiliary::MultipleTargetHosts
include ::Msf::Exploit::Remote::SocketServer
include ::Msf::Exploit::Remote::SMB::Server::HashCapture

Expand All @@ -15,7 +16,7 @@ def initialize(info = {})
OptPort.new('SRVPORT', [true, 'The local port to listen on.', 445]),
OptString.new('SMBDomain', [true, 'The domain name used during SMB exchange.', 'WORKGROUP'], aliases: ['DOMAIN_NAME']),
OptInt.new('SRV_TIMEOUT', [true, 'Seconds that the server socket will wait for a response after the client has initiated communication.', 25]),
OptAddressRange.new('RELAY_TARGETS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['SMBHOST']),
OptAddressRange.new('RHOSTS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['SMBHOST', 'RELAY_TARGETS']),
OptInt.new('RELAY_TIMEOUT', [true, 'Seconds that the relay socket will wait for a response after the client has initiated communication.', 25])
], self.class)
end
Expand Down
21 changes: 16 additions & 5 deletions lib/msf/core/exploit/remote/tcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,22 @@ def print_prefix
# Otherwise we are logging in the global context where rhost can be any
# size (being an alias for rhosts), which is not very useful to insert into
# a single log line.
if rhost && rhost.split(' ').length == 1
Copy link
Contributor Author

@zeroSteiner zeroSteiner Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check for a space does not account for all of the different ways RangeWalker can handle multiple hosts. Instead, we should actually use RangeWalker to count how many hosts were targeted and only report it if there's a single target. Because this calculation is too expensive to run each time a message is printed, we cache the value.

The comment implies that this was always the intention.

super + peer + ' - '
else
super
unless instance_variable_defined?(:@print_prefix)
if rhost.present? && Rex::Socket::RangeWalker.new(rhost).length == 1
@print_prefix = peer + ' - '
else
@print_prefix = ''
end
end

super + @print_prefix
end

def replicant
obj = super
# invalidate the cached print_prefix in case the target changes
obj.remove_instance_variable(:@print_prefix) if instance_variable_defined?(:@print_prefix)
obj
end

##
Expand Down Expand Up @@ -259,7 +270,7 @@ def lport

# Returns the rhost:rport
def peer
"#{rhost}:#{rport}"
Rex::Socket.to_authority(rhost, rport)
end

#
Expand Down
10 changes: 9 additions & 1 deletion lib/msf/core/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,17 @@ def set_via(opts)
# exploit instance. Store references from and to the exploit module.
#
def set_from_exploit(m)
target_host = nil
unless m.target_host.blank?
# only propagate the target_host value if it's exactly 1 host
if (rw = Rex::Socket::RangeWalker.new(m.target_host)).length == 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because RHOSTS wasn't being split any more, RHOST was still a range. This caused Rex::Socket.getaddress to raise a resolution error. Instead we use RangeWalker again to count how many hosts were being target and only propagate the address if it's exactly one. This also fixes an issues in the previous implementation where if a hostname resolved to multiple addresses, the wrong one may have been selected and copied to #target_host. This avoids that by only copying when we're confident that the target is correct because it's a single value.

target_host = rw.next_ip
end
end

self.via = { 'Exploit' => m.fullname }
self.via['Payload'] = ('payload/' + m.datastore['PAYLOAD'].to_s) if m.datastore['PAYLOAD']
self.target_host = Rex::Socket.getaddress(m.target_host) if (m.target_host.to_s.strip.length > 0)
self.target_host = target_host
self.target_port = m.target_port if (m.target_port.to_i != 0)
self.workspace = m.workspace
self.username = m.owner
Expand Down
2 changes: 1 addition & 1 deletion lib/msf/ui/console/command_dispatcher/auxiliary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def cmd_run(*args, action: nil, opts: {})

begin
# Check if this is a scanner module or doesn't target remote hosts
if rhosts.blank? || mod.class.included_modules.include?(Msf::Auxiliary::Scanner)
if rhosts.blank? || mod.class.included_modules.include?(Msf::Auxiliary::MultipleTargetHosts)
mod_with_opts.run_simple(
'Action' => args[:action],
'LocalInput' => driver.input,
Expand Down
2 changes: 1 addition & 1 deletion lib/msf/ui/console/command_dispatcher/exploit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def cmd_exploit(*args, opts: {})

driver.run_single('reload_lib -a') if args[:reload_libs]

if rhosts && has_rhosts_option
if rhosts && has_rhosts_option && !mod.class.included_modules.include?(Msf::Auxiliary::MultipleTargetHosts)
rhosts_walker = Msf::RhostsWalker.new(rhosts, mod_with_opts.datastore)
rhosts_walker_count = rhosts_walker.count
rhosts_walker = rhosts_walker.to_enum
Expand Down
10 changes: 9 additions & 1 deletion lib/rex/proto/smb/simple_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,15 @@ def peerport
end

def peerinfo
"#{peerhost}:#{peerport}"
Rex::Socket.to_authority(peerhost, peerport)
end

def signing_required
if client.is_a?(Rex::Proto::SMB::Client)
client.peer_require_signing
else
client.signing_required
end
end

private
Expand Down
36 changes: 23 additions & 13 deletions modules/auxiliary/server/relay/esc8.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class MetasploitModule < Msf::Auxiliary
include ::Msf::Exploit::Remote::SMB::RelayServer
include ::Msf::Exploit::Remote::HttpClient

def initialize
def initialize(_info = {})
super({
'Name' => 'ESC8 Relay: SMB to HTTP(S)',
'Description' => %q{
Expand Down Expand Up @@ -40,8 +40,6 @@ def initialize
OptBool.new('RANDOMIZE_TARGETS', [true, 'Whether the relay targets should be randomized', true]),
]
)

deregister_options('RHOSTS')
end

def relay_targets
Expand All @@ -54,7 +52,7 @@ def relay_targets
)
end

def initial_handshake?(target_ip)
def check_host(target_ip)
res = send_request_raw(
{
'rhost' => target_ip,
Expand All @@ -67,18 +65,30 @@ def initial_handshake?(target_ip)
)
disconnect

res&.code == 401
end
return Exploit::CheckCode::Unknown if res.nil?
unless res.code == 401
return Exploit::CheckCode::Safe('The target does not require authentication.')
end

unless res.headers['WWW-Authenticate'].include?('NTLM') && res.body.present?
return Exploit::CheckCode::Safe('The target does not support NTLM.')
end

def check_options
if datastore['RHOSTS'].present?
print_warning('Warning: RHOSTS datastore value has been set which is not supported by this module. Please verify RELAY_TARGETS is set correctly.')
if datastore['SSL']
# if the target is over SSL, downgrade to "Detected" because Extended Protection for Authentication may or may not be enabled
Exploit::CheckCode::Detected('Server replied that authentication is required and NTLM is supported.')
else
Exploit::CheckCode::Appears('Server replied that authentication is required and NTLM is supported.')
end
end

def validate
super

case datastore['MODE']
when 'SPECIFIC_TEMPLATE'
if datastore['CERT_TEMPLATE'].nil? || datastore['CERT_TEMPLATE'].blank?
fail_with(Failure::BadConfig, 'CERT_TEMPLATE must be set in AUTO and SPECIFIC_TEMPLATE mode')
if datastore['CERT_TEMPLATE'].blank?
raise Msf::OptionValidateError.new({ 'CERT_TEMPLATE' => 'CERT_TEMPLATE must be set when MODE is SPECIFIC_TEMPLATE' })
end
when 'ALL', 'AUTO', 'QUERY_ONLY'
unless datastore['CERT_TEMPLATE'].nil? || datastore['CERT_TEMPLATE'].blank?
Expand All @@ -88,11 +98,11 @@ def check_options
end

def run
check_options
@issued_certs = {}
relay_targets.each do |target|
vprint_status("Checking endpoint on #{target}")
unless initial_handshake?(target.ip)
check_code = check_host(target.ip)
if [Exploit::CheckCode::Unknown, Exploit::CheckCode::Safe].include?(check_code)
fail_with(Failure::UnexpectedReply, "Web Enrollment does not appear to be enabled on #{target}")
end
end
Expand Down
36 changes: 27 additions & 9 deletions modules/exploits/windows/smb/smb_relay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ module is not able to clean up after itself. The service and payload
)

deregister_options(
'RPORT', 'RHOSTS', 'SMBPass', 'SMBUser', 'CommandShellCleanupCommand', 'AutoVerifySession'
'RPORT', 'SMBPass', 'SMBUser', 'CommandShellCleanupCommand', 'AutoVerifySession'
)
if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
add_info('New in Metasploit 6.4 - The %grnCREATE_SMB_SESSION%clr action within this module can open an interactive session')
Expand Down Expand Up @@ -163,11 +163,7 @@ def validate_service_stub_encoder!
end
end

def exploit
if datastore['RHOSTS'].present?
print_warning('Warning: RHOSTS datastore value has been set which is not supported by this module. Please verify RELAY_TARGETS is set correctly.')
end

def validate
case action.name
when 'PSEXEC'
validate_service_stub_encoder!
Expand Down Expand Up @@ -205,7 +201,7 @@ def run_psexec(relay_connection)
framework.threads.spawn(thread_name, false, new_mod_instance) do |mod_instance|
mod_instance.exploit_smb_target
rescue StandardError => e
print_error("Failed running psexec against target #{datastore['RHOST']} - #{e.class} #{e.message}")
print_error("Failed running psexec against target #{relay_connection.target.ip} - #{e.class} #{e.message}")
elog(e)
# ensure
# # Note: Don't cleanup explicitly, as the shared replicant state leads to payload handlers etc getting closed.
Expand All @@ -217,12 +213,31 @@ def run_psexec(relay_connection)
def relay_targets
Msf::Exploit::Remote::SMB::Relay::TargetList.new(
:smb,
445,
datastore['RELAY_TARGETS'],
rport,
datastore['RHOSTS'],
randomize_targets: datastore['RANDOMIZE_TARGETS']
)
end

def check_host(target_ip)
generic_message = 'Failed to connect and negotiate an SMB connection.'
begin
simple = connect(false, direct: true)
protocol = simple.client.negotiate
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError, Errno::ECONNRESET
return Exploit::CheckCode::Unknown(generic_message)
rescue ::Exception => e # rubocop:disable Lint/RescueException
elog(generic_message, error: e)
return Exploit::CheckCode::Unknown(generic_message)
end

if simple.signing_required
return Exploit::CheckCode::Safe('Signing is required by the target server.')
end

Exploit::CheckCode::Vulnerable('Signing is not required by the target server.')
end

# Called after a successful connection to a relayed host is opened
def exploit_smb_target
# automatically select an SMB share unless one is explicitly specified
Expand Down Expand Up @@ -287,4 +302,7 @@ def session_setup(client)
s
end

def rport
445
end
end
Loading