---
title: "Anatomy of a compromise: the JavaScript injection that kept coming back"
date: "2020-01-30"
path: "/200-anatomy-of-a-compromise-the-javascript-injection-that-kept-coming-back.html"
section: "/ramblings"
description: "A fake card form injected into a Magento 1 checkout four times in four months, why removing the script never fixed it, and how the PHP that put it back was found."
---

This one ran from the autumn of 2019 into January 2020 on a Magento 1 shop. A skimmer turned up in the checkout, was removed, and came back. Four times. The lesson is in why.

## What the injection did

The script was a block of obfuscated JavaScript in the page. The obfuscation was the usual kind: every string pulled out into an array of hex escapes and referenced by index.

```javascript
var _0xabcd=["\x73\x77\x69\x74\x63\x68\x4D\x65\x74\x68\x6F\x64","\x64\x69\x73\x61\x62\x6C\x65\x64", ...];
```

Decoded, it did the following on the checkout payment step:

1. Waited for the payment methods section to load.
2. Added a new payment option labelled "Pay by Credit Card / Debit Card" at the top of the list, styled to match.
3. When a shopper picked it, hid the real payment form and showed a fake one: card type, number, expiry month and year, and security code. The field ids were copied from a standard Magento card payment module, so the fields picked up the theme's normal styling.
4. On every change to those fields, saved the values into a cookie.
5. On submit, read the cookie, hex-encoded it and sent it to a remote server by creating an image element whose `src` was a script on that server, with the card data in a query parameter dressed up as a discount code. Then cleared the cookie.

From the shopper's side it was just another payment option on a checkout that looked normal.

## Why removing it did not work

Each time the script was found, it was taken out of the page. Each time, a few weeks later, it was back.

Comparing the copies I kept from each round, the wording of the fake label changed slightly and some helper functions came and went, but the collection server was the same every time. Same operator, coming back in the same way, not a new attacker.

The script itself was only the payload. Something with write access to the site was putting it back, which meant PHP on the server. Cleaning the visible JavaScript was dealing with the symptom.

## Finding the persistence

By the fourth round the injected code pulled in a separate `.js` file, so I searched every PHP file for the name of that file:

```bash
cd /var/www/example.com
find ./ -name "*.php" -type f -print0 | xargs -0 grep -l "name-of-the-script.js"
```

Then I ran [php-malware-finder](https://github.com/jvoisin/php-malware-finder) over the whole code base. It is a set of YARA rules for suspicious PHP: obfuscation, `eval` of decoded strings, dangerous functions fed from request data, non-printable characters.

```bash
phpmalwarefinder /var/www/example.com/
```

Output is one line per hit, the rule name then the file:

```text
DodgyPhp /var/www/example.com/cron.php
DodgyPhp /var/www/example.com/lib/Varien/Autoload.php
ObfuscatedPhp /var/www/example.com/app/code/core/Mage/Core/Model/Translate.php
ObfuscatedPhp /var/www/example.com/app/design/frontend/<theme>/default/template/<module>/<file>.phtml
NonPrintableChars /var/www/example.com/app/code/local/<vendor>/<module>/sql/<setup>/mysql4-install-1.0.php
```

It flagged seventeen files. The rules are deliberately noisy, so some of those will be false positives, but look at where the hits were:

- **The autoloaders.** Three autoloader files were flagged, including `lib/Varien/Autoload.php`. An autoloader runs on every single request before anything else. Code hidden there can check whether the skimmer is still in place and put it back.
- **`cron.php`.** Magento's cron entry point runs every few minutes from the system crontab. Another way to put things back on a timer, and nobody reads `cron.php`.
- **Core models** that load on most pages, such as the translation model.
- **Theme templates** and **third-party theme modules**, including install scripts that nobody ever opens again.
- **A custom module's controller**, which is the shop's own code and the last place you would think to look.

That is the persistence. Each of those files was a way back in or a way to rewrite the page. Remove the script and leave any one of them, and it returns.

## Clean-up that holds

- Take copies of the code, database and logs first.
- Replace all of Magento core (`app/code/core`, `lib`, `js`, the root PHP files) with a clean copy of exactly the same release, plus the same patches. Do not try to clean core files by hand.
- Replace the theme and third-party modules with clean copies from the vendor.
- For the shop's own code, compare against version control. If there is no version control, read every file.
- Check the database too. Magento 1 skimmers also hide in `core_config_data` (the design head and footer settings) and in CMS blocks:

```sql
SELECT config_id, path FROM core_config_data WHERE value LIKE '%<script%';
SELECT block_id, identifier FROM cms_block WHERE content LIKE '%<script%';
```

- Check the admin users table for accounts you do not recognise.
- Rotate everything: admin passwords, the database password, FTP and SSH credentials, API users and keys.
- Then run the scanner again and get a clean result before calling it done.

## What would have prevented it

- Applying the security patches as they came out. Most Magento 1 compromises went in through holes that already had a patch.
- Code that the web server user cannot write to. If PHP cannot modify `lib/Varien/Autoload.php`, a webshell running as PHP cannot either.
- File integrity monitoring: code in Git, and a scheduled `git status` on the server that alerts on any change.
- Payment through a hosted page or hosted fields, so a fake form in your checkout has nothing real to imitate.
- A Content Security Policy that limits where scripts can load from and where images can be requested from. Report-only mode at least tells you something is calling out.

Today: Magento 1 went end of life in June 2020 and gets no more security patches from Adobe. A shop that keeps getting reinfected on Magento 1 needs a migration plan, not another clean-up.

## 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)
