# -*- coding: binary -*- require 'rex/proto/smb' require 'rex/proto/ntlm' require 'rex/proto/dcerpc' require 'rex/encoder/ndr' module Msf ### # # This mixin provides utility methods for interacting with a SMB/CIFS service on # a remote machine. These methods may generally be useful in the context of # exploitation. This mixin extends the Tcp exploit mixin. Only one SMB # service can be accessed at a time using this class. # ### module Exploit::Remote::SMB require 'msf/core/exploit/smb/authenticated' require 'msf/core/exploit/smb/psexec' include Exploit::Remote::Tcp include Exploit::Remote::NTLM::Client SIMPLE = Rex::Proto::SMB::SimpleClient XCEPT = Rex::Proto::SMB::Exceptions CONST = Rex::Proto::SMB::Constants # Alias over the Rex DCERPC protocol modules DCERPCPacket = Rex::Proto::DCERPC::Packet DCERPCClient = Rex::Proto::DCERPC::Client DCERPCResponse = Rex::Proto::DCERPC::Response DCERPCUUID = Rex::Proto::DCERPC::UUID NDR = Rex::Encoder::NDR def initialize(info = {}) super register_evasion_options( [ OptBool.new('SMB::pipe_evasion', [ true, 'Enable segmented read/writes for SMB Pipes', false]), OptInt.new('SMB::pipe_write_min_size', [ true, 'Minimum buffer size for pipe writes', 1]), OptInt.new('SMB::pipe_write_max_size', [ true, 'Maximum buffer size for pipe writes', 1024]), OptInt.new('SMB::pipe_read_min_size', [ true, 'Minimum buffer size for pipe reads', 1]), OptInt.new('SMB::pipe_read_max_size', [ true, 'Maximum buffer size for pipe reads', 1024]), OptInt.new('SMB::pad_data_level', [ true, 'Place extra padding between headers and data (level 0-3)', 0]), OptInt.new('SMB::pad_file_level', [ true, 'Obscure path names used in open/create (level 0-3)', 0]), OptInt.new('SMB::obscure_trans_pipe_level', [ true, 'Obscure PIPE string in TransNamedPipe (level 0-3)', 0]), ], Msf::Exploit::Remote::SMB) register_advanced_options( [ OptBool.new('SMBDirect', [ true, 'The target port is a raw SMB service (not NetBIOS)', true ]), OptString.new('SMBUser', [ false, 'The username to authenticate as', '']), OptString.new('SMBPass', [ false, 'The password for the specified username', '']), OptString.new('SMBDomain', [ false, 'The Windows domain to use for authentication', '.']), OptString.new('SMBName', [ true, 'The NetBIOS hostname (required for port 139 connections)', '*SMBSERVER']), OptBool.new('SMB::VerifySignature', [ true, "Enforces client-side verification of server response signatures", false]), OptInt.new('SMB::ChunkSize', [ true, 'The chunk size for SMB segments, bigger values will increase speed but break NT 4.0 and SMB signing', 500]), # # Control the identified operating system of the client # OptString.new('SMB::Native_OS', [ true, 'The Native OS to send during authentication', 'Windows 2000 2195']), OptString.new('SMB::Native_LM', [ true, 'The Native LM to send during authentication', 'Windows 2000 5.0']), ], Msf::Exploit::Remote::SMB) register_options( [ Opt::RHOST, OptInt.new('RPORT', [ true, 'Set the SMB service port', 445]) ], Msf::Exploit::Remote::SMB) register_autofilter_ports([ 139, 445]) register_autofilter_services(%W{ netbios-ssn microsoft-ds }) end # Override {Exploit::Remote::Tcp#connect} to setup an SMB connection # and configure evasion options # # Also populates {#simple}. # # @param (see Exploit::Remote::Tcp#connect) # @return (see Exploit::Remote::Tcp#connect) def connect(global=true) disconnect() if global s = super(global) self.sock = s if global # Disable direct SMB when SMBDirect has not been set # and the destination port is configured as 139 direct = smb_direct if(datastore.default?('SMBDirect') and rport.to_i == 139) direct = false end c = SIMPLE.new(s, direct) # setup pipe evasion foo if datastore['SMB::pipe_evasion'] # XXX - insert code to change the instance of the read/write functions to do segmentation end if (datastore['SMB::pad_data_level']) c.client.evasion_opts['pad_data'] = datastore['SMB::pad_data_level'] end if (datastore['SMB::pad_file_level']) c.client.evasion_opts['pad_file'] = datastore['SMB::pad_file_level'] end if (datastore['SMB::obscure_trans_pipe_level']) c.client.evasion_opts['obscure_trans_pipe'] = datastore['SMB::obscure_trans_pipe_level'] end self.simple = c if global c end # Convert a standard ASCII string to 16-bit Unicode def unicode(str) Rex::Text.to_unicode(str) end # Establishes an SMB session over the default socket and connects to # the IPC$ share. # # You should call {#connect} before calling this # # @return [void] def smb_login simple.login( datastore['SMBName'], datastore['SMBUser'], datastore['SMBPass'], datastore['SMBDomain'], datastore['SMB::VerifySignature'], datastore['NTLM::UseNTLMv2'], datastore['NTLM::UseNTLM2_session'], datastore['NTLM::SendLM'], datastore['NTLM::UseLMKey'], datastore['NTLM::SendNTLM'], datastore['SMB::Native_OS'], datastore['SMB::Native_LM'], {:use_spn => datastore['NTLM::SendSPN'], :name => self.rhost} ) simple.connect("\\\\#{datastore['RHOST']}\\IPC$") end # This method returns the native operating system of the peer def smb_peer_os self.simple.client.peer_native_os end # This method returns the native lanman version of the peer def smb_peer_lm self.simple.client.peer_native_lm end # This method opens a handle to an IPC pipe def smb_create(pipe) self.simple.create_pipe(pipe) end #the default chunk size of 48000 for OpenFile is not compatible when signing is enabled (and with some nt4 implementations) #cause it looks like MS windows refuse to sign big packet and send STATUS_ACCESS_DENIED #fd.chunk_size = 500 is better def smb_open(path, perm) self.simple.open(path, perm, datastore['SMB::ChunkSize']) end def smb_hostname datastore['SMBName'] || '*SMBSERVER' end def smb_direct datastore['SMBDirect'] end def domain datastore['SMBDomain'] end def smbhost if domain == "." "#{rhost}:#{rport}" else "#{rhost}:#{rport}|#{domain}" end end # If the username contains a / slash, then # split it as a domain/username. NOTE: this # is predicated on forward slashes, and not # Microsoft's backwards slash convention. def domain_username_split(user) return user if(user.nil? || user.empty?) if !user[/\//] # Only /, not \! return [nil,user] else return user.split("/",2) end end def splitname(uname) if datastore["PRESERVE_DOMAINS"] d,u = domain_username_split(uname) return u else return uname end end # Whether a remote file exists # # @param file [String] Path to a file to remove, relative to the # most-recently connected share # @raise [Rex::Proto::SMB::Exceptions::ErrorCode] def smb_file_exist?(file) begin fd = simple.open(file, 'ro') rescue XCEPT::ErrorCode => e # If attempting to open the file results in a "*_NOT_FOUND" error, # then we can be sure the file is not there. # # Copy-pasted from smb/exceptions.rb to avoid the gymnastics # required to pull them out of a giant inverted hash # # 0xC0000034 => "STATUS_OBJECT_NAME_NOT_FOUND", # 0xC000003A => "STATUS_OBJECT_PATH_NOT_FOUND", # 0xC0000225 => "STATUS_NOT_FOUND", error_is_not_found = [ 0xC0000034, 0xC000003A, 0xC0000225 ].include?(e.error_code) # If the server returns some other error, then there was a # permissions problem or some other difficulty that we can't # really account for and hope the caller can deal with it. raise e unless error_is_not_found found = !error_is_not_found else # There was no exception, so we know the file is openable fd.close found = true end found end # Remove remote file # # @param file (see #smb_file_exist?) # @return [void] def smb_file_rm(file) fd = smb_open(file, 'ro') fd.delete end # # Fingerprinting methods # # Calls the EnumPrinters() function of the spooler service def smb_enumprinters(flags, name, level, blen) stub = NDR.long(flags) + (name ? NDR.uwstring(name) : NDR.long(0)) + NDR.long(level) + NDR.long(rand(0xffffffff)+1)+ NDR.long(blen) + "\x00" * blen + NDR.long(blen) handle = dcerpc_handle( '12345678-1234-abcd-ef00-0123456789ab', '1.0', 'ncacn_np', ["\\SPOOLSS"] ) begin dcerpc_bind(handle) dcerpc.call(0x00, stub) return dcerpc.last_response.stub_data rescue ::Interrupt raise $! rescue ::Exception => e return nil end end # This method dumps the print provider strings from the spooler def smb_enumprintproviders resp = smb_enumprinters(8, nil, 1, 0) return nil if not resp rptr, tmp, blen = resp.unpack("V*") resp = smb_enumprinters(8, nil, 1, blen) return nil if not resp bcnt,pcnt,stat = resp[-12, 12].unpack("VVV") return nil if stat != 0 return nil if pcnt == 0 return nil if bcnt > blen return nil if pcnt < 3 # # The correct way, which leads to invalid offsets :-( # #providers = [] # #0.upto(pcnt-1) do |i| # flags,desc_o,name_o,comm_o = resp[8 + (i*16), 16].unpack("VVVV") # # #desc = read_unicode(resp,8+desc_o).gsub("\x00", '') # #name = read_unicode(resp,8+name_o).gsub("\x00", '') # #comm = read_unicode(resp,8+comm_o).gsub("\x00", '') # #providers << [flags,desc,name,comm] #end # #providers return resp end # This method performs an extensive set of fingerprinting operations def smb_fingerprint fprint = {} # Connect to the server if needed if(not self.simple) connect() smb_login() end os = 'Unknown' sp = '' case smb_peer_os() when 'Windows NT 4.0' os = 'Windows NT 4.0' when 'Windows 5.0' os = 'Windows 2000' when 'Windows 5.1' os = 'Windows XP' when /Windows XP (\d+) Service Pack (\d+)/ os = 'Windows XP' sp = 'Service Pack ' + $2 when /Windows Server 2003 (\d+)$/ os = 'Windows 2003' sp = 'No Service Pack' when /Windows Server 2003 (\d+) Service Pack (\d+)/ os = 'Windows 2003' sp = 'Service Pack ' + $2 when /Windows Server 2003 R2 (\d+) Service Pack (\d+)/ os = 'Windows 2003 R2' sp = 'Service Pack ' + $2 when /Windows Vista \(TM\) (\w+|\w+ \w+) (\d+) Service Pack (\d+)/ os = 'Windows Vista ' + $1 sp = 'Service Pack ' + $3 when /Windows Vista \(TM\) (\w+|\w+ \w+) (\d+)/ os = 'Windows Vista ' + $1 sp = '(Build ' + $2 + ')' when /Windows Server \(R\) 2008 (([\-\w]+ ){1,4})(\d+) Service Pack (\d+)/ os = 'Windows 2008 ' + $1.strip sp = 'Service Pack ' + $4 when /Windows Server \(R\) 2008 (([\-\w]+ ){1,4})(\d+)/ os = 'Windows 2008 ' + $1.strip sp = '(Build ' + $3 + ')' when /Windows \(R\) Storage Server 2008 (([\-\w]+ ){1,4})(\d+) Service Pack (\d+)/ os = 'Windows 2008 Storage Server ' + $1.strip sp = 'Service Pack ' + $4 when /Windows \(R\) Storage Server 2008 (([\-\w]+ ){1,4})(\d+)/ os = 'Windows 2008 Storage Server ' + $1.strip sp = '(Build ' + $3 + ')' when /Windows 7 (([\-\w]+ ){1,4})(\d+)/ os = 'Windows 7 ' + $1.strip sp = '(Build ' + $3 + ')' when /^(Windows.*) Service Pack (\d+)/ os = $1.strip sp = 'Service Pack ' + $2 when /^(Windows.*) (\d+)/ os = $1.strip sp = '(Build ' + $2 + ')' when 'VxWorks' os = 'VxWorks' sp = smb_peer_lm() when 'Unix' os = 'Unix' sv = smb_peer_lm() case sv when /Samba\s+(.*)/i sp = 'Samba ' + $1 end end if (os == 'Windows XP' and sp.length == 0) # SRVSVC was blocked in SP2 begin smb_create("\\SRVSVC") sp = 'Service Pack 0 / 1' rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e if (e.error_code == 0xc0000022) sp = 'Service Pack 2+' end end end if (os == 'Windows 2000' and sp.length == 0) # LLSRPC was blocked in a post-SP4 update begin smb_create("\\LLSRPC") sp = 'Service Pack 0 - 4' rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e if (e.error_code == 0xc0000022) sp = 'Service Pack 4 with MS05-010+' end end end # # Perform granular XP SP checks if LSARPC is exposed # if (os == 'Windows XP') # # Service Pack 2 added a range(0,64000) to opnum 0x22 in SRVSVC # Credit to spoonm for first use of unbounded [out] buffers # handle = dcerpc_handle( '4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\BROWSER"] ) begin dcerpc_bind(handle) stub = NDR.uwstring(Rex::Text.rand_text_alpha(rand(10)+1)) + NDR.wstring(Rex::Text.rand_text_alpha(rand(10)+1)) + NDR.long(64001) + NDR.long(0) + NDR.long(0) dcerpc.call(0x22, stub) sp = "Service Pack 0 / 1" rescue ::Interrupt raise $! rescue ::Rex::Proto::SMB::Exceptions::ErrorCode rescue ::Rex::Proto::SMB::Exceptions::ReadPacket rescue ::Rex::Proto::DCERPC::Exceptions::Fault sp = "Service Pack 2+" rescue ::Exception end # # Service Pack 3 fixed information leaks via [unique][out] pointers # Call SRVSVC::NetRemoteTOD() to return [out] [ref] [unique] # Credit: # Pointer leak is well known, but Immunity also covered in a paper # Silent fix of pointer leak in SP3 and detection method by Rhys Kidd # handle = dcerpc_handle( '4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\BROWSER"] ) begin dcerpc_bind(handle) stub = NDR.uwstring(Rex::Text.rand_text_alpha(rand(8)+1)) resp = dcerpc.call(0x1c, stub) if(resp and resp[0,4] == "\x00\x00\x02\x00") sp = "Service Pack 3" else if(resp and sp =~ /Service Pack 2\+/) sp = "Service Pack 2" end end rescue ::Interrupt raise $! rescue ::Rex::Proto::SMB::Exceptions::ErrorCode rescue ::Rex::Proto::SMB::Exceptions::ReadPacket rescue ::Exception end end # # Remote language detection via Print Providers # Credit: http://immunityinc.com/downloads/Remote_Language_Detection_in_Immunity_CANVAS.odt # lang = 'Unknown' sigs = { 'English' => [ Rex::Text.to_unicode('Windows NT Remote Printers'), Rex::Text.to_unicode('LanMan Print Services') ], 'Spanish' => [ Rex::Text.to_unicode('Impresoras remotas Windows NT'), Rex::Text.to_unicode('Impresoras remotas de Windows NT') ], 'Italian' => [ Rex::Text.to_unicode('Stampanti remote di Windows NT'), Rex::Text.to_unicode('Servizi di stampa LanMan') ], 'French' => [ Rex::Text.to_unicode('Imprimantes distantes NT'), Rex::Text.to_unicode('Imprimantes distantes pour Windows NT'), Rex::Text.to_unicode("Services d'impression LanMan") ], 'German' => [ Rex::Text.to_unicode('Remotedrucker') ], 'Portuguese - Brazilian' => [ Rex::Text.to_unicode('Impr. remotas Windows NT'), Rex::Text.to_unicode('Impressoras remotas do Windows NT') ], 'Portuguese' => [ Rex::Text.to_unicode('Imp. remotas do Windows NT') ], 'Hungarian' => [ Rex::Text.to_unicode("\x54\xe1\x76\x6f\x6c\x69\x20\x6e\x79\x6f\x6d\x74\x61\x74\xf3\x6b") ], 'Finnish' => [ Rex::Text.to_unicode("\x45\x74\xe4\x74\x75\x6c\x6f\x73\x74\x69\x6d\x65\x74") ], 'Dutch' => [ Rex::Text.to_unicode('Externe printers voor NT') ], 'Danish' => [ Rex::Text.to_unicode('Fjernprintere') ], 'Swedish' => [ Rex::Text.to_unicode("\x46\x6a\xe4\x72\x72\x73\x6b\x72\x69\x76\x61\x72\x65") ], 'Polish' => [ Rex::Text.to_unicode('Zdalne drukarki') ], 'Czech' => [ Rex::Text.to_unicode("\x56\x7a\x64\xe1\x6c\x65\x6e\xe9\x20\x74\x69\x73\x6b\xe1\x72\x6e\x79") ], 'Turkish' => [ "\x59\x00\x61\x00\x7a\x00\x31\x01\x63\x00\x31\x01\x6c\x00\x61\x00\x72\x00" ], 'Japanese' => [ "\xea\x30\xe2\x30\xfc\x30\xc8\x30\x20\x00\xd7\x30\xea\x30\xf3\x30\xbf\x30" ], 'Chinese - Traditional' => [ "\xdc\x8f\x0b\x7a\x53\x62\x70\x53\x3a\x67" ], 'Chinese - Traditional / Taiwan' => [ "\x60\x90\xef\x7a\x70\x53\x68\x88\x5f\x6a", ], 'Korean' => [ "\xd0\xc6\xa9\xac\x20\x00\x04\xd5\xb0\xb9\x30\xd1", ], 'Russian' => [ "\x1f\x04\x40\x04\x38\x04\x3d\x04\x42\x04\x35\x04\x40\x04\x4b\x04\x20\x00\x43\x04\x34\x04\x30\x04\x3b\x04\x35\x04\x3d\x04\x3d\x04\x3e\x04\x33\x04\x3e\x04\x20\x00\x34\x04\x3e\x04\x41\x04\x42\x04\x43\x04\x3f\x04\x30\x04", ], } begin prov = smb_enumprintproviders() if(prov) sigs.each_key do |k| sigs[k].each do |s| if(prov.index(s)) lang = k break end break if lang != 'Unknown' end break if lang != 'Unknown' end if(lang == 'Unknown') @fpcache ||= {} mhash = ::Digest::MD5.hexdigest(prov[4,prov.length-4]) if(not @fpcache[mhash]) buff = "\n" buff << "*** NEW FINGERPRINT: PLEASE SEND TO [ msfdev[at]metasploit.com ]\n" buff << " VERS: $Revision$\n" buff << " HOST: #{rhost}\n" buff << " OS: #{os}\n" buff << " SP: #{sp}\n" prov.unpack("H*")[0].scan(/.{64}|.*/).each do |line| next if line.length == 0 buff << " FP: #{line}\n" end prov.split(/\x00\x00+/).each do |line| line.gsub!("\x00",'') line.strip! next if line.length < 6 buff << " TXT: #{line}\n" end buff << "*** END FINGERPRINT\n" print_line(buff) @fpcache[mhash] = true end end end rescue ::Interrupt raise $! rescue ::Rex::Proto::SMB::Exceptions::ErrorCode end fprint['os'] = os fprint['sp'] = sp fprint['lang'] = lang fprint end # @return [Rex::Proto::SMB::SimpleClient] attr_accessor :simple end ### # # This mixin provides a minimal SMB server # ### module Exploit::Remote::SMBServer include Exploit::Remote::TcpServer include 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 register_options( [ OptPort.new('SRVPORT', [ true, "The local port to listen on.", 445 ]) ], self.class) 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} end def smb_stop(c) @state.delete(c) end def smb_recv(c) smb = @state[c] smb[:data] ||= '' smb[:data] << c.get_once 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]+$/, '') host_src = UTILS.nbname_decode(pkt_nbs.v['Payload'][35,32]).gsub(/[\x00\x20]+$/, '') 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 response to requests, ignore server replies if (pkt['Payload']['SMB'].v['Flags1'] & 128 != 0) print_status("Ignoring server response from #{smb[:name]}") next 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'] = 0x88 if esn pkt['Payload']['SMB'].v['Flags2'] = 0xc801 else pkt['Payload']['SMB'].v['Flags2'] = 0xc001 end pkt['Payload']['SMB'].v['ErrorClass'] = errorclass c.put(pkt.to_s) end end end