---
title: "Anatomy of a compromise: a PHP link-injection backdoor"
date: "2015-09-04"
path: "/178-anatomy-of-a-compromise-php-link-injection-backdoor.html"
section: "/ramblings"
description: "How a SAPE link-selling client was hidden inside a CMS bootstrap file, how it decided who saw the links, and how to find and remove one."
---

This is the first of a short series on real compromises on sites I looked after. The sites are anonymised; the techniques are not. This one is from 2015: an old Joomla site, several years behind on updates, that had been quietly turned into a paid link farm.

## What it was

SAPE is a Russian link-selling network. A site owner installs their PHP client, the network sells links "on" that site to SEO customers, and the client fetches the current list of links from the network's dispenser servers and prints them into the page. Used as intended, it is a dodgy but voluntary arrangement. Used by an attacker, it is someone else selling links on your site.

In this case the whole client library had been pasted into the CMS's core bootstrap file, the one every page request includes before anything else. That meant:

- No new files in the web root to notice. The file name and location were exactly what you would expect.
- It ran on every page, regardless of template or component.
- It survived template changes and most "clean-up" passes that only look at themes and uploads.

Alongside the library was a small extra function, called from the site footer, that decided whether to print the links and then did so:

```php
function josfooter(){
    error_reporting(0);
    // ... visitor checks ...
    $sape = new SAPE_client($o);
    echo $sape->return_links();
}
```

## How it chose who saw the links

The point of link injection is that search engines see the links and the site owner does not. The added function did three things before printing anything:

1. It skipped its checks entirely for two hard-coded IP addresses and for a browser carrying a particular cookie. Those were the operator's own ways of always seeing the real output.
2. It built a list of "bot" signatures: IP ranges belonging to the main search engines of the day, user agents containing words like `bot`, `crawler`, `spider`, `curl` and `PHP`, and visitors whose referrer mentioned the link network or whose browser language was Russian.
3. Depending on which group the visitor fell into, it wrapped the output differently.

By the time I had the file the branches of that last check were mostly empty, so the cloaking was half-dismantled, but the intent is clear from what is left. The links went into the footer inside a wrapper, and the site owner, browsing normally, was never meant to notice them.

## The cache in /tmp

The client caches the link list locally so it does not call home on every request. The stock client writes a `.links.db` file next to itself, which is easy to spot. This copy had been edited to write its cache to `/tmp` under a name made to look like a PHP session file: `sess_` followed by a long random-looking string. Anyone glancing at `/tmp` on a PHP server sees hundreds of those and moves on.

That is worth remembering as an indicator. A real session file is small, short-lived and full of serialised PHP. A cache file is refreshed on a timer, never expires and contains link HTML.

## Finding it

A few checks catch this kind of kit quickly.

Search the code for the client's class names and constant. The kit's names are public and rarely changed:

```bash
grep -rl --include='*.php' -e 'SAPE_client' -e '_SAPE_USER' /var/www/example.com/
```

Fetch the page as a search engine and as a browser and compare the two:

```bash
curl -s -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' https://www.example.com/ > as-bot.html
curl -s -A 'Mozilla/5.0 (Windows NT 6.1; rv:40.0) Gecko/20100101 Firefox/40.0' https://www.example.com/ > as-browser.html
diff as-bot.html as-browser.html
```

This only catches user-agent cloaking; anything keyed on search engine IP ranges will look the same from your machine. Search Console's "Fetch as Google" is the way to see what the crawler really gets.

Look for session-named files that are not sessions:

```bash
find /tmp -maxdepth 1 -name 'sess_*' -mtime +1 -exec grep -l '<a ' {} +
```

Compare the core against a clean copy of the same release. Download the exact version you run from the vendor, unpack it somewhere else and diff it:

```bash
diff -rq /var/www/example.com/ /tmp/clean-release/ | grep -v -e '/images/' -e '/cache/'
```

Any core file that differs is either a patch you applied and should have recorded, or something you need to read.

## Using the indicators in the code

Malware that hard-codes addresses hands you its own indicators. This client had the operator's two "always show me the real thing" IPs, plus five addresses for the link network's checkers. I pulled them out and searched every rotated access log for them, to see when they first turned up and what else they had asked for:

```bash
cd /var/log/apache2
for ip in 192.0.2.17 198.51.100.42 203.0.113.9; do
    echo "== $ip"
    zgrep -h "^$ip " access.log* | awk '{print $4, $6, $7, $9}' | sort | head -20
done
```

`zgrep` reads both the compressed and current logs, so you do not need separate `zcat` and `grep` passes. The first appearance of the operator's address in the logs is usually close to the date the code went in, and the requests either side of it are where the way in tends to show.

## Clean-up

- Take a copy of the infected site and logs before changing anything.
- Replace the core with a clean copy of the same release rather than trying to cut the injected code out by hand. Then update to a supported release.
- Delete the cache file in `/tmp`.
- Check the template and every installed extension against clean copies too; a second copy of the same function in a template is common.
- Change the CMS admin passwords, the database password and any FTP or hosting control panel passwords, because you do not know which one was used.

## What would have prevented it

- Keeping the CMS and its extensions up to date. Most of these kits went in through known, long-patched holes in old installs.
- Code files not writable by the web server user. If PHP cannot write to the bootstrap file, a vulnerable form cannot either.
- Some form of file integrity check: a nightly `diff` against a known-good copy, or keeping the site in version control and running `git status` on the server from cron and mailing any changes.
- Looking at the site the way a search engine does every so often.

Today: the kit's code relies on `eregi()`, which was removed in PHP 7, so this particular copy would fail outright on a current PHP. The approach has not gone anywhere.

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