DEV Community

Jason Miller
Jason Miller

Posted on Originally published at axeploit.com

Patching Elementor Pro doesn't remove the webshell. Check before you close the ticket.

Elementor shipped the fix for CVE-2026-32475 on August 19. Attackers started exploiting it the same day, and Wordfence blocked more than 190,000 attempts in the first five days. If your site ran Elementor Pro 4.2.1 or earlier during that window, updating is the second thing to do. The first is finding out whether someone got there before you.

The plugin has 6 million+ active installs and a public PoC on GitHub. "We patch within a week" lost this race before it started.

The bug, in one paragraph

Two loops in modules/forms/fields/upload.php disagree about your upload. The attacker submits the File Upload field as an array: empty first element, PHP payload second. validation() hits the empty entry (UPLOAD_ERR_NO_FILE) and aborts, never inspecting file two. process_field() skips the empty entry and moves the .php into wp-content/uploads/elementor/forms/ under a uniqid() filename, keeping the attacker's extension. Delivery is one POST to /wp-admin/admin-ajax.php with action=elementor_pro_forms_send_form. No auth, no nonce.

The sources argue about preconditions. Wordfence says the field must not be marked required. The vendor notice, per BleepingComputer, points at the multiple file upload option. The PoC assumes no CAPTCHA and an uploads directory that executes PHP. You can spend an afternoon reconstructing your exact August form config, or ten minutes looking for the shell. One of those settles the question.

Ten minutes of hunting

# PHP where only PDFs and images belong
find /var/www/html/wp-content/uploads -type f \
  \( -iname "*.php" -o -iname "*.phtml" -o -iname "*.phar" \) -ls

# Everything written since the day before the patch
find /var/www/html/wp-content/uploads -type f -newermt "2026-08-18" | less
Enter fullscreen mode Exit fullscreen mode

On a healthy site the first command returns nothing. Anything with eval, base64_decode, shell_exec, or a $_REQUEST gate is a shell.

The exploit is a two-step: POST to the form handler, then GET the dropped file to run commands. POST bodies are not logged by default, but the GET is, random filename and all.

grep "elementor_pro_forms_send_form" /var/log/apache2/access.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

grep -E "GET /wp-content/uploads/elementor/forms/[^ ]*\.ph" \
  /var/log/apache2/access.log
Enter fullscreen mode Exit fullscreen mode

The pattern: the same IP POSTs to admin-ajax.php, then minutes later GETs a .php file under the forms directory. A 200 on that GET means the shell executed. Confirmed compromise.

Then check for persistence:

wp user list --role=administrator
wp cron event list
wp core verify-checksums
Enter fullscreen mode Exit fullscreen mode

Unknown admin, strange cron event, failed checksum: assume full compromise. Elementor Pro is premium, so wp plugin verify-checksums cannot validate it against wordpress.org. Download a clean copy from your Elementor account and diff it against disk.

Found a shell? Do not just rm it. Copy it somewhere safe, record timestamps, pull the logs around its creation time, block its URL at the web server. Then rotate everything the shell could read: the DB credentials in wp-config.php, admin passwords, API keys, and regenerate the WordPress salts to kill active sessions. If the shell was reachable more than a day or two, restore from a backup predating the first malicious POST. Cleaning a site an attacker has lived on for weeks is a confidence game you usually lose.

The fix that survives the next plugin CVE

There are two windows, and the patch only closes one. It closes the vulnerability window. Only hunting closes the dwell window, and most teams close the ticket at "updated."

So deploy the mitigation that does not care how fast you patch: never execute PHP out of the uploads tree. On Apache with mod_php, php_flag engine off in an .htaccess works, but that directive does nothing under PHP-FPM, which most modern stacks run. Deny the files instead:

# Apache 2.4 + PHP-FPM
<FilesMatch "\.ph(p|tml|ar)$">
    Require all denied
</FilesMatch>
Enter fullscreen mode Exit fullscreen mode
location ~* ^/wp-content/uploads/.*\.ph(p|tml|ar)$ {
    deny all;
}
Enter fullscreen mode Exit fullscreen mode

Wordfence's "Disable Code Execution for Uploads directory" gets you the same result.

Do these today:

  • Run the find command. Zero PHP files under uploads is the only acceptable answer.
  • Grep current and rotated logs (zgrep) for the POST-then-GET pattern. A 200 on the GET means it ran.
  • wp user list --role=administrator, wp cron event list, wp core verify-checksums.
  • Block PHP execution under wp-content/uploads at the server config, not the plugin.

Honest question: how many WordPress sites are you running where uploads still execute PHP, and what is actually stopping you from killing that today?

Longer writeup with the full timeline and containment steps: https://axeploit.com/blog/patching-elementor-pro-doesn-t-remove-the-webshell-here-s-how-to-check

Top comments (0)