rapid7/metasploit-framework

View on GitHub
lib/rex/proto/mssql/client_mixin.rb

Summary

Maintainability
F
5 days
Test Coverage
module Rex
module Proto
module MSSQL
# A base mixin of useful mssql methods for parsing structures etc
module ClientMixin
  include Msf::Module::UI::Message
  extend Forwardable
  def_delegators :@framework_module, :print_prefix, :print_status, :print_error, :print_good, :print_warning, :print_line
  # Encryption
  ENCRYPT_OFF     = 0x00 #Encryption is available but off.
  ENCRYPT_ON      = 0x01 #Encryption is available and on.
  ENCRYPT_NOT_SUP = 0x02 #Encryption is not available.
  ENCRYPT_REQ     = 0x03 #Encryption is required.

  # Packet Type
  TYPE_SQL_BATCH                   = 1  # (Client) SQL command
  TYPE_PRE_TDS7_LOGIN              = 2  # (Client) Pre-login with version < 7 (unused)
  TYPE_RPC                         = 3  # (Client) RPC
  TYPE_TABLE_RESPONSE              = 4  # (Server)  Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters,
  # Request Completion, Error and Info Messages, Attention Acknowledgement
  TYPE_ATTENTION_SIGNAL            = 6  # (Client) Attention
  TYPE_BULK_LOAD                   = 7  # (Client) SQL Command with binary data
  TYPE_TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager
  TYPE_TDS7_LOGIN                  = 16 # (Client) Login
  TYPE_SSPI_MESSAGE                = 17 # (Client) Login
  TYPE_PRE_LOGIN_MESSAGE           = 18 # (Client) pre-login with version > 7

  # Status
  STATUS_NORMAL                  = 0x00
  STATUS_END_OF_MESSAGE          = 0x01
  STATUS_IGNORE_EVENT            = 0x02
  STATUS_RESETCONNECTION         = 0x08 # TDS 7.1+
  STATUS_RESETCONNECTIONSKIPTRAN = 0x10 # TDS 7.3+

  # Mappings for ENVCHANGE types
  # See the TDS Specification here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/2b3eb7e5-d43d-4d1b-bf4d-76b9e3afc791
  module ENVCHANGE
    DATABASE = 1
    LANGUAGE = 2
    CHARACTER_SET = 3
    PACKET_SIZE = 4
    UNICODE_LOCAL_ID = 5
    UNICODE_COMPARISON_FLAGS = 6
    SQL_COLLATION = 7
    BEGIN_TRANSACTION = 8
    COMMIT_TRANSACTION = 9
    ROLLBACK_TRANSACTION = 10
    ENLIST_DTC_TRANSACTION = 11
    DEFECT_TRANSACTION = 12
    REAL_TIME_LOG_SHIPPING = 13
    PROMOTE_TRANSACTION = 15
    TRANSACTION_MANAGER_ADDRESS = 16
    TRANSACTION_ENDED = 17
    COMPLETION_ACKNOWLEDGEMENT = 18
    NAME_OF_USER_INSTANCE = 19
    ROUTING_INFORMATION = 20
  end

  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'    => "Response",
        'Columns'   => info[:colnames],
        'SortIndex' => -1
      )

      info[:rows].each do |row|
        tbl << row.map{ |x| x.nil? ? 'nil' : x }
      end

      print_line(tbl.to_s)
    end
  end

  def mssql_prelogin_packet
    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")

    # if manually set, we will honour
    if tdsencryption == true
      encryption = ENCRYPT_ON
    else
      encryption = ENCRYPT_NOT_SUP
    end

    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
    pkt
  end

  def parse_prelogin_response(resp)
    data = {}
    if resp.length > 5 # minimum size for response specification
      version_index = resp.slice(1, 2).unpack('n')[0]

      major = resp.slice(version_index, 1).unpack('C')[0]
      minor = resp.slice(version_index+1, 1).unpack('C')[0]
      build = resp.slice(version_index+2, 2).unpack('n')[0]

      enc_index = resp.slice(6, 2).unpack('n')[0]
      data[:encryption] = resp.slice(enc_index, 1).unpack('C')[0]
    end

    if major && minor && build
      data[:version] = "#{major}.#{minor}.#{build}"
    end

    return data
  end

  def mssql_send_recv(req, timeout=15, check_status = true)
    sock.put(req)

    # Read the 8 byte header to get the length and status
    # Read the length to get the data
    # If the status is 0, read another header and more data

    done = false
    resp = ""

    while(not done)
      head = sock.get_once(8, timeout)
      if !(head && head.length == 8)
        return false
      end

      # Is this the last buffer?
      if head[1, 1] == "\x01" || !check_status
        done = true
      end

      # Grab this block's length
      rlen = head[2, 2].unpack('n')[0] - 8

      while(rlen > 0)
        buff = sock.get_once(rlen, timeout)
        return if not buff
        resp << buff
        rlen -= buff.length
      end
    end

    resp
  end

  #
  # Encrypt a password according to the TDS protocol (encode)
  #
  def mssql_tds_encrypt(pass)
    # Convert to unicode, swap 4 bits both ways, xor with 0xa5
    Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*")
  end

  def mssql_xpcmdshell(cmd, doprint=false, opts={})
    force_enable = false
    begin
      res = 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...")
            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
  #
  # Parse a raw TDS reply from the server
  #
  def mssql_parse_tds_reply(data, info)
    info[:errors] ||= []
    info[:colinfos] ||= []
    info[:colnames] ||= []

    # Parse out the columns
    cols = data.slice!(0, 2).unpack('v')[0]
    0.upto(cols-1) do |col_idx|
      col = {}
      info[:colinfos][col_idx] = col

      col[:utype] = data.slice!(0, 2).unpack('v')[0]
      col[:flags] = data.slice!(0, 2).unpack('v')[0]
      col[:type]  = data.slice!(0, 1).unpack('C')[0]
      case col[:type]
      when 48
        col[:id] = :tinyint

      when 52
        col[:id] = :smallint

      when 56
        col[:id] = :rawint

      when 61
        col[:id] = :datetime

      when 34
        col[:id]            = :image
        col[:max_size]      = data.slice!(0, 4).unpack('V')[0]
        col[:value_length]  = data.slice!(0, 2).unpack('v')[0]
        col[:value]         = data.slice!(0, col[:value_length]  * 2).gsub("\x00", '')

      when 109
        col[:id] = :float
        col[:value_length] = data.slice!(0, 1).unpack('C')[0]

      when 108
        col[:id] = :numeric
        col[:value_length] = data.slice!(0, 1).unpack('C')[0]
        col[:precision] = data.slice!(0, 1).unpack('C')[0]
        col[:scale] = data.slice!(0, 1).unpack('C')[0]

      when 60
        col[:id] = :money

      when 110
        col[:value_length] = data.slice!(0, 1).unpack('C')[0]
        case col[:value_length]
        when 8
          col[:id] = :money
        when 4
          col[:id] = :smallmoney
        else
          col[:id] = :unknown
        end

      when 111
        col[:value_length] = data.slice!(0, 1).unpack('C')[0]
        case col[:value_length]
        when 4
          col[:id] = :smalldatetime
        when 8
          col[:id] = :datetime
        else
          col[:id] = :unknown
        end

      when 122
        col[:id] = :smallmoney

      when 59
        col[:id] = :float

      when 58
        col[:id] = :smalldatetime

      when 36
        col[:id] = :guid
        col[:value_length] = data.slice!(0, 1).unpack('C')[0]

      when 38
        col[:id] = :int
        col[:int_size] = data.slice!(0, 1).unpack('C')[0]

      when 50
        col[:id] = :bit

      when 99
        col[:id] = :ntext
        col[:max_size] = data.slice!(0, 4).unpack('V')[0]
        col[:codepage] = data.slice!(0, 2).unpack('v')[0]
        col[:cflags] = data.slice!(0, 2).unpack('v')[0]
        col[:charset_id] =  data.slice!(0, 1).unpack('C')[0]
        col[:namelen] = data.slice!(0, 1).unpack('C')[0]
        col[:table_name] = data.slice!(0, (col[:namelen] * 2) + 1).gsub("\x00", '')

      when 104
        col[:id] = :bitn
        col[:int_size] = data.slice!(0, 1).unpack('C')[0]

      when 127
        col[:id] = :bigint

      when 165
        col[:id] = :hex
        col[:max_size] = data.slice!(0, 2).unpack('v')[0]

      when 173
        col[:id] = :hex # binary(2)
        col[:max_size] = data.slice!(0, 2).unpack('v')[0]

      when 231, 175, 167, 239
        col[:id] = :string
        col[:max_size] = data.slice!(0, 2).unpack('v')[0]
        col[:codepage] = data.slice!(0, 2).unpack('v')[0]
        col[:cflags] = data.slice!(0, 2).unpack('v')[0]
        col[:charset_id] =  data.slice!(0, 1).unpack('C')[0]

      else
        col[:id] = :unknown

        # See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/ce3183a6-9d89-47e8-a02f-de5a1a1303de for details about column types
        info[:errors] << "Unsupported column type: #{col[:type]}. "
        return info
      end

      col[:msg_len] = data.slice!(0, 1).unpack('C')[0]

      if col[:msg_len] && col[:msg_len] > 0
        col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '')
      end
      info[:colnames] << (col[:name] || 'NULL')
    end
  end

  #
  # Parse individual tokens from a TDS reply
  #
  def mssql_parse_reply(data, info)
    info[:errors] = []
    return if not data
    states = []
    until data.empty? || info[:errors].any?
      token = data.slice!(0, 1).unpack('C')[0]
      case token
      when 0x81
        states << :mssql_parse_tds_reply
        mssql_parse_tds_reply(data, info)
      when 0xd1
        states << :mssql_parse_tds_row
        mssql_parse_tds_row(data, info)
      when 0xe3
        states << :mssql_parse_env
        mssql_parse_env(data, info)
      when 0x79
        states << :mssql_parse_ret
        mssql_parse_ret(data, info)
      when 0xfd, 0xfe, 0xff
        states << :mssql_parse_done
        mssql_parse_done(data, info)
      when 0xad
        states << :mssql_parse_login_ack
        mssql_parse_login_ack(data, info)
      when 0xab
        states << :mssql_parse_info
        mssql_parse_info(data, info)
      when 0xaa
        states << :mssql_parse_error
        mssql_parse_error(data, info)
      when nil
        break
      else
        info[:errors] << "unsupported token: #{token}. Previous states: #{states}"
        break
      end
    end
    info
  end

  #
  # Parse a single row of a TDS reply
  #
  def mssql_parse_tds_row(data, info)
    info[:rows] ||= []
    row = []

    info[:colinfos].each do |col|

      if(data.length == 0)
        row << "<EMPTY>"
        next
      end

      case col[:id]
      when :hex
        str = ""
        len = data.slice!(0, 2).unpack('v')[0]
        if len > 0 && len < 65535
          str << data.slice!(0, len)
        end
        row << str.unpack("H*")[0]

      when :guid
        read_length = data.slice!(0, 1).unpack1('C')
        if read_length == 0
          row << nil
        else
          row << Rex::Text.to_guid(data.slice!(0, read_length))
        end

      when :string
        str = ""
        len = data.slice!(0, 2).unpack('v')[0]
        if len > 0 && len < 65535
          str << data.slice!(0, len)
        end
        row << str.gsub("\x00", '')

      when :ntext
        str = nil
        ptrlen = data.slice!(0, 1).unpack("C")[0]
        ptr = data.slice!(0, ptrlen)
        unless ptrlen == 0
          timestamp = data.slice!(0, 8)
          datalen = data.slice!(0, 4).unpack("V")[0]
          if datalen > 0 && datalen < 65535
            str = data.slice!(0, datalen).gsub("\x00", '')
          else
            str = ''
          end
        end
        row << str

      when :float
        datalen = data.slice!(0, 1).unpack('C')[0]
        case datalen
        when 8
          row << data.slice!(0, datalen).unpack('E')[0]
        when 4
          row << data.slice!(0, datalen).unpack('e')[0]
        else
          row << nil
        end

      when :numeric
        varlen = data.slice!(0, 1).unpack('C')[0]
        if varlen == 0
          row << nil
        else
          sign = data.slice!(0, 1).unpack('C')[0]
          raw = data.slice!(0, varlen - 1)
          value = ''

          case varlen
          when 5
            value = raw.unpack('L')[0]/(10**col[:scale]).to_f
          when 9
            value = raw.unpack('Q')[0]/(10**col[:scale]).to_f
          when 13
            chunks = raw.unpack('L3')
            value = chunks[2] << 64 | chunks[1] << 32 | chunks[0]
            value /= (10**col[:scale]).to_f
          when 17
            chunks = raw.unpack('L4')
            value = chunks[3] << 96 | chunks[2] << 64 | chunks[1] << 32 | chunks[0]
            value /= (10**col[:scale]).to_f
          end
          case sign
          when 1
            row << value
          when 0
            row << value * -1
          end
        end

      when :money
        datalen = data.slice!(0, 1).unpack('C')[0]
        if datalen == 0
          row << nil
        else
          raw = data.slice!(0, datalen)
          rev = raw.slice(4, 4) << raw.slice(0, 4)
          row << rev.unpack('q')[0]/10000.0
        end

      when :smallmoney
        datalen = data.slice!(0, 1).unpack('C')[0]
        if datalen == 0
          row << nil
        else
          row << data.slice!(0, datalen).unpack('l')[0] / 10000.0
        end

      when :smalldatetime
        datalen = data.slice!(0, 1).unpack('C')[0]
        if datalen == 0
          row << nil
        else
          days = data.slice!(0, 2).unpack('S')[0]
          minutes = data.slice!(0, 2).unpack('S')[0] / 1440.0
          row << DateTime.new(1900, 1, 1) + days + minutes
        end

      when :datetime
        datalen = data.slice!(0, 1).unpack('C')[0]
        if datalen == 0
          row << nil
        else
          days = data.slice!(0, 4).unpack('l')[0]
          minutes = data.slice!(0, 4).unpack('l')[0] / 1440.0
          row << DateTime.new(1900, 1, 1) + days + minutes
        end

      when :rawint
        row << data.slice!(0, 4).unpack('V')[0]

      when :bigint
        row << data.slice!(0, 8).unpack("H*")[0]

      when :smallint
        row << data.slice!(0, 2).unpack("v")[0]

      when :smallint3
        row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0]

      when :tinyint
        row << data.slice!(0, 1).unpack("C")[0]

      when :bitn
        has_value = data.slice!(0, 1).unpack("C")[0]
        if has_value == 0
          row << nil
        else
          row << data.slice!(0, 1).unpack("C")[0]
        end

      when :bit
        row << data.slice!(0, 1).unpack("C")[0]

      when :image
        str = ''
        len = data.slice!(0, 1).unpack('C')[0]
        str = data.slice!(0, len) if len && len > 0
        row << str.unpack("H*")[0]

      when :int
        len = data.slice!(0, 1).unpack("C")[0]
        raw = data.slice!(0, len) if len && len > 0

        case len
        when 0, 255
          row << ''
        when 1
          row << raw.unpack("C")[0]
        when 2
          row << raw.unpack('v')[0]
        when 4
          row << raw.unpack('V')[0]
        when 5
          row << raw.unpack('V')[0] # XXX: missing high byte
        when 8
          row << raw.unpack('VV')[0] # XXX: missing high dword
        else
          info[:errors] << "invalid integer size: #{len} #{data[0, 16].unpack("H*")[0]}"
        end
      else
        info[:errors] << "unknown column type: #{col.inspect}"
      end
    end

    info[:rows] << row
    info
  end

  #
  # Parse a "ret" TDS token
  #
  def mssql_parse_ret(data, info)
    ret = data.slice!(0, 4).unpack('N')[0]
    info[:ret] = ret
    info
  end

  #
  # Parse a "done" TDS token
  #
  def mssql_parse_done(data, info)
    status, cmd, rows = data.slice!(0, 8).unpack('vvV')
    info[:done] = { :status => status, :cmd => cmd, :rows => rows }
    info
  end

  #
  # Parse an "error" TDS token
  #
  def mssql_parse_error(data, info)
    len  = data.slice!(0, 2).unpack('v')[0]
    buff = data.slice!(0, len)

    errno, state, sev, elen = buff.slice!(0, 8).unpack('VCCv')
    emsg = buff.slice!(0, elen * 2)
    emsg.gsub!("\x00", '')

    info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
    info
  end

  #
  # Parse an "environment change" TDS token
  #
  def mssql_parse_env(data, info)
    len  = data.slice!(0, 2).unpack('v')[0]
    buff = data.slice!(0, len)
    type = buff.slice!(0, 1).unpack('C')[0]

    nval = ''
    nlen = buff.slice!(0, 1).unpack('C')[0] || 0
    nval = buff.slice!(0, nlen * 2).gsub("\x00", '') if nlen > 0

    oval = ''
    olen = buff.slice!(0, 1).unpack('C')[0] || 0
    oval = buff.slice!(0, olen * 2).gsub("\x00", '') if olen > 0

    info[:envs] ||= []
    info[:envs] << { :type => type, :old => oval, :new => nval }

    self.current_database = nval if type == ENVCHANGE::DATABASE

    info
  end

  #
  # Parse an "information" TDS token
  #
  def mssql_parse_info(data, info)
    len  = data.slice!(0, 2).unpack('v')[0]
    buff = data.slice!(0, len)

    errno, state, sev, elen = buff.slice!(0, 8).unpack('VCCv')
    emsg = buff.slice!(0, elen * 2)
    emsg.gsub!("\x00", '')

    info[:infos] ||= []
    info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
    info
  end

  #
  # Parse a "login ack" TDS token
  #
  def mssql_parse_login_ack(data, info)
    len = data.slice!(0, 2).unpack('v')[0]
    _buff = data.slice!(0, len)
    info[:login_ack] = true
  end

end
end
end
end