Got alerted to some anomalous login + post updates on a blog I administer. I decided to reverse engineer it for a bit of fun. Here’s some of my analysis if it helps anyone.
This can be detected by site site scanners that check the hashes of theme/plugin files, or if you notice the 3 additional lines in the main theme file, or the stage1 backdoor in the uploads directory.
To avoid feeding script-kiddies, there will be no raw source printed here, other than brief snippets and screenshots. If you would like the archive of the various source files and are a (reputable) security researcher, please contact me.
Most of this backdoor’s code lives inside the database, making it a little difficult to stumble on if you’re not looking for it.
Symptoms
- You’ll see an injected (hidden with css) div inside the rendered pages, linking to pages which were added by the attackers. Your site may be flagged by the checker @ https://sitecheck.sucuri.net/.
- You’ll tiny see modifications to the wordpress theme
- You’ll (may) find a disguised backdoors in the
uploads/
directory.
Infection
-
The Adversary authenticates with stolen admin credentials.
-
Stage1 is dropped into
uploads/
directory by the adversary.- location:
wp-content/uploads/2020/index.php
(modified version of dolly.php). Must be removed to prevent re-infection. - Common guidance (for non-experiened users) will tell them this isn’t malware, but that’s false.
- This contains a line containing a some strings and a pseudorandom “magic” string, which is also the URL param for code execution and heartbeats:
- This string is also present in the
wp_options
table. - I’ve randomized the one I found, which’s similar (but not equal)
af1b8652c8
:
- This string is also present in the
- location:
-
Stage1 is called via a POST request to install stage2 and setup it’s various persistience methods.
-
Stage2 lives in both the database and on-disk.
- location:
wp-content/themes/virtue/functions.php
- essentially a
create_function()
call with the base64 payload:
- database
wp_options
underthemes_css
key - base64 encoded payload.- inside this, there’s a key
color
with the magic value from stage1. - nested inside the
fonts
key there is a keyhtml
with a base64 encoded payload for initializing the backdoor - this payload was obfuscated using typical php obfuscation methods and is relatively trivial.
- inside this, there’s a key
- removing either the loader from
functions.php
or thewp_options
entry should stop the execution, but you should remove both.
- location:
Behavior
- the malware can be triggered under any of the following conditions
- a POST to
index.php
with the magic query param, which executes the raw PHP code viaexec()
. - a GET parameter with the key = ‘a’ + the first 5 chars of the MD5 hash of magic query parameter:
- this triggers a GET request to the C2 domain with the following info:
- the value of the get query value
- the
REMOTE_ADDR
- the
HTTP_USER_AGENT
- the http referrer
- it parses this reply and sets the HTTP headers and body depending on their contents.
- this triggers a GET request to the C2 domain with the following info:
- Additionally, it allows serving of posts from a table named
backupdb_wp_posts
under hidden menus on each page. These were observed to be korean texts with references to app downloads, etc. - It also stores user’s IP addresses in an obfuscated form inside of
backupdb_wp_lstat
Exploitation
- We observed an adversary with the useragent
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
(googlebot) issuing POST requests to index.php. Each of these created a new post inside thebackupdb_wp_posts
table, however, this content is simplyexec()
’ed allowing for the attackers to execute anything on the system. - During observation of the calls during traffic inspection, the payload contained php code for inserting posts. However, they could’ve contained anything.
Post-Exploit Cleanup
- Exploit-Specific
- Remove
wp-content/uploads/2020/index.php
(may be named differently!) - Remove backdoor lines from
wp-content/themes/virtue/functions.php
(or your current theme?) - Remove
wp_options
entry with bulk of backdoor code. - Delete entries in
backupdb_wp_posts
table - Delete entries in the
backupdb_wp_lstat
- Rotate All passwords - observed infection vector was compromised credentials.
- Remove
- Rotate database secrets
- Rotate wordpress tokens
Indicators of compromise (IOCs)
-
wp-content/uploads/2020/index.php
is a modified version of the dolly plugin, with a php web-shell for RCE. -
entries for posts inside a table named
backupdb_wp_posts
(if your prefix iswp_
) -
entries inside a table named
backupdb_wp_lstat
, containing obfuscated IP addresses -
Entry in
wp_options
with the namethemes_css
to allow for persistince.- Here’s a formatted version of the value for this key in the options table, with some nice extras to disguise it:
a:5:{ s:3:"css"; s:103:"font-family: sans-serif; line-height: 1.15; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"; s:5:"style"; s:15:"create_function"; // <-- CREATE FUNCTION NAME s:5:"color"; s:10:"af1b8652c8"; // <-- MAGIC VALUE s:5:"fonts"; s:13:"base64_decode"; // <-- BASE64 DECODE FUNC NAME s:4:"html" s:6172:"<base64 string>"; // <-- BASE64 BACKDOOR PAYLOAD }
-
Lines in
wp-content/themes/virtue/functions.php
for loading the exploit from the wordpress options, referencing the following:$wp_template_css = get_option('themes_css' );
, followed by some lines for loading+executing it:
-
The following strings inside theme files for your wordpress installation:
themes_css
string used as the option key for the code. This can be found in thefunctions.php
initialization.
Security Recommendations
To avoid most of these backdoors from functioning, you can change some php settings to harden the install a bit. Disabling shell execution, and eval()
are two that come to mind, among others. Even if they manage to plant a backdoor / webshell, etc…, it many won’t function because most of them rely on those functions for code execution. Unfortunately, this this wordpress install didn’t have these measures implemented yet.
Telling users to use strong and unique passwords is also paramount, despite how many times they hear it. The original infection vector was a user’s password becoming compromised.
Disable commonly abused functions
note: this may break some things, depending on what you’re running!
In your php.ini file (for example /etc/php/7.2/apache2/php.ini
, you’ll need to find the correct config file your install is using), replace your disable_functions
line with the following snippet:
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
source: php default config, https://www.cyberciti.biz/faq/linux-unix-apache-lighttpd-phpini-disable-functions/
Disable eval()
note: this may break some things.
Install the following extension: https://github.com/mk-j/PHP_diseval_extension. This disables eval() similar to how disable_functions
operates.
Wordpress Security plugins
Install a plugin like WP Cerber or Sucuri Security for a dashboard + simple theme/plugin scanner, as well as login rate limiting, logging, and login alerting.
Hide README
The readme can be used for fingerprinting. If all you’re using is wordpress+apache, you can add this to your apache2.conf file:
<files readme.html>
order allow,deny
deny from all
</files>
Disable Directory Listing:
Run the following to disable the autoindex
apache module:
sudo a2dismod --force autoindex