# -*- coding: binary -*-
require 'rex/proto/smb'
require 'rex/proto/ntlm'
require 'rex/proto/dcerpc'
require 'rex/encoder/ndr'
require 'recog'

module Msf
  module Exploit::Remote::SMB
    # 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 Client

      include Msf::Exploit::Remote::Tcp
      include Msf::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::Client)

        register_advanced_options(
        [
          OptBool.new('SMBDirect', [ false, '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::Client)

        register_options(
        [
          Opt::RHOST,
          OptPort.new('RPORT', [ true, 'The SMB service port', 445])
        ], Msf::Exploit::Remote::SMB::Client)

        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()
          # The login method can throw any number of exceptions, we don't
          # care since we still get the native_lm/native_os.
          begin
            smb_login()
          rescue ::Rex::Proto::SMB::Exceptions::NoReply,
                 ::Rex::Proto::SMB::Exceptions::ErrorCode,
                 ::Rex::Proto::SMB::Exceptions::LoginError
          end
        end

        fprint['native_os'] = smb_peer_os()
        fprint['native_lm'] = smb_peer_lm()

        # Leverage Recog for SMB native OS fingerprinting
        fp_match = Recog::Nizer.match('smb.native_os', fprint['native_os']) || { }

        os = fp_match['os.product'] || 'Unknown'
        sp = fp_match['os.version'] || ''

        # Metasploit prefers 'Windows 2003' vs 'Windows Server 2003'
        if os =~ /^Windows Server/
          os = os.sub(/^Windows Server/, 'Windows')
        end

        if fp_match['os.edition']
          fprint['edition'] = fp_match['os.edition']
        end

        if fp_match['os.build']
          fprint['build'] = fp_match['os.build']
        end

        if sp == ''
          sp = smb_fingerprint_windows_sp(os)
        end

        lang = smb_fingerprint_windows_lang

        fprint['os']   = os
        fprint['sp']   = sp
        fprint['lang'] = lang

        fprint
      end

      #
      # Determine the service pack level of a Windows system via SMB probes
      #
      def smb_fingerprint_windows_sp(os)
        sp = ''

        if (os == 'Windows XP')
          # 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

        sp
      end


      #
      # Determine the native language pack of a Windows system via SMB probes
      #
      def smb_fingerprint_windows_lang

        #
        # 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+/n).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
        lang
      end

      # @return [Rex::Proto::SMB::SimpleClient]
      attr_accessor :simple
    end
  end
end
