Reverse Engineering some Wordpress Malware

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:
    Stage 1 Backdoor
    Stage 1 exploit loaded via the wp-admin plugin editor, used to drop stage 2.
  • 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:
    Persistience Code
    Code used to load malware from the wp_options table
    • database wp_options under themes_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 key html with a base64 encoded payload for initializing the backdoor
      • this payload was obfuscated using typical php obfuscation methods and is relatively trivial.
    • removing either the loader from functions.php or the wp_options entry should stop the execution, but you should remove both.

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 via exec().
  • 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.
  • 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 the backupdb_wp_posts table, however, this content is simply exec()’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.
  • 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 is wp_)

  • entries inside a table named backupdb_wp_lstat, containing obfuscated IP addresses

  • Entry in wp_options with the name themes_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 the functions.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

Change Log

  • 4/12/2020 - Initial Revision

Found a typo or technical problem? file an issue!