---
title: "Anatomy of a compromise: a Magento skimmer hiding behind an image URL"
date: "2019-03-15"
path: "/193-anatomy-of-a-compromise-magento-image-post-backdoor.html"
section: "/ramblings"
description: "How a few lines of server-side PHP copied every form post on a Magento shop to a remote URL dressed up as an image, and how to find code like it."
---

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:

```php
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](https://blog.sucuri.net/2017/04/ecommerce-security-customer-data-breaches-using-images.html) 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:

```bash
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:

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

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

```bash
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

- Remove the code and replace any core file it lived in with a clean copy.
- Look for the way in. Code like this does not write itself; there will be a webshell, a stolen admin or FTP login, or an unpatched hole.
- Assume every customer who submitted a form while it was live is affected, and treat it as a card data breach: tell your acquirer or payment provider, and get advice on notification.
- Rotate admin, database, FTP and SSH credentials.

## What would have prevented it

- Patching. Magento shops that were behind on security patches were being broken into in bulk.
- Card fields that never touch your server. A hosted payment page or the provider's hosted fields means there is no card number in `$_POST` to steal.
- Restricting outbound connections from the web server. A shop needs to talk to its payment gateway, its mail relay and not much else. With a default-deny outbound rule, the POST to the remote host would have failed.
- If nothing in the shop fetches remote URLs with PHP's file functions, set `allow_url_fopen = Off`. It would not stop a skimmer that uses `curl`, but it closes the route this one used.
- File integrity monitoring on the code base, so a changed core or template file raises an alert the same day.

## Other parts

- [A PHP link-injection backdoor](/178-anatomy-of-a-compromise-php-link-injection-backdoor.html)
- [Forensics on a breached shop server](/190-anatomy-of-a-compromise-shop-server-forensics.html)
- [Coin miners: the probe that missed](/192-anatomy-of-a-compromise-thinkphp-exploit-coin-miner.html)
- [A Magento skimmer hiding behind an image URL](/193-anatomy-of-a-compromise-magento-image-post-backdoor.html)
- [The JavaScript injection that kept coming back](/200-anatomy-of-a-compromise-the-javascript-injection-that-kept-coming-back.html)
- [A wp-login brute force on Elastic Beanstalk](/203-anatomy-of-a-compromise-a-wp-login-flood.html)
