Peter Davies

Ramblings

Anatomy of a compromise: the JavaScript injection that kept coming back

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.

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:

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 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.

phpmalwarefinder /var/www/example.com/

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

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:

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

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

What would have prevented it

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