Path: blob/master/lib/rex/proto/mssql/client_mixin.rb
33819 views
require 'rex/proto/ms_tds'12module Rex3module Proto4module MSSQL5# A base mixin of useful mssql methods for parsing structures etc6module ClientMixin7include Msf::Module::UI::Message8include Rex::Proto::MsTds9extend Forwardable10def_delegators :@framework_module, :print_prefix, :print_status, :print_error, :print_good, :print_warning, :print_line11# Encryption12ENCRYPT_OFF = 0x00 #Encryption is available but off.13ENCRYPT_ON = 0x01 #Encryption is available and on.14ENCRYPT_NOT_SUP = 0x02 #Encryption is not available.15ENCRYPT_REQ = 0x03 #Encryption is required.1617# Packet Type18TYPE_SQL_BATCH = 1 # (Client) SQL command19TYPE_PRE_TDS7_LOGIN = 2 # (Client) Pre-login with version < 7 (unused)20TYPE_RPC = 3 # (Client) RPC21TYPE_TABLE_RESPONSE = 4 # (Server) Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters,22# Request Completion, Error and Info Messages, Attention Acknowledgement23TYPE_ATTENTION_SIGNAL = 6 # (Client) Attention24TYPE_BULK_LOAD = 7 # (Client) SQL Command with binary data25TYPE_TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager26TYPE_TDS7_LOGIN = 16 # (Client) Login27TYPE_SSPI_MESSAGE = 17 # (Client) Login28TYPE_PRE_LOGIN_MESSAGE = 18 # (Client) pre-login with version > 72930# Status31STATUS_NORMAL = MsTdsStatus::NORMAL32STATUS_END_OF_MESSAGE = MsTdsStatus::END_OF_MESSAGE33STATUS_IGNORE_EVENT = MsTdsStatus::IGNORE_EVENT34STATUS_RESETCONNECTION = MsTdsStatus::RESETCONNECTION35STATUS_RESETCONNECTIONSKIPTRAN = MsTdsStatus::RESECCONNECTIONTRAN3637# Mappings for ENVCHANGE types38# See the TDS Specification here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/2b3eb7e5-d43d-4d1b-bf4d-76b9e3afc79139module ENVCHANGE40DATABASE = 141LANGUAGE = 242CHARACTER_SET = 343PACKET_SIZE = 444UNICODE_LOCAL_ID = 545UNICODE_COMPARISON_FLAGS = 646SQL_COLLATION = 747BEGIN_TRANSACTION = 848COMMIT_TRANSACTION = 949ROLLBACK_TRANSACTION = 1050ENLIST_DTC_TRANSACTION = 1151DEFECT_TRANSACTION = 1252REAL_TIME_LOG_SHIPPING = 1353PROMOTE_TRANSACTION = 1554TRANSACTION_MANAGER_ADDRESS = 1655TRANSACTION_ENDED = 1756COMPLETION_ACKNOWLEDGEMENT = 1857NAME_OF_USER_INSTANCE = 1958ROUTING_INFORMATION = 2059end6061def mssql_print_reply(info)62print_status("SQL Query: #{info[:sql]}")6364if info[:done] && info[:done][:rows].to_i > 065print_status("Row Count: #{info[:done][:rows]} (Status: #{info[:done][:status]} Command: #{info[:done][:cmd]})")66end6768if info[:errors] && !info[:errors].empty?69info[:errors].each do |err|70print_error(err)71end72end7374if info[:rows] && !info[:rows].empty?7576tbl = Rex::Text::Table.new(77'Indent' => 1,78'Header' => "Response",79'Columns' => info[:colnames],80'SortIndex' => -181)8283info[:rows].each do |row|84tbl << row.map{ |x| x.nil? ? 'nil' : x }85end8687print_line(tbl.to_s)88end89end9091def mssql_prelogin_packet92pkt_data_token = ""93pkt_data = ""9495pkt_hdr = MsTdsHeader.new(96packet_type: MsTdsType::PRE_LOGIN_MESSAGE97)9899version = [0x55010008, 0x0000].pack("Vv")100101# if manually set, we will honour102if tdsencryption == true103encryption = ENCRYPT_ON104else105encryption = ENCRYPT_NOT_SUP106end107108instoptdata = "MSSQLServer\0"109110threadid = "\0\0" + Rex::Text.rand_text(2)111112idx = 21 # size of pkt_data_token113pkt_data_token << [1140x00, # Token 0 type Version115idx , # VersionOffset116version.length, # VersionLength1171180x01, # Token 1 type Encryption119idx = idx + version.length, # EncryptionOffset1200x01, # EncryptionLength1211220x02, # Token 2 type InstOpt123idx = idx + 1, # InstOptOffset124instoptdata.length, # InstOptLength1251260x03, # Token 3 type Threadid127idx + instoptdata.length, # ThreadIdOffset1280x04, # ThreadIdLength1291300xFF131].pack('CnnCnnCnnCnnC')132133pkt_data << pkt_data_token134pkt_data << version135pkt_data << encryption136pkt_data << instoptdata137pkt_data << threadid138139pkt_hdr.packet_length += pkt_data.length140141pkt = pkt_hdr.to_binary_s + pkt_data142pkt143end144145def parse_prelogin_response(resp)146data = {}147if resp.length > 5 # minimum size for response specification148version_index = resp.slice(1, 2).unpack('n')[0]149150major = resp.slice(version_index, 1).unpack('C')[0]151minor = resp.slice(version_index+1, 1).unpack('C')[0]152build = resp.slice(version_index+2, 2).unpack('n')[0]153154enc_index = resp.slice(6, 2).unpack('n')[0]155data[:encryption] = resp.slice(enc_index, 1).unpack('C')[0]156end157158if major && minor && build159data[:version] = "#{major}.#{minor}.#{build}"160end161162return data163end164165def mssql_send_recv(req, timeout=15, check_status = true)166sock.put(req)167168# Read the 8 byte header to get the length and status169# Read the length to get the data170# If the status is 0, read another header and more data171172done = false173resp = ""174175while(not done)176head = sock.get_once(8, timeout)177if !(head && head.length == 8)178return false179end180181# Is this the last buffer?182if head[1, 1] == "\x01" || !check_status183done = true184end185186# Grab this block's length187rlen = head[2, 2].unpack('n')[0] - 8188189while(rlen > 0)190buff = sock.get_once(rlen, timeout)191return if not buff192resp << buff193rlen -= buff.length194end195end196197resp198end199200def mssql_xpcmdshell(cmd, doprint=false, opts={})201force_enable = false202begin203res = query("EXEC master..xp_cmdshell '#{cmd}'", false, opts)204if res[:errors] && !res[:errors].empty?205if res[:errors].join =~ /xp_cmdshell/206if force_enable207print_error("The xp_cmdshell procedure is not available and could not be enabled")208raise RuntimeError, "Failed to execute command"209else210print_status("The server may have xp_cmdshell disabled, trying to enable it...")211query(mssql_xpcmdshell_enable())212raise RuntimeError, "xp_cmdshell disabled"213end214end215end216217mssql_print_reply(res) if doprint218219return res220221rescue RuntimeError => e222if e.to_s =~ /xp_cmdshell disabled/223force_enable = true224retry225end226raise e227end228end229#230# Parse a raw TDS reply from the server231#232def mssql_parse_tds_reply(data, info)233info[:errors] ||= []234info[:colinfos] ||= []235info[:colnames] ||= []236237# Parse out the columns238cols = data.slice!(0, 2).unpack('v')[0]2390.upto(cols-1) do |col_idx|240col = {}241info[:colinfos][col_idx] = col242243col[:utype] = data.slice!(0, 2).unpack('v')[0]244col[:flags] = data.slice!(0, 2).unpack('v')[0]245col[:type] = data.slice!(0, 1).unpack('C')[0]246case col[:type]247when 48248col[:id] = :tinyint249250when 52251col[:id] = :smallint252253when 56254col[:id] = :rawint255256when 61257col[:id] = :datetime258259when 34260col[:id] = :image261col[:max_size] = data.slice!(0, 4).unpack('V')[0]262col[:value_length] = data.slice!(0, 2).unpack('v')[0]263col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '')264265when 109266col[:id] = :float267col[:value_length] = data.slice!(0, 1).unpack('C')[0]268269when 108270col[:id] = :numeric271col[:value_length] = data.slice!(0, 1).unpack('C')[0]272col[:precision] = data.slice!(0, 1).unpack('C')[0]273col[:scale] = data.slice!(0, 1).unpack('C')[0]274275when 60276col[:id] = :money277278when 110279col[:value_length] = data.slice!(0, 1).unpack('C')[0]280case col[:value_length]281when 8282col[:id] = :money283when 4284col[:id] = :smallmoney285else286col[:id] = :unknown287end288289when 111290col[:value_length] = data.slice!(0, 1).unpack('C')[0]291case col[:value_length]292when 4293col[:id] = :smalldatetime294when 8295col[:id] = :datetime296else297col[:id] = :unknown298end299300when 122301col[:id] = :smallmoney302303when 59304col[:id] = :float305306when 58307col[:id] = :smalldatetime308309when 36310col[:id] = :guid311col[:value_length] = data.slice!(0, 1).unpack('C')[0]312313when 38314col[:id] = :int315col[:int_size] = data.slice!(0, 1).unpack('C')[0]316317when 50318col[:id] = :bit319320when 99321col[:id] = :ntext322col[:max_size] = data.slice!(0, 4).unpack('V')[0]323col[:codepage] = data.slice!(0, 2).unpack('v')[0]324col[:cflags] = data.slice!(0, 2).unpack('v')[0]325col[:charset_id] = data.slice!(0, 1).unpack('C')[0]326col[:namelen] = data.slice!(0, 1).unpack('C')[0]327col[:table_name] = data.slice!(0, (col[:namelen] * 2) + 1).gsub("\x00", '')328329when 104330col[:id] = :bitn331col[:int_size] = data.slice!(0, 1).unpack('C')[0]332333when 127334col[:id] = :bigint335336when 165337col[:id] = :hex338col[:max_size] = data.slice!(0, 2).unpack('v')[0]339340when 173341col[:id] = :hex # binary(2)342col[:max_size] = data.slice!(0, 2).unpack('v')[0]343344when 231, 175, 167, 239345col[:id] = :string346col[:max_size] = data.slice!(0, 2).unpack('v')[0]347col[:codepage] = data.slice!(0, 2).unpack('v')[0]348col[:cflags] = data.slice!(0, 2).unpack('v')[0]349col[:charset_id] = data.slice!(0, 1).unpack('C')[0]350351else352col[:id] = :unknown353354# See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/ce3183a6-9d89-47e8-a02f-de5a1a1303de for details about column types355info[:errors] << "Unsupported column type: #{col[:type]}. "356return info357end358359col[:msg_len] = data.slice!(0, 1).unpack('C')[0]360361if col[:msg_len] && col[:msg_len] > 0362col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '')363end364info[:colnames] << (col[:name] || 'NULL')365end366end367368#369# Parse individual tokens from a TDS reply370#371def mssql_parse_reply(data, info=nil)372info ||= {}373info[:errors] = []374return if not data375states = []376until data.empty? || info[:errors].any?377token = data.slice!(0, 1).unpack('C')[0]378case token379when 0x81380states << :mssql_parse_tds_reply381mssql_parse_tds_reply(data, info)382when 0xd1383states << :mssql_parse_tds_row384mssql_parse_tds_row(data, info)385when 0xe3386states << :mssql_parse_env387mssql_parse_env(data, info)388when 0x79389states << :mssql_parse_ret390mssql_parse_ret(data, info)391when 0xfd, 0xfe, 0xff392states << :mssql_parse_done393mssql_parse_done(data, info)394when 0xad395states << :mssql_parse_login_ack396mssql_parse_login_ack(data, info)397when 0xab398states << :mssql_parse_info399mssql_parse_info(data, info)400when 0xaa401states << :mssql_parse_error402mssql_parse_error(data, info)403when nil404break405else406info[:errors] << "unsupported token: #{token}. Previous states: #{states}"407break408end409end410info411end412413#414# Parse a single row of a TDS reply415#416def mssql_parse_tds_row(data, info)417info[:rows] ||= []418row = []419420info[:colinfos].each do |col|421422if(data.length == 0)423row << "<EMPTY>"424next425end426427case col[:id]428when :hex429str = ""430len = data.slice!(0, 2).unpack('v')[0]431if len > 0 && len < 65535432str << data.slice!(0, len)433end434row << str.unpack("H*")[0]435436when :guid437read_length = data.slice!(0, 1).unpack1('C')438if read_length == 0439row << nil440else441row << Rex::Text.to_guid(data.slice!(0, read_length))442end443444when :string445str = ""446len = data.slice!(0, 2).unpack('v')[0]447if len > 0 && len < 65535448str << data.slice!(0, len)449end450row << str.gsub("\x00", '')451452when :ntext453str = nil454ptrlen = data.slice!(0, 1).unpack("C")[0]455ptr = data.slice!(0, ptrlen)456unless ptrlen == 0457timestamp = data.slice!(0, 8)458datalen = data.slice!(0, 4).unpack("V")[0]459if datalen > 0 && datalen < 65535460str = data.slice!(0, datalen).gsub("\x00", '')461else462str = ''463end464end465row << str466467when :float468datalen = data.slice!(0, 1).unpack('C')[0]469case datalen470when 8471row << data.slice!(0, datalen).unpack('E')[0]472when 4473row << data.slice!(0, datalen).unpack('e')[0]474else475row << nil476end477478when :numeric479varlen = data.slice!(0, 1).unpack('C')[0]480if varlen == 0481row << nil482else483sign = data.slice!(0, 1).unpack('C')[0]484raw = data.slice!(0, varlen - 1)485value = ''486487case varlen488when 5489value = raw.unpack('L')[0]/(10**col[:scale]).to_f490when 9491value = raw.unpack('Q')[0]/(10**col[:scale]).to_f492when 13493chunks = raw.unpack('L3')494value = chunks[2] << 64 | chunks[1] << 32 | chunks[0]495value /= (10**col[:scale]).to_f496when 17497chunks = raw.unpack('L4')498value = chunks[3] << 96 | chunks[2] << 64 | chunks[1] << 32 | chunks[0]499value /= (10**col[:scale]).to_f500end501case sign502when 1503row << value504when 0505row << value * -1506end507end508509when :money510datalen = data.slice!(0, 1).unpack('C')[0]511if datalen == 0512row << nil513else514raw = data.slice!(0, datalen)515rev = raw.slice(4, 4) << raw.slice(0, 4)516row << rev.unpack('q')[0]/10000.0517end518519when :smallmoney520datalen = data.slice!(0, 1).unpack('C')[0]521if datalen == 0522row << nil523else524row << data.slice!(0, datalen).unpack('l')[0] / 10000.0525end526527when :smalldatetime528datalen = data.slice!(0, 1).unpack('C')[0]529if datalen == 0530row << nil531else532days = data.slice!(0, 2).unpack('S')[0]533minutes = data.slice!(0, 2).unpack('S')[0] / 1440.0534row << DateTime.new(1900, 1, 1) + days + minutes535end536537when :datetime538datalen = data.slice!(0, 1).unpack('C')[0]539if datalen == 0540row << nil541else542days = data.slice!(0, 4).unpack('l')[0]543minutes = data.slice!(0, 4).unpack('l')[0] / 1440.0544row << DateTime.new(1900, 1, 1) + days + minutes545end546547when :rawint548row << data.slice!(0, 4).unpack('V')[0]549550when :bigint551row << data.slice!(0, 8).unpack("H*")[0]552553when :smallint554row << data.slice!(0, 2).unpack("v")[0]555556when :smallint3557row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0]558559when :tinyint560row << data.slice!(0, 1).unpack("C")[0]561562when :bitn563has_value = data.slice!(0, 1).unpack("C")[0]564if has_value == 0565row << nil566else567row << data.slice!(0, 1).unpack("C")[0]568end569570when :bit571row << data.slice!(0, 1).unpack("C")[0]572573when :image574str = ''575len = data.slice!(0, 1).unpack('C')[0]576str = data.slice!(0, len) if len && len > 0577row << str.unpack("H*")[0]578579when :int580len = data.slice!(0, 1).unpack("C")[0]581raw = data.slice!(0, len) if len && len > 0582583case len584when 0, 255585row << ''586when 1587row << raw.unpack("C")[0]588when 2589row << raw.unpack('v')[0]590when 4591row << raw.unpack('V')[0]592when 5593row << raw.unpack('V')[0] # XXX: missing high byte594when 8595row << raw.unpack('VV')[0] # XXX: missing high dword596else597info[:errors] << "invalid integer size: #{len} #{data[0, 16].unpack("H*")[0]}"598end599else600info[:errors] << "unknown column type: #{col.inspect}"601end602end603604info[:rows] << row605info606end607608#609# Parse a "ret" TDS token610#611def mssql_parse_ret(data, info)612ret = data.slice!(0, 4).unpack('N')[0]613info[:ret] = ret614info615end616617#618# Parse a "done" TDS token619#620def mssql_parse_done(data, info)621status, cmd, rows = data.slice!(0, 8).unpack('vvV')622info[:done] = { :status => status, :cmd => cmd, :rows => rows }623info624end625626#627# Parse an "error" TDS token628#629def mssql_parse_error(data, info)630len = data.slice!(0, 2).unpack('v')[0]631buff = data.slice!(0, len)632633errno, state, sev, elen = buff.slice!(0, 8).unpack('VCCv')634emsg = buff.slice!(0, elen * 2)635emsg.gsub!("\x00", '')636637info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"638info639end640641#642# Parse an "environment change" TDS token643#644def mssql_parse_env(data, info)645len = data.slice!(0, 2).unpack('v')[0]646buff = data.slice!(0, len)647type = buff.slice!(0, 1).unpack('C')[0]648649nval = ''650nlen = buff.slice!(0, 1).unpack('C')[0] || 0651nval = buff.slice!(0, nlen * 2).gsub("\x00", '') if nlen > 0652653oval = ''654olen = buff.slice!(0, 1).unpack('C')[0] || 0655oval = buff.slice!(0, olen * 2).gsub("\x00", '') if olen > 0656657info[:envs] ||= []658info[:envs] << { :type => type, :old => oval, :new => nval }659660self.current_database = nval if type == ENVCHANGE::DATABASE661662info663end664665#666# Parse an "information" TDS token667#668def mssql_parse_info(data, info)669len = data.slice!(0, 2).unpack('v')[0]670buff = data.slice!(0, len)671672errno, state, sev, elen = buff.slice!(0, 8).unpack('VCCv')673emsg = buff.slice!(0, elen * 2)674emsg.gsub!("\x00", '')675676info[:infos] ||= []677info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"678info679end680681#682# Parse a "login ack" TDS token683#684def mssql_parse_login_ack(data, info)685len = data.slice!(0, 2).unpack('v')[0]686_buff = data.slice!(0, len)687info[:login_ack] = true688end689690end691end692end693end694695696