# -*- 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