Have you ever had one of those feelings that something is wrong, but you can’t quite put your finger on it?
This was the feeling for a client of mine; they knew something was wrong on their site but couldn’t quite pinpoint the problem.
They worked with their web agency, ensuring all their plugins were up to date, setting up a security plugin, running malware scans and making test transactions; everything came back fine.
But it wasn’t fine. In fact, their site had been compromised, and this is how we figured it out.
My name is Tim, and I’m a WordPress security consultant; I help my clients keep their sites and site visitors safe.
On being asked to look into things by my client, there were immediate alarm bells:
- Some of their email was bouncing, and they were getting spam reports; they had been placed on at least one email blocklist.
- Sales had dropped, and while this was initially thought to be due to the change in payment gateways, something still felt low, as the number of visitors going into the sale flow was similar.
- At least one customer had directly complained that somebody had stolen their card details after making a purchase.
With this and the gut feeling that something was wrong, the client asked me to complete a website security review. This service combines automated testing, manual review and log analysis. I came in with the assumption that something needed to be corrected, and while writing the review, I was looking for issues.
From the start, there were curious issues; one that jumped out was a file in the root of the site index.py. Python isn’t normally used as a CGI script, so this naming was a little strange, but the file itself is empty. On a hunch, I looked at the htaccess file, but nothing out of the ordinary appeared there.
wp-cli verify checksum commands for both WordPress core and plugins did not return anything of note. So I continued the review process, which included grabbing plugin and theme information. A couple of things stood out, WP SMTP Pro being the big one as we know the domain is being blocked for spam by some servers; the presence of WP SMTP Pro might mean the email credentials could have been gained this way if the site had been compromised.
All the plugins were up to date; someone may have done the updates before giving me access, much like the people who tidy and clean for their cleaner.
Looking through the access logs, they are quite noisy, but it’s clear there are several attempts to hit certain endpoints that look malicious; this could be a bot just probing, but it looked more like there was or had been a valid endpoint. Unhelpfully, it was hitting index.php with a bunch of parameters; none appeared to be hitting our strange index.py.
Firing a curl session and hitting the endpoint resulted in nothing. I honestly didn’t expect there to be anything, but it was worth trying. So I then did what everyone else would do, I googled it, and nothing came back for those specific endpoints.
At this stage, we are exceedingly lucky; it’s pure chance I happen to be working on this, but there it is, and sure enough, the file appears to be some sort of card skimmer.
A card skimmer takes a copy of the card details the visitor puts into the checkout form and sends/stores the data for the bad actor to harvest. In this instance, it was storing those details as an image file in the wp-content/uploads folder.
So now we can confirm the customer’s original comment that they were a victim of card fraud post visiting the site; we can also explain the drop in sales, as several anti-malware tools will be flagging the page as unsafe.
However, we have many unanswered questions, not least what triggered its appearance.
The card skimmer file is loading from the site’s root, SSH’ing into the server. There it is, a new file, created 7 minutes earlier; something else caught my eye, the index.py file now was not empty. Inside was Python code.
This code appears to be a mini command and control script. It trawls through the wp-content/themes folder and adds to each function.php a WordPress enqueue function to add the card skimmer code. A separate function removes the file and the enqueue function from the theme’s functions’ files before wiping down the Python file’s contents.
This is quite neat yet odd; the person had enough knowledge to put a card skimmer for a very specific payment gateway on, yet not the knowledge to know what theme the site had active. Does this point to a script kiddie or someone hacking together multiple exploits, maybe?
Each stage has, so far, led to more questions; the obvious one is what prompted this file to be populated and triggered.
This is where our luck partially ran out; having read the code, I had a good idea that, at some point, this file would most likely vanish, and I needed to preserve both this and the JS file. Yet as I exited my text editor and instantly regretted not saving the output buffer, I realised the files were gone. Clearly, the automated task to remove them had run.
The files had to be generated by some task; nothing in the access logs had pointed to a remote request, so let’s assume it’s some automated task, which led us to the cron.
Linux system cron is a mechanism to run scheduled tasks, it defines tasks per user, and they can run on a range of schedules. Sure enough, the www user had three separate cronjobs.
The first made a remote request to a URL on a remote system that used the format index.php and a random set of letters, and then something that clearly looked like a key. This is similar to the API hits we could see in our own access logs. Could it be that this server was, at one point, acting as a code repository for the malware? The returned request was piped in to the index.py file.
The second cron ran the index.py file, and the third ran a short period of time later with an additional argument to trigger the removal.
Ok, so taking the URL in the cron, and hitting it with curl, returned nothing.
Nothing came back.
Ever again, the next cron ran, and the file wasn’t populated; the skimmer never came back. It just stopped. It could be a coincidence that I hit the endpoint, and it all suddenly stops but what a hell of a coincidence.
Hoping to uncover more information, I carry on with the review. Grabbing a list of all the admin users using WP-CLI, I notice one, in particular, stands out, bushra.redacted, it wasn’t the username itself but the format, as that is the format of a WooCommerce customer while the other admins were using their first names. What also stood out is this client had two roles, customer and administrator.
By default, the WordPress admin area doesn’t show multiple roles very well, and the knowledge a user can have multiple roles is not that common. Looking at the date, they appear to have been created in early April.
We may have our bad actor.
A case of incredibly bad luck
The following is slightly hypothetical, and I cannot prove this is the case, but it fits with what evidence we have. My client may qualify for some of the unluckiest folks around.
Back in the first quarter of the year, they were looking to move away from their payment provider WooCommerce Payments, to Authorize.net. They performed the move in very early April.
In late March, WooCommerce Payments fixed a severe vulnerability that allowed anyone to set a header in their HTTP request and pretend to be a given user. This vulnerability was incredibly serious and was quickly exploited by bad actors.
I believe that the client was caught up in this vulnerability mid-changing payment gateways, that our bad actor managed to upgrade an account they created (or simply used an existing one and modified the details), and gained enough access to create a shell, which allowed adding the Python and the cron job.
I think, initially, the bad actor used the Python script to extract data and information, including the SMTP details, or maybe used it as a direct spam relay. It also may have briefly been used as a code repository for the malware. So other sites would be making curl requests from cronjobs to it.
The client changed their payment gateway, specifically to one vulnerable to card skimmers, this was detected, and the payload was changed.
Why is this interesting?
This might have been one of those incredibly rare cases of a manual hack, or at least the bad actor having some manual involvement. The ad-hoc nature of the scripts, the pivoting of the attack, and the fact things were being covered up to avoid detection.
Yet it doesn’t feel targeted; this wasn’t someone out to ruin the client, a dry run before large-scale deployment, perhaps?
The most interesting thing is that the remote URL suddenly stopped working. Did I trigger a self-destruct? Reaching out to the owners of the hacked site, they claim, and I have no reason to doubt them, that nothing was there anymore. Did the bad actor clean up, or was that automated?
So a report is written, recommendations are plenty, a full site clean up to begin and breach notifications to send. While obviously not thrilled, the client can let go of that nagging feeling.
This could be you; I don’t want to hard-sell you after a scary story. But most of my clients have a website security review after they’ve had a scare, but it doesn’t have to be that way. Let’s get ahead, and why don’t I help you cast an eye over your WordPress site? For details and to book a call with me, see the Website Security Review page.
Cover Photo by Max Bender