Drop-ins and must-use plugins
advanced-cache.php, object-cache.php and files in mu-plugins load before ordinary plugins without being activated, and are a natural second loader.
Guide · Core-file includes
You opened index.php, wp-blog-header.php, wp-load.php or wp-config.php and found a line above the normal code: an @include followed by a string of \x2f and \x68 sequences, or of three-digit numbers after backslashes. It is an obfuscated path to a second file, usually hidden somewhere with an innocent name such as favicon.ico, and it runs on every page load. This guide covers how to find every copy, read the path without running it, and remove both halves in an order that holds.
Published Last reviewed
What it is
PHP’s include statement loads and runs another file. The @ in front suppresses any error, so if the target ever goes missing the site keeps working and nobody sees a warning. Put that one line at the top of index.php, which runs on every front-end request, and the attacker’s file runs on every front-end request too — before WordPress, its plugins, or any security plugin has loaded.
The path is written as escape sequences so that it does not read as a path. In a PHP double-quoted string, \x2f is a forward slash written in hexadecimal and \057 is the same character in octal; a whole path can be spelled this way, often mixing both forms and plain letters. Searching the site for the name of the payload finds nothing, because the name never appears in readable form.
The target is chosen to be overlooked. A file called .favicon.ico in wp-includes/images, a .png inside an uploads folder, a .txt in a cache directory: PHP does not care about the extension, because include runs whatever the file contains. Image and icon files are also exactly what most people skip when they review a hacked site.
Figure 1 · The same file, clean and injected
One line above the comment block loads a PHP file disguised as an icon.
<?php
/**
* Front to the WordPress application. This file
* doesn't do anything, but loads
*/
The opening tag, then WordPress’s own documentation comment.
<?php
@include "\x2fh\x6fme\x2facct\x2fpublic_html\x2fwp-includes\x2fimages\x2f.favicon.ico";
/**
* Front to the WordPress application. This file
An error-suppressed include of an escaped path, above the comment, to an icon file that is really PHP.
Where to look
Work from the WordPress root over SSH or a file manager with hidden files shown. Every check here only reads.
A clean index.php, wp-blog-header.php and wp-load.php start with <?php and a documentation comment. Anything between the opening tag and that comment deserves a closer look:
head -n 5 index.php wp-blog-header.php wp-load.php wp-settings.php wp-config.php
Some injections sit on the same line as the opening tag, padded with spaces so the code runs off the edge of an editor window. Scroll right, or check the line length.
WordPress’s own includes use __DIR__ or ABSPATH and plain text; none uses @ or escape sequences:
grep -nE '@(include|require)|\\x[0-9A-Fa-f]{2}|\\[0-7]{3}' index.php wp-blog-header.php wp-load.php wp-settings.php wp-config.php
In wp-config.php, ignore matches inside the authentication keys and salts: those are random strings and can contain anything.
An escaped @include is rarely placed only once. This lists every PHP file that includes an escaped path:
grep -rlE '@(include|require)(_once)?[[:space:]]*[(]?[[:space:]]*.\\([xX][0-9A-Fa-f]{2}|[0-7]{3})' --include='*.php' .
Theme functions.php files and plugin main files are the usual second places. Each match is a separate loader to record.
Core files that differ from the official release for your version, and files that should not exist in wp-admin and wp-includes:
wp core verify-checksums
wp-config.php is not part of the release, so this check cannot see an include there — the grep above can. WP-CLI commands that load WordPress also run wp-config.php, so check that file first.
Files with non-PHP extensions that contain a PHP opening tag:
find . -type f \( -name '*.ico' -o -name '*.png' -o -name '*.gif' -o -name '*.jpg' -o -name '*.txt' \) -exec grep -l '<?php' {} + 2>/dev/null
A real image never contains <?php. Every result is a payload, whether or not an include currently points at it.
Read it without running it
You need the decoded path to find the payload, and you should get it without executing anything. Two safe ways: paste the quoted string into the snippet decoder on this site, which works entirely in your browser, or let the shell’s printf turn the escapes back into characters. Copy only the escaped text between the quotes, and keep it inside single quotes so the shell does not interpret it:
printf '%b\n' '\x2fh\x6fme\x2facct\x2fpublic_html\x2fwp-includes\x2fimages\x2f.favicon.ico'
Do not decode it with php -r or an online PHP sandbox that evaluates code. A PHP double-quoted string can contain expressions that run when the string is evaluated, so an attacker’s string is not safe to hand to PHP even when it looks like data.
Removing it
The include and its target are two halves of one mechanism, and something wrote both. Remove them together, after recording them, and then deal with whatever wrote them.
Copy every file carrying the include, and every target it points to, with their timestamps, into a directory outside the web root, and record the hashes:
mkdir -p ~/incident
cp -p index.php wp-blog-header.php wp-load.php wp-config.php ~/incident/
sha256sum ~/incident/*
Add each decoded target path to the copy and the hash list. The modification times bound the window everything else gets checked against.
Re-download the version you are running over the existing core files, leaving wp-content alone:
wp core download --skip-content --force --version=$(wp core version)
This restores index.php, wp-blog-header.php, wp-load.php and the rest of core. It does not touch wp-config.php, which is not part of the release: remove the include line from it by hand, keeping everything else.
Delete the decoded targets and any other PHP-in-image files the search found, then run the searches again. A clean result on both is the bar, not the absence of the one line you started with.
Something had write access to the WordPress root: a vulnerable plugin, a backdoor elsewhere, stolen FTP or control-panel credentials, or a neighbouring site in the same account. Removing the include without finding that mechanism means it comes back.
Control panel, SFTP and FTP, the database user, WordPress administrators and application passwords, and the authentication keys and salts in wp-config.php — from a device that is not part of the incident.
Repeat both searches and the checksum check. An include that reappears means a persistence layer is still running: a scheduled task, an auto_prepend_file directive, a database loader, or another account.
What else to check
A loader at the top of index.php is the visible end of an infection, and it is usually not alone. In the ClickFix incident documented on this site, the same chain lived in obfuscated PHP files, base64 content in the options table, the active theme’s functions.php, and a .user.ini startup directive at once. Any surviving layer can put the include back.
advanced-cache.php, object-cache.php and files in mu-plugins load before ordinary plugins without being activated, and are a natural second loader.
Encoded loader material stored in autoloaded options survives every file replacement, because it is not a file.
A plugin folder named like a real plugin, or a modified hello.php, is a common place for the code that writes includes.
One hosting account is one incident. Run the same searches across every WordPress installation the account holds.
Common questions
Not at the top of a WordPress core file. WordPress includes its own files with __DIR__ or ABSPATH and plain text, never with the @ operator and never with an escaped path. Some plugins do use escape sequences for other reasons, but an escaped include placed above the opening comment of index.php, wp-blog-header.php or wp-load.php has no legitimate explanation.
Because whatever wrote it is still running. Common causes are a second loader elsewhere — a drop-in, a must-use plugin, an auto_prepend_file directive, or a database loader — a scheduled task that rewrites the file, or credentials the attacker still holds. Remove the include, its target, and the mechanism together.
Open it in a text editor from the filesystem, not in a browser. It is a PHP file with an image extension; requesting it over HTTP may execute it, and an image viewer will simply fail to show it. Record its hash and timestamps before you remove it.
Replacing core removes the include from index.php, wp-blog-header.php, wp-load.php and other core files. It does not touch wp-config.php, which is not part of the release, or the payload the include pointed to, or whatever wrote the line. Each of those needs its own step.
Scanners match patterns, and an escaped path is designed not to match. A core-file comparison does catch the change to index.php and wp-load.php, but not to wp-config.php. The searches in this guide look for the shape of the injection rather than a known signature.
Deleting the payload alone leaves a working loader: the @ hides the error, and the next time anything writes a file to that path, it runs again. Remove both halves, after preserving them, and then find what wrote them.