Peter Davies

Ramblings

Anatomy of a compromise: a Magento skimmer hiding behind an image URL

Most card skimming on Magento in 2019 was JavaScript in the checkout page, the Magecart style. This one was a few lines of PHP on the server instead. That makes it invisible to anything that only looks at what the browser receives, and very easy to miss in a code review if you are not looking for it.

What it did

On every request, if there was any POST data at all, it:

  1. Flattened the whole $_POST array into a key=value&key=value string, walking into nested arrays. Magento posts card fields as payment[cc_number], payment[cc_cid] and so on, so nested arrays were the point.
  2. Gave the visitor a random id in a cookie, if they did not have one already, valid for about a day. This let the operator stitch together the separate posts from one visitor: login, billing address, then payment.
  3. Base64-encoded the string and sent it, with the shop's host name and the visitor's id, as a POST to a remote URL ending in .jpg.

The shape of it, defanged:

if (count($_POST) >= 1) {
    $data = flatten_post('', $_POST, 0);
    // set or reuse a visitor id cookie, then POST base64($data) to hxxps://<remote-host>/<logo>.jpg
}

Because it caught every form, it collected customer logins and addresses as well as card details on any page that took them.

Why the image name

The exfiltration went out with file_get_contents() and a stream_context_create() POST context, so there was no curl call to grep for. And if anyone did glance at outbound traffic or a proxy log, a request for something like a payment provider's logo .jpg looks harmless. The remote "image" was a script that took the POST body.

Sucuri wrote up a JavaScript variant of the same idea in 2017, also sending data to a URL dressed as a JPG.

Finding code like this

It is plain PHP with no obfuscation, which is exactly why signature scanners can miss it. Look for the combination of things it has to do.

Files that both read $_POST and build an outbound HTTP request:

cd /var/www/example.com
grep -rlE --include='*.php' --include='*.phtml' 'stream_context_create|http_build_query|curl_init' . \
  | xargs grep -l '\$_POST'

On Magento that gives a manageable list; most of core and the payment modules use the request object rather than $_POST directly, so anything in a template, a theme or index.php that touches $_POST deserves a look.

Files that set cookies with names nothing in your code uses:

grep -rn --include='*.php' --include='*.phtml' 'setcookie(' . | grep -v '/lib/'

Files changed recently, bearing in mind that timestamps can be faked with touch:

find . -type f \( -name '*.php' -o -name '*.phtml' \) -mtime -30 -not -path './var/*'

And compare core against a clean copy of the same Magento release, as in the other parts of this series. Any core, library or index.php file that differs needs reading.

Clean-up

What would have prevented it

Other parts