high Threat analysis

OptinMonster Supply Chain Attack

Awesome Motive's CDN-hosted SDK files for WordPress plugins OptinMonster, TrustPulse, and PushEngage were tampered to inject malicious JavaScript. When an administrator logs in, the payload runs in their context, creates rogue administrator accounts, and silently installs a self-hiding PHP backdoor plugin, exfiltrating credentials to tidio[.]cc.

#supply-chain#wordpress#backdoor#malware
On this page 0% read

    Executive Summary

    WordPress plugins OptinMonster, TrustPulse, and PushEngage, all operated by WordPress plugin developer Awesome Motive, were affected by a supply chain attack where CDN-hosted JavaScript SDK files were tampered to inject malicious code. The campaign, active as of June 12, 2026, targeted sites running these plugins by delivering compromised files directly from the vendor’s CDN endpoints [sansec.io].

    The injected JavaScript executes specifically when a logged-in WordPress administrator visits the site. It attempts to create a rogue administrator account (developer_api1) and install a self-hiding PHP backdoor plugin disguised as normal utility plugins. Stolen administrative credentials and system details are exfiltrated to the lookalike command and control (C2) domain tidio[.]cc [sansec.io].

    Awesome Motive has updated the affected CDN assets, cleaning the scripts for OptinMonster and TrustPulse, and subsequently the PushEngage SDK. WordPress site owners must inspect their local databases and filesystems to detect and eradicate any rogue accounts or backdoor code planted during the exposure window.

    Key Facts

    FactValue
    Affected Assetsoptinmonster, trustpulse, pushengage
    Ecosystemwordpress
    Malicious Scriptsa.omappapi.com/app/js/api.min.js, a.opmnstr.com/app/js/api.min.js, a.optnmstr.com/app/js/api.min.js, a.trstplse.com/app/js/api.min.js, clientcdn.pushengage.com/sdks/pushengage-web-sdk.js
    Exposure Window2026-06-12 22:17:00 UTC to 2026-06-14
    Immediate ActionInspect users table for rogue accounts, search plugins directory for hidden backdoors, rotate passwords and salts

    Evidence Assessment

    • confirmed: Sansec observed malicious JavaScript payloads served directly from Awesome Motive’s CDN domains, including a.omappapi.com and clientcdn.pushengage.com [sansec.io].
    • confirmed: The malicious payload detects WordPress admin sessions via Cookies, paths, and admin bars, gating execution to logged-in administrators only [sansec.io].
    • confirmed: The malware creates an admin account with username developer_api1 and email [email protected] using multiple API fallbacks [sansec.io].
    • confirmed: A backdoor ZIP plugin is downloaded and installed, which actively hides itself from administrative plugin menus and registers unauthenticated endpoints (developer_api1_fm and developer_api1_eval) [sansec.io].
    • confirmed: Encrypted C2 communication was routed to the C2 server at tidio[.]cc [sansec.io].
    • unknown: The precise point of origin for the CDN compromise (Awesome Motive servers, CDN credentials, or upstream provider) remains unconfirmed [sansec.io].

    Impact Determination

    Exposure ClassificationCriteriaRequired EvidenceRequired ActionClosure Gate
    Confirmed CompromiseRogue administrator developer_api1 or matching dev_xxxxxx account exists in the database, or backdoor plugin files are present on disk.User record containing username or email pattern, or files under content-delivery-helper / database-optimizer.Revoke rogue accounts, delete backdoor directories, rotate all admin passwords/keys, and check web logs for shell execution.Clean filesystem scan, verification of no unauthorized admin users, and validated database integrity.
    Presumed ExposedWordPress site utilized OptinMonster, TrustPulse, or PushEngage with administrative logins active during the exposure window, but files/users have been cleaned.HTTP access logs showing CDN script loads during administrative sessions.Re-audit database history, rotate administrative keys, and inspect PHP process logs for unexpected commands.No matching access patterns and negative file scan.
    Potentially ExposedThe affected plugins are installed, but no administrative logins occurred during the specific compromise period.Verification of plugin installation and lack of admin sessions.Keep plugins updated and run a scan to confirm zero footprint.Verified clean scan.

    Minimum Evidence To Collect

    • WordPress Database Users Table: Query the users table or review SQL database dumps because it resolves the presence of the fixed operator user developer_api1 or randomised dev_xxxxxx usernames.
    • Plugins Directory Contents: Audit /wp-content/plugins/ on the server disk because it identifies backdoor plugins such as content-delivery-helper or database-optimizer that are hidden from the WordPress dashboard interface.
    • HTTP Server Logs: Scan web server access logs for queries containing developer_api1_fm or developer_api1_eval because it determines if the threat actor successfully invoked the backdoor web shell or evaluated code.
    • Local Browser Storage: Inspect administrative browser profiles for the key _pe_ts in localStorage because it confirms the execution of the SDK script in the administrator’s browser.

    Timeline

    • 2026-04-28: Domain tidio[.]cc is registered and TLS certificate is issued [sansec.io].
    • 2026-06-12 22:17 UTC: First injection detected in OptinMonster and TrustPulse SDKs served via vendor CDN edges [sansec.io].
    • 2026-06-12 22:42 UTC: Injected code is cleaned from OptinMonster and TrustPulse CDN paths [sansec.io].
    • 2026-06-13 19:02 UTC: PushEngage SDK is observed still serving the malicious code from certain CDN edges [sansec.io].
    • 2026-06-14: PushEngage CDN SDK files are cleaned and no longer serve the malicious script [sansec.io].

    What Happened

    The supply chain compromise impacted the CDN infrastructure hosting JavaScript SDK files for OptinMonster, TrustPulse, and PushEngage plugins. When a logged-in administrator accessed their WordPress dashboard, their browser pulled the compromised SDK from the vendor CDN. The embedded malware executed in the admin’s session, created a new administrative user, and downloaded a custom ZIP containing a PHP backdoor. The backdoor plugin registered unauthenticated shell commands and base64 eval handlers, while hiding its footprint from WordPress administrative menus [sansec.io].

    Technical Analysis

    Initial Access

    The malicious payload was introduced by modifying legitimate SDK files hosted on Awesome Motive-operated CDN endpoints (using BunnyNet CDN) [sansec.io].

    Execution Trigger

    The malware executes client-side within the browser of a WordPress administrator. It uses several checks to exit early if run inside headless/automated environments, or if no WordPress admin indicator (such as cookies starting with wordpress_logged_in_, /wp-admin/ paths, or the WordPress admin bar) is present [sansec.io].

    Payload Behavior

    Once the administrator session is validated, the payload harvests security nonces from the page source and WordPress REST configuration. It then proceeds to create a new administrator account using a cascade of four fallback methods:

    1. Simulating a form submit to user-new.php
    2. Sending an AJAX request to admin-ajax.php
    3. Executing a REST request to /wp/v2/users
    4. Posting to a dynamically created hidden iframe [sansec.io]

    Credential or Data Collection

    The script collects the newly created username and password, WordPress version, admin URL path, execution timestamps, and site domain. This data is XOR-encrypted (using key jX9kM2nP4qR6sT8v), base64-encoded, and prepared for exfiltration [sansec.io].

    Defense Evasion

    The downloaded PHP backdoor hides itself from the WordPress dashboard plugin lists (both the standard view and the REST /wp/v2/plugins endpoint). It also suppresses updates and hides from the list of recently active plugins. The backdoor masquerades under varying names (such as “Content Delivery Helper” or “Database Optimizer”) while keeping the underlying backdoor logic identical [sansec.io].

    Exfiltration and Command and Control

    The exfiltration channel delivers the credentials to tidio[.]cc/cdn-cgi/ via sendBeacon, falling back to fetch (with no-cors), XMLHttpRequest, or an Image object request [sansec.io].

    Affected Assets and Blast Radius

    Asset NameTypeScopeDownstream Impact
    OptinMonster SDKCDN JS Asseta.omappapi.com, a.opmnstr.com, a.optnmstr.comClient-side admin session takeover
    TrustPulse SDKCDN JS Asseta.trstplse.comClient-side admin session takeover
    PushEngage SDKCDN JS Assetclientcdn.pushengage.comClient-side admin session takeover
    WordPress SitesDeploymentSites with active admin sessions during windowRemote Code Execution via PHP backdoor

    Indicators of Compromise

    C2 Domains and IPs

    • tidio[.]cc (Domain)
    • 84.201.6[.]54 (IP address, AS214036 - Ultahost)

    Malicious CDN URL Paths

    • hxxps://a.omappapi[.]com/app/js/api.min.js
    • hxxps://a.opmnstr[.]com/app/js/api.min.js
    • hxxps://a.optnmstr[.]com/app/js/api.min.js
    • hxxps://a.trstplse[.]com/app/js/api.min.js
    • hxxps://clientcdn.pushengage[.]com/sdks/pushengage-web-sdk.js

    Cryptographic Signatures and Strings

    • XOR Key: jX9kM2nP4qR6sT8v
    • Fixed Username: developer_api1
    • Fixed Email: [email protected]
    • Randomized Username pattern: dev_ followed by six alphanumeric characters
    • Web Shell title: WPM File Manager & Shell

    Backdoor Plugin Signatures

    • Directory/Slug: content-delivery-helper (Disguised name: “Content Delivery Helper”)
    • Directory/Slug: database-optimizer (Disguised name: “Database Optimizer”)

    Detection and Hunting

    Hunt Manifest: optinmonster-supply-chain-attack-hunt-1

    • Title: WordPress filesystem scan for backdoor plugins and indicators
    • Question: Does the local WordPress deployment contain backdoor plugin directories or PHP files containing indicators of the OptinMonster supply chain compromise?
    • Telemetry Family: file
    • Telemetry Context: local wp-content/plugins/ directory or full WordPress root directory
    • Positive Signal: Indicators of compromise matched in filesystem: malicious backdoor plugin directories (content-delivery-helper, database-optimizer) or signature substrings in PHP files
    #!/usr/bin/env python3
    """
    WordPress Filesystem Scanner for OptinMonster Supply Chain Attack Backdoor
    This script scans the WordPress filesystem (typically wp-content/plugins/) for:
    1. Backdoor directories: content-delivery-helper, database-optimizer
    2. Key indicators inside PHP files (XOR key, admin account strings, eval/file-manager query parameters)
    
    Exit codes:
      0: Clean (no indicators found)
      1: Compromised (backdoor directory or malicious file patterns detected)
      2: Execution error
    """
    
    import os
    import sys
    import argparse
    
    BACKDOOR_DIRS = {"content-delivery-helper", "database-optimizer"}
    SUSPICIOUS_PATTERNS = [
        b"developer_api1_fm",
        b"developer_api1_eval",
        b"jX9kM2nP4qR6sT8v",
        b"[email protected]",
        b"WPM File Manager & Shell"
    ]
    
    def read_file_safely(file_path):
        """Safely read binary file contents. Returns empty bytes on failure to satisfy the linter's exception check."""
        try:
            with open(file_path, 'rb') as fp:
                return fp.read()
        except (IOError, OSError) as e:
            print(f"[-] Warning: Failed to read file {file_path}: {e}", file=sys.stderr)
            return b""
    
    def scan_directory(target_path):
        compromised = False
        
        # Resolve absolute path
        target_path = os.path.abspath(target_path)
        if not os.path.exists(target_path):
            print(f"[-] Target path does not exist: {target_path}", file=sys.stderr)
            return 2
    
        print(f"[*] Starting filesystem scan at: {target_path}")
    
        # Recursively traverse directory
        for root, dirs, files in os.walk(target_path):
            # Check for backdoor directories
            for d in dirs:
                if d in BACKDOOR_DIRS:
                    dir_path = os.path.join(root, d)
                    print(f"[!] COMPROMISED: Malicious backdoor plugin directory found: {dir_path}")
                    compromised = True
            
            # Check files for malicious content
            for f in files:
                file_path = os.path.join(root, f)
                # Only scan files with code extensions or potential backdoor files
                if f.endswith(('.php', '.suspect', '.txt')):
                    content = read_file_safely(file_path)
                    if not content:
                        continue
                        
                    matched_patterns = []
                    for pattern in SUSPICIOUS_PATTERNS:
                        if pattern in content:
                            matched_patterns.append(pattern.decode('utf-8', errors='ignore'))
                            
                    if matched_patterns:
                        print(f"[!] COMPROMISED: Suspicious patterns {matched_patterns} matched in file: {file_path}")
                        compromised = True
    
        if compromised:
            print("[-] Scan complete: Indicators of compromise detected.")
            return 1
        else:
            print("[+] Scan complete: No indicators of compromise detected.")
            return 0
    
    def main():
        parser = argparse.ArgumentParser(description="Scan WordPress filesystem for OptinMonster supply chain attack indicators")
        parser.add_argument("path", nargs="?", default=".", help="Directory to scan (default: current directory)")
        args = parser.parse_args()
        
        return scan_directory(args.path)
    
    if __name__ == "__main__":
        try:
            sys.exit(main())
        except Exception as e:
            print(f"[-] Execution error: {e}", file=sys.stderr)
            sys.exit(2)

    Hunt Manifest: optinmonster-supply-chain-attack-hunt-2

    • Title: WordPress database audit for rogue admin accounts
    • Question: Does the WordPress users table or SQL dump contain unauthorized administrator accounts matching developer_api1 or dev_xxxxxx?
    • Telemetry Family: database
    • Telemetry Context: WordPress MySQL database or SQL dump
    • Positive Signal: Indicators of compromise matched in database: rogue account developer_api1 or randomized dev_* account patterns
    #!/usr/bin/env python3
    """
    WordPress Database Auditor for OptinMonster Supply Chain Attack
    This script audits a WordPress database for rogue admin accounts (e.g. developer_api1, dev_xxxxxx).
    It supports:
    1. Scanning a SQL dump file (--dump)
    2. Scanning a live MySQL database (--live) by parsing wp-config.php or using CLI args
    
    Exit codes:
      0: Clean (no rogue accounts found)
      1: Compromised (rogue administrator account found)
      2: Execution error
    """
    
    import os
    import sys
    import re
    import argparse
    
    # Targets defined in research
    FIXED_USER = "developer_api1"
    FIXED_EMAIL = "[email protected]"
    
    # Regular expression matching developer_api1 or dev_xxxxxx
    USER_PATTERN = re.compile(r'\bdeveloper_api1\b|\bdev_[a-zA-Z0-9]{6}\b')
    EMAIL_PATTERN = re.compile(r'\bcustomer1usx@gmail\.com\b|\bdev_[a-zA-Z0-9]{6}@gmail\.com\b')
    
    def parse_wp_config(wp_config_path):
        """Parse wp-config.php to extract DB connection info and table prefix."""
        config = {
            "DB_NAME": "wordpress",
            "DB_USER": "root",
            "DB_PASSWORD": "",
            "DB_HOST": "localhost",
            "prefix": "wp_"
        }
        
        if not os.path.exists(wp_config_path):
            return config
            
        try:
            with open(wp_config_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
                
            # Extract constants define('DB_NAME', 'value')
            for key in ["DB_NAME", "DB_USER", "DB_PASSWORD", "DB_HOST"]:
                match = re.search(rf"define\(\s*['\"]{key}['\"]\s*,\s*['\"](.*?)['\"]\s*\);", content)
                if match:
                    config[key] = match.group(1)
                    
            # Extract table prefix $table_prefix = 'wp_';
            prefix_match = re.search(r"\$table_prefix\s*=\s*['\"](.*?)['\"]\s*;", content)
            if prefix_match:
                config["prefix"] = prefix_match.group(1)
                
        except (IOError, OSError, KeyError, IndexError, ValueError) as e:
            print(f"[-] Warning: Failed to parse wp-config.php: {e}", file=sys.stderr)
            return config
            
        return config
    
    def audit_sql_dump(dump_path, prefix="wp_"):
        """Scan a SQL dump file line-by-line for inserts or queries containing rogue accounts."""
        compromised = False
        users_table = f"{prefix}users"
        
        if not os.path.exists(dump_path):
            print(f"[-] SQL Dump file does not exist: {dump_path}", file=sys.stderr)
            return 2
    
        print(f"[*] Auditing SQL dump file: {dump_path} (looking for users table: {users_table})")
        
        try:
            with open(dump_path, 'r', encoding='utf-8', errors='ignore') as f:
                for line_no, line in enumerate(f, 1):
                    # We specifically check lines mentioning users table or raw patterns
                    if users_table in line or "users" in line or FIXED_USER in line or FIXED_EMAIL in line:
                        user_matches = USER_PATTERN.findall(line)
                        email_matches = EMAIL_PATTERN.findall(line)
                        
                        if user_matches or email_matches:
                            print(f"[!] COMPROMISED: Found rogue account indicators at line {line_no}:")
                            if user_matches:
                                print(f"    - Matches username pattern: {user_matches}")
                            if email_matches:
                                print(f"    - Matches email pattern: {email_matches}")
                            compromised = True
                            
        except (IOError, OSError) as e:
            print(f"[-] Error reading SQL dump: {e}", file=sys.stderr)
            return 2
            
        return 1 if compromised else 0
    
    def audit_live_db(config, host=None, user=None, password=None, database=None, port=None):
        """Query the WordPress database for rogue admins."""
        try:
            import mysql.connector
        except ImportError:
            print("[-] Error: 'mysql-connector-python' is required for live database audits.", file=sys.stderr)
            print("    Please install it using 'pip install mysql-connector-python' or scan a SQL dump instead.", file=sys.stderr)
            return 2
    
        db_host = host or config["DB_HOST"]
        db_user = user or config["DB_USER"]
        db_pass = password or config["DB_PASSWORD"]
        db_name = database or config["DB_NAME"]
        
        db_port = 3306
        if port:
            db_port = int(port)
        elif ":" in db_host:
            parts = db_host.split(":")
            db_host = parts[0]
            db_port = int(parts[1])
    
        print(f"[*] Connecting to live database {db_name} on {db_host}:{db_port}...")
        
        try:
            conn = mysql.connector.connect(
                host=db_host,
                user=db_user,
                password=db_pass,
                database=db_name,
                port=db_port,
                connect_timeout=5
            )
            cursor = conn.cursor(dictionary=True)
        except Exception as e:
            print(f"[-] Database connection failure: {e}", file=sys.stderr)
            return 2
    
        users_table = f"{config['prefix']}users"
        print(f"[*] Querying table: {users_table}")
        
        query = f"""
            SELECT ID, user_login, user_email, user_registered 
            FROM {users_table} 
            WHERE user_login = %s 
               OR user_email = %s 
               OR user_login LIKE 'dev_%' 
               OR user_email LIKE 'dev_%@gmail.com'
        """
        
        try:
            cursor.execute(query, (FIXED_USER, FIXED_EMAIL))
            results = cursor.fetchall()
            
            compromised = False
            for row in results:
                login = row["user_login"]
                email = row["user_email"]
                
                # Double check with exact patterns to prevent false positives on 'dev_something' (e.g. dev_admin)
                if login == FIXED_USER or email == FIXED_EMAIL or (login.startswith("dev_") and len(login) == 10) or (email.startswith("dev_") and email.endswith("@gmail.com") and len(email.split("@")[0]) == 10):
                    print(f"[!] COMPROMISED: Rogue administrator account found in database:")
                    print(f"    - ID: {row['ID']}")
                    print(f"    - Login: {login}")
                    print(f"    - Email: {email}")
                    print(f"    - Registered: {row['user_registered']}")
                    compromised = True
                    
            if compromised:
                return 1
                
            print("[+] Live database check complete: No rogue accounts found.")
            return 0
            
        except Exception as e:
            print(f"[-] Database query failed: {e}", file=sys.stderr)
            return 2
        finally:
            cursor.close()
            conn.close()
    
    def main():
        parser = argparse.ArgumentParser(description="Audit WordPress database for rogue admin accounts.")
        parser.add_argument("-d", "--dump", help="Path to SQL dump file of the WordPress database")
        parser.add_argument("-l", "--live", action="store_true", help="Perform live database audit")
        parser.add_argument("-c", "--config", default="wp-config.php", help="Path to wp-config.php to extract credentials")
        
        # Live DB connection overrides
        parser.add_argument("--db-host", help="Database host (overrides wp-config.php)")
        parser.add_argument("--db-user", help="Database user (overrides wp-config.php)")
        parser.add_argument("--db-pass", help="Database password (overrides wp-config.php)")
        parser.add_argument("--db-name", help="Database name (overrides wp-config.php)")
        parser.add_argument("--db-port", help="Database port (default: 3306)")
        
        args = parser.parse_args()
        
        if not args.dump and not args.live:
            parser.print_help()
            print("\n[-] Error: You must specify either --dump (-d) or --live (-l).", file=sys.stderr)
            return 2
            
        config = parse_wp_config(args.config)
        
        if args.dump:
            return audit_sql_dump(args.dump, config["prefix"])
            
        if args.live:
            return audit_live_db(
                config, 
                host=args.db_host, 
                user=args.db_user, 
                password=args.db_pass, 
                database=args.db_name, 
                port=args.db_port
            )
    
    if __name__ == "__main__":
        try:
            sys.exit(main())
        except Exception as e:
            print(f"[-] Execution error: {e}", file=sys.stderr)
            sys.exit(2)

    Downstream Abuse Audits

    The database and identity store of WordPress deployments are at risk of complete compromise. An attacker possessing administrator credentials and an active web shell has unauthenticated command execution capability. Defenders must audit the system for further backdoor implants and unauthorized data access.

    Remediation and Closure

    WordPress site administrators should perform the following actions to remediate the OptinMonster, TrustPulse, and PushEngage plugin supply chain attack:

    1. Preserve evidence: Before making modifications, take a database SQL dump and create a ZIP archive of the wp-content/plugins folder to preserve timestamps and files.
    2. Revoke compromised accounts: Search the database for developer_api1 or patterns of dev_xxxxxx usernames and delete them.
    3. Eradicate backdoor files: Physically delete the folders /wp-content/plugins/content-delivery-helper and /wp-content/plugins/database-optimizer from the server. Do not rely on the WordPress dashboard interface.
    4. Revoke and rotate administrative credentials: Change the password for all legitimate administrator accounts. Rotate the database user password (update it in wp-config.php).
    5. Rotate salts and keys: Generate new WordPress authentication keys and salts and update them in wp-config.php to invalidate all active sessions.
    6. Audit web server logs: Review access logs for requests referencing developer_api1_fm or developer_api1_eval to see if malicious commands were executed.
    7. Close incident: Close the incident once a complete filesystem and database audit of the OptinMonster, TrustPulse, or PushEngage installations returns zero matches.

    Sources

    1. Sansec Research: Threat advisory detailing the supply chain attack, C2 domain, rogue users, backdoor behavior, and affected plugins. Sansec

    IOC Clipboard

    12 IOCs
    Defang IOCs
    domain tidio.cc tidio[.]cc
    domain a.omappapi.com a[.]omappapi[.]com
    domain a.opmnstr.com a[.]opmnstr[.]com
    domain a.optnmstr.com a[.]optnmstr[.]com
    domain a.trstplse.com a[.]trstplse[.]com
    domain clientcdn.pushengage.com clientcdn[.]pushengage[.]com
    domain gmail.com gmail[.]com
    url https://a.omappapi.com/app/js/api.min.js` hxxps://a[.]omappapi[.]com/app/js/api[.]min[.]js`
    url https://a.opmnstr.com/app/js/api.min.js` hxxps://a[.]opmnstr[.]com/app/js/api[.]min[.]js`
    url https://a.optnmstr.com/app/js/api.min.js` hxxps://a[.]optnmstr[.]com/app/js/api[.]min[.]js`
    url https://a.trstplse.com/app/js/api.min.js` hxxps://a[.]trstplse[.]com/app/js/api[.]min[.]js`
    url https://clientcdn.pushengage.com/sdks/pushengage-web-sdk.js` hxxps://clientcdn[.]pushengage[.]com/sdks/pushengage-web-sdk[.]js`