Guide · Drop-in backdoors

A drop-in loads before every plugin, needs no activation, and is easy to overlook.

WordPress loads a handful of specially named files straight from wp-content — advanced-cache.php, object-cache.php, db.php and a few others — before any plugin runs. Caching and database plugins use them legitimately, which is exactly why a backdoor planted under one of those names gets left alone. This guide covers which drop-ins exist, when each one loads, how to tell one written by a real plugin from a loader, and how to remove a rogue one without breaking the site.

Symptom
An advanced-cache.php, object-cache.php or db.php nobody can account for
Mechanism
A specially named file in wp-content that WordPress loads on its own
Visibility
Absent from the normal plugin list; shown only on the Drop-ins tab
Complication
Caching and database plugins write the same files legitimately

How drop-ins work

What a WordPress drop-in is, and why attackers like the names

A drop-in is a file with a reserved name, placed directly in wp-content, that WordPress loads by itself to replace or extend part of its own behaviour. There is no activation step and no setting in the admin area. If the file exists — and, for advanced-cache.php, if the WP_CACHE constant is true — it runs on every request, before must-use plugins, ordinary plugins and the theme.

The legitimate uses are common. Page-caching plugins write advanced-cache.php so a cached page can be served before most of WordPress loads; object-cache plugins write object-cache.php to connect WordPress to Redis or Memcached; database plugins, and debugging tools such as Query Monitor, use db.php. That familiarity is the attraction: a file called object-cache.php reads as infrastructure, and most people who find one assume a plugin put it there.

Drop-ins do not appear in the ordinary list of plugins. WordPress shows a separate Drop-ins tab on the Plugins screen when any exist, which most owners never open. And because a drop-in loads before every security plugin, a loader there runs before anything that might notice it.

Diagram · Where drop-ins and must-use plugins sit

Drop-ins live loose in wp-content, by exact name. Nothing activates them.

wp-content/in the order WordPress reaches each part on a request

  • advanced-cache.phpDrop-in: loaded first when WP_CACHE is true in wp-config.php
  • db.phpDrop-in: replaces the database class whenever the file exists
  • object-cache.phpDrop-in: replaces the object cache whenever the file exists
  • index.phpThe empty “Silence is golden” file, not a drop-in
  • mu-plugins/Must-use plugins: every PHP file directly inside loads, no activation
  • plugins/Ordinary plugins: only those in the active_plugins option load
  • themes/The active theme’s functions.php loads after all of the above
  • uploads/Media; PHP here is never loaded by WordPress itself
  • Loads before any plugin, without being activated
A simplified wp-content directory. Only files with these exact names, directly in wp-content, are drop-ins; files in mu-plugins load next; ordinary plugins load only if they are active.

The full list

Which file names are drop-ins, and when each one loads

Only these exact names, directly in wp-content, are treated as drop-ins. A file such as object-cache-old.php is not loaded unless something includes it.

Figure 1 · The drop-ins, and what makes one suspicious

Every drop-in has a legitimate owner. A loader under the same name has none.

Drop-inLoads whenNormally written bySuspicious if
advanced-cache.phpWP_CACHE is trueA page-caching pluginNo caching plugin installed
object-cache.phpThe file existsAn object-cache plugin or hostA few lines, encoded strings
db.phpThe file existsA database plugin, Query MonitorIncludes a file outside a plugin
maintenance.phpMaintenance modeA custom maintenance pageAny code beyond a message
sunrise.phpMultisite, SUNRISE setDomain-mapping codePresent on a single site
When each drop-in loads, what normally writes it, and what should make you look twice. All of them load before every ordinary plugin.

advanced-cache.php

Loaded very early when WP_CACHE is defined as true in wp-config.php. A WP_CACHE line with no caching plugin installed is a reason to look at the file.

object-cache.php

Loaded whenever the file exists, to replace WordPress’s object cache. No constant is needed, so the file alone is enough.

db.php

Loaded whenever the file exists, to replace the database class. Legitimately written by database plugins and by Query Monitor, which installs it as a symbolic link to its own folder.

maintenance.php, db-error.php, php-error.php

Custom pages shown during maintenance mode, a database connection failure, or a fatal error. They should contain a message and little else.

fatal-error-handler.php

Replaces WordPress’s fatal error handler. Rarely used legitimately; any one you did not install deserves a close look.

sunrise.php and the blog-*.php files

Multisite only. sunrise.php loads when SUNRISE is defined, usually for domain mapping; blog-deleted.php, blog-inactive.php and blog-suspended.php are status pages. On a single site they should not exist.

Where to look

How to check every drop-in without running it

Work from the WordPress root over SSH or a file manager with hidden files shown. Every check here only reads.

  1. 01

    List every PHP file directly in wp-content

    With full timestamps, so a file written during the incident stands out from the rest:

    shell
    ls -la --time-style=full-iso wp-content/*.php

    index.php is normal. Anything else is a drop-in candidate; note its owner, size and modification time.

  2. 02

    Check whether page caching is switched on

    advanced-cache.php only loads when WP_CACHE is true:

    shell
    grep -n 'WP_CACHE' wp-config.php

    WP_CACHE true with no page-caching plugin installed means something set it — and a caching plugin you removed long ago can leave both the constant and the file behind.

  3. 03

    Read the header of each drop-in

    A drop-in written by a plugin names that plugin in its opening comment and usually loads code from the plugin’s own folder:

    shell
    head -n 25 wp-content/advanced-cache.php wp-content/object-cache.php wp-content/db.php 2>/dev/null

    Check that the plugin it names is actually installed in wp-content/plugins. A drop-in that names no plugin, or a plugin that is not there, has no owner.

  4. 04

    Search the drop-ins for loader patterns

    Encoding, indirect execution and request parameters that should never appear in a cache or database layer:

    shell
    grep -nE 'eval[[:space:]]*\(|base64_decode|gzinflate|str_rot13|assert[[:space:]]*\(|create_function|\$_(POST|GET|REQUEST|COOKIE)' wp-content/*.php

    A caching drop-in may legitimately read cookies to decide whether to serve a cached page. Encoded strings and eval have no such explanation.

  5. 05

    Look for a symbolic link

    Query Monitor’s db.php, and some caching setups, are symbolic links into a plugin folder rather than copies. ls -la shows the target after an arrow; a link into a plugin you can name is explainable, a link anywhere else is not.

Real or rogue

How to tell a caching drop-in from a backdoor

It has an owner you can name

A legitimate drop-in identifies the plugin that wrote it, and that plugin is installed and configured. Its settings screen usually reports the drop-in’s status, and offers to rewrite it.

It is the size of real code

Caching and database drop-ins are hundreds or thousands of lines of readable PHP. A file of a few lines, or one long line of encoded text, is not doing caching.

It loads from the plugin, not from elsewhere

Includes that point into the owning plugin’s folder are normal. Includes of files in uploads, cache folders, hidden files, or paths written as escape sequences are not.

Its timestamp fits its story

A drop-in written when the caching plugin was installed or updated is plausible. One written inside the incident window, on a site where nobody touched caching, is part of the incident.

Removing it

Removing a rogue drop-in without breaking the site

WordPress falls back to its own behaviour when an object-cache.php or advanced-cache.php disappears, so removing a rogue one rarely breaks anything. db.php is the exception worth checking first.

  1. 01

    Preserve the file first

    Copy every drop-in you are about to change, with its timestamps, outside the web root, and hash it:

    shell
    mkdir -p ~/incident
    cp -p wp-content/*.php ~/incident/
    sha256sum ~/incident/*.php

    The modification times bound the window everything else in the account gets checked against.

  2. 02

    Remove the rogue file

    Delete the drop-in that no installed plugin owns. If it was advanced-cache.php and no page-caching plugin is installed, also set WP_CACHE back to false, or remove the line, in wp-config.php.

  3. 03

    Let a legitimate plugin rewrite its own drop-in

    If the name belongs to a caching or database plugin you do use but the content was replaced, delete the file and let the plugin regenerate it from its own settings screen, or deactivate and reactivate the plugin — never edit the drop-in back by hand.

  4. 04

    Check db.php before deleting it

    A db.php from a multi-database plugin is load-bearing: removing it can take the site offline if the setup depends on it. Confirm what the plugin expects before deleting a db.php that turns out to be legitimate.

  5. 05

    Find what wrote it, then rotate

    Something had write access to wp-content. Look for the other layers below, then rotate the control panel, SFTP, database and WordPress administrator credentials, and the keys and salts in wp-config.php.

  6. 06

    Re-check after a few days

    List wp-content/*.php again and compare against the preserved copy. A drop-in that reappears means a persistence layer is still writing it.

What else to check

A drop-in loader rarely travels alone

A drop-in is one way to run code early without being activated. The same infection usually has others, and any one of them can write the drop-in back. In the ClickFix incident documented on this site, the chain lived in four layers at once: obfuscated PHP files, base64 content in the options table, the active theme’s functions.php, and a .user.ini startup directive.

Must-use plugins

Every PHP file directly in wp-content/mu-plugins loads without activation, just after the drop-ins.

Includes in core files

An escaped @include at the top of index.php or wp-config.php is the same idea one level up.

PHP startup directives

auto_prepend_file in .user.ini runs a file before WordPress, and before any drop-in.

Scheduled tasks

A WP-Cron event or system cron job that re-fetches the payload will rewrite a deleted drop-in.

Common questions

Common questions about drop-in backdoors

I found an object-cache.php I don’t remember adding. Is my site hacked?

Not necessarily. Object-cache plugins and many hosts write this file to connect WordPress to Redis or Memcached. Read its header: a legitimate one names the plugin or host that wrote it and is substantial, readable code. If it names nothing, is a few lines long, or contains encoded strings or eval, treat it as a loader.

Why doesn’t my security plugin flag it?

A drop-in loads before every ordinary plugin, so it has already run by the time a security plugin starts. Scanners that do read wp-content may also treat drop-ins as expected files, because so many legitimate plugins write them.

Will deleting advanced-cache.php or object-cache.php break my site?

Usually not. Without advanced-cache.php, WordPress serves pages uncached; without object-cache.php, it uses its built-in in-memory cache. If a legitimate caching plugin needs the file, it can regenerate it. db.php is the one to check first, because some multi-database setups depend on it.

Where do I see drop-ins in the WordPress admin?

On the Plugins screen, a Drop-ins tab appears when any drop-in exists. WP-CLI lists them with wp plugin list --status=dropin. Both load WordPress — and therefore the drop-ins — so read the files from the filesystem first.

Can a drop-in be hidden somewhere other than wp-content?

Drop-ins are loaded from the content directory, which wp-config.php can move by defining WP_CONTENT_DIR. Check for that constant: if it is set, the drop-ins live wherever it points.

The drop-in came back after I deleted it. What does that mean?

Something else is still running and writing it: a must-use plugin, an include in a core file, an auto_prepend_file directive, a database loader, a scheduled task, or credentials the attacker still holds. Remove the layers together, then rotate access.