242 lines
7.6 KiB
Ruby
242 lines
7.6 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
module Msf
|
|
module Exploit::Remote::SMB
|
|
# This mixin provides a minimal SMB server
|
|
module Server
|
|
include Msf::Exploit::Remote::TcpServer
|
|
include Msf::Exploit::NTLM
|
|
CONST = ::Rex::Proto::SMB::Constants
|
|
CRYPT = ::Rex::Proto::SMB::Crypt
|
|
UTILS = ::Rex::Proto::SMB::Utils
|
|
XCEPT = ::Rex::Proto::SMB::Exceptions
|
|
EVADE = ::Rex::Proto::SMB::Evasions
|
|
|
|
def initialize(info = {})
|
|
super
|
|
|
|
deregister_options('SSL', 'SSLCert')
|
|
register_options(
|
|
[
|
|
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 445 ])
|
|
], self.class)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptInt.new('SMBServerMaximumBuffer', [ true, 'The maximum number of data in megabytes to buffer', 2 ]),
|
|
OptInt.new('SMBServerIdleTimeout', [ true, 'The maximum amount of time to keep an idle session open in seconds', 120 ])
|
|
], self.class)
|
|
|
|
@smb_server_last_pool_sweep = Time.now.to_f
|
|
@smb_server_pool_mutex = Mutex.new
|
|
@smb_server_request_counter = 0
|
|
end
|
|
|
|
def setup
|
|
super
|
|
@state = {}
|
|
end
|
|
|
|
def on_client_connect(client)
|
|
# print_status("New SMB connection from #{client.peerhost}:#{client.peerport}")
|
|
smb_conn(client)
|
|
end
|
|
|
|
def on_client_data(client)
|
|
# print_status("New data from #{client.peerhost}:#{client.peerport}")
|
|
smb_recv(client)
|
|
true
|
|
end
|
|
|
|
def on_client_close(client)
|
|
smb_stop(client)
|
|
end
|
|
|
|
def smb_conn(c)
|
|
@state[c] = {:name => "#{c.peerhost}:#{c.peerport}", :ip => c.peerhost, :port => c.peerport}
|
|
smb_pool_update(c)
|
|
end
|
|
|
|
def smb_stop(c)
|
|
# Make sure the socket is closed
|
|
begin
|
|
c.close
|
|
# Handle any number of errors that a double-close or failed shutdown can trigger
|
|
rescue ::IOError, ::EOFError,
|
|
::Errno::ECONNRESET, ::Errno::ENOTCONN, ::Errno::ECONNABORTED,
|
|
::Errno::ETIMEDOUT, ::Errno::ENETRESET, ::Errno::ESHUTDOWN
|
|
end
|
|
|
|
# Delete the state table entry
|
|
@state.delete(c)
|
|
end
|
|
|
|
def smb_recv(c)
|
|
smb = @state[c]
|
|
smb[:data] ||= ''
|
|
|
|
buff = ''
|
|
begin
|
|
buff = c.get_once(-1, 0.25)
|
|
# Handle any number of errors that a read can trigger depending on socket state
|
|
rescue ::IOError, ::EOFError,
|
|
::Errno::ECONNRESET, ::Errno::ENOTCONN, ::Errno::ECONNABORTED,
|
|
::Errno::ETIMEDOUT, ::Errno::ENETRESET, ::Errno::ESHUTDOWN
|
|
vprint_status("Dropping connection from #{smb[:name]} due to exception: #{$!.class} #{$!}")
|
|
smb_stop(c)
|
|
return
|
|
end
|
|
|
|
# The client said it had data, but lied, kill the session
|
|
unless buff and buff.length > 0
|
|
vprint_status("Dropping connection from #{smb[:name]} due to empty payload...")
|
|
smb_stop(c)
|
|
return
|
|
end
|
|
|
|
# Append the new data to the buffer
|
|
smb[:data] << buff
|
|
|
|
# Prevent a simplistic DoS if the buffer is too big
|
|
if smb[:data].length > (1024*1024*datastore['SMBServerMaximumBuffer'])
|
|
vprint_status("Dropping connection from #{smb[:name]} due to oversized buffer of #{smb[:data].length} bytes...")
|
|
smb_stop(c)
|
|
return
|
|
end
|
|
|
|
# Update the last-seen timestamp and purge old entries
|
|
smb_pool_update(c)
|
|
|
|
while(smb[:data].length > 0)
|
|
|
|
return if smb[:data].length < 4
|
|
|
|
plen = smb[:data][2,2].unpack('n')[0]
|
|
|
|
return if smb[:data].length < plen+4
|
|
|
|
buff = smb[:data].slice!(0, plen+4)
|
|
|
|
pkt_nbs = CONST::NBRAW_PKT.make_struct
|
|
pkt_nbs.from_s(buff)
|
|
|
|
# print_status("NetBIOS request from #{smb[:name]} #{pkt_nbs.v['Type']} #{pkt_nbs.v['Flags']} #{buff.inspect}")
|
|
|
|
# Check for a NetBIOS name request
|
|
if (pkt_nbs.v['Type'] == 0x81)
|
|
# Accept any name they happen to send
|
|
|
|
host_dst = UTILS.nbname_decode(pkt_nbs.v['Payload'][1,32]).gsub(/[\x00\x20]+$/n, '')
|
|
host_src = UTILS.nbname_decode(pkt_nbs.v['Payload'][35,32]).gsub(/[\x00\x20]+$/n, '')
|
|
|
|
smb[:nbdst] = host_dst
|
|
smb[:nbsrc] = host_src
|
|
|
|
# print_status("NetBIOS session request from #{smb[:name]} (asking for #{host_dst} from #{host_src})")
|
|
c.write("\x82\x00\x00\x00")
|
|
next
|
|
end
|
|
|
|
|
|
#
|
|
# TODO: Support AndX parameters
|
|
#
|
|
|
|
|
|
# Cast this to a generic SMB structure
|
|
pkt = CONST::SMB_BASE_PKT.make_struct
|
|
pkt.from_s(buff)
|
|
|
|
# Only respond to requests, ignore server replies
|
|
if (pkt['Payload']['SMB'].v['Flags1'] & 128 != 0)
|
|
vprint_status("Dropping connection from #{smb[:name]} due to missing client request flag")
|
|
smb_stop(c)
|
|
return
|
|
end
|
|
|
|
cmd = pkt['Payload']['SMB'].v['Command']
|
|
begin
|
|
smb_cmd_dispatch(cmd, c, buff)
|
|
rescue ::Interrupt
|
|
raise $!
|
|
rescue ::Exception => e
|
|
print_status("Error processing request from #{smb[:name]} (#{cmd}): #{e.class} #{e} #{e.backtrace}")
|
|
next
|
|
end
|
|
end
|
|
end
|
|
|
|
def smb_cmd_dispatch(cmd, c, buff)
|
|
smb = @state[c]
|
|
print_status("Received command #{cmd} from #{smb[:name]}")
|
|
end
|
|
|
|
def smb_set_defaults(c, pkt)
|
|
smb = @state[c]
|
|
pkt['Payload']['SMB'].v['ProcessID'] = smb[:process_id].to_i
|
|
pkt['Payload']['SMB'].v['UserID'] = smb[:user_id].to_i
|
|
pkt['Payload']['SMB'].v['TreeID'] = smb[:tree_id].to_i
|
|
pkt['Payload']['SMB'].v['MultiplexID'] = smb[:multiplex_id].to_i
|
|
end
|
|
|
|
def smb_error(cmd, c, errorclass, esn = false)
|
|
# 0xc0000022 = Deny
|
|
# 0xc000006D = Logon_Failure
|
|
# 0x00000000 = Ignore
|
|
pkt = CONST::SMB_BASE_PKT.make_struct
|
|
smb_set_defaults(c, pkt)
|
|
pkt['Payload']['SMB'].v['Command'] = cmd
|
|
pkt['Payload']['SMB'].v['Flags1'] = CONST::FLAGS_REQ_RES | CONST::FLAGS_CASE_SENSITIVE
|
|
if esn
|
|
pkt['Payload']['SMB'].v['Flags2'] =
|
|
CONST::FLAGS2_UNICODE_STRINGS +
|
|
CONST::FLAGS2_EXTENDED_SECURITY +
|
|
CONST::FLAGS2_32_BIT_ERROR_CODES +
|
|
CONST::FLAGS2_LONG_PATH_COMPONENTS
|
|
else
|
|
pkt['Payload']['SMB'].v['Flags2'] =
|
|
CONST::FLAGS2_UNICODE_STRINGS +
|
|
CONST::FLAGS2_32_BIT_ERROR_CODES +
|
|
CONST::FLAGS2_LONG_PATH_COMPONENTS
|
|
end
|
|
pkt['Payload']['SMB'].v['ErrorClass'] = errorclass
|
|
c.put(pkt.to_s)
|
|
end
|
|
|
|
# Update the last-seen timestamp and purge old entries
|
|
def smb_pool_update(c)
|
|
|
|
@state[c][:last_action] = Time.now.to_f
|
|
@smb_server_request_counter += 1
|
|
|
|
unless @smb_server_request_counter % 100 == 0 ||
|
|
@smb_server_last_pool_sweep + datastore['SMBServerIdleTimeout'].to_f < Time.now.to_f
|
|
return
|
|
end
|
|
|
|
# Synchronize pool sweeps in case we move to threaded services
|
|
@smb_server_pool_mutex.synchronize do
|
|
purge_list = []
|
|
|
|
@smb_server_last_pool_sweep = Time.now.to_f
|
|
|
|
@state.keys.each do |sc|
|
|
if @state[sc][:last_action] + datastore['SMBServerIdleTimeout'].to_f < Time.now.to_f
|
|
purge_list << sc
|
|
end
|
|
end
|
|
|
|
# Purge any idle connections to rescue file descriptors
|
|
purge_list.each do |sc|
|
|
vprint_status("Dropping connection from #{@state[sc][:name]} due to idle timeout...")
|
|
smb_stop(sc)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
end
|
|
|