Chat on WhatsApp
Upgrades & Patches 10 min read

Was My Magento Server Hacked? How to Check for PHP Remote Code Execution

php2shell leaked before the patch, so the first question is not when to update PHP but whether someone already got in. These are the read-only checks I ran on my own Magento VPS: worker processes, access logs, changed files and the Magento database.

Was My Magento Server Hacked? How to Check for PHP Remote Code Execution

If you want to check if a Magento server is hacked through a PHP remote code execution bug, you do it in four passes, all read-only: running processes, web access logs, files on disk, and the Magento database. This post is the exact list I ran on my own CloudPanel VPS on 21 October 2026, before the PHP security release that day (PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35), which fixes the two php2shell vulnerabilities, CVE-2026-109618 and CVE-2026-109619.

My result was boring, which is the result you want. Nothing in it is magic, and nothing in it proves a server is clean. It tells you whether there are the obvious signs, and it tells you fast.

2critical php2shell CVEs, CVSS v4.0 9.2 each
0PHP workers with shell children on my VPS
0access log hits for the exploit strings
0PHP files in media or upload folders

What do we actually know about php2shell?

Less than you would like. Details of two critical bugs in PHP core leaked on 20 October 2026, before the planned disclosure. The PHP project posted a notice the same day: a security release scheduled for Wednesday 21 October 2026 at 14:00 UTC, two vulnerabilities, each rated 9.2 on CVSS v4.0. The release came out on 21 October as PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. I cover the update itself in my php2shell write-up.

Huntress wrote the clearest summary I found. As of their post they had no confirmed indicators of compromise (their table literally says TBD), and they had not seen exploitation in the wild in their partner environments. What they did give is a description of what exploitation would likely look like: a PHP worker process starting a shell, an interpreter or a download tool, command lines with reverse shell patterns such as /dev/tcp/, fsockopen or mkfifo, and recon commands like whoami, id or uname -a running from the web server. That is what the first two checks below look for.

If you do not yet know how many PHP builds live on your box (host PHP, Docker images, control panel PHP, the CLI that cron uses), sort that out first with my post on finding every PHP version on your server. You cannot check workers you do not know exist.

Check 1: is a PHP worker spawning a shell?

On a healthy Magento server, the web side of PHP (php-fpm workers, or apache2 with mod_php) almost never starts other programs. A php-fpm spawning a shell is the single loudest sign of RCE. This loop lists the child processes of every PHP or Apache worker and keeps only the interesting ones:

# read-only: list shells, interpreters and download tools started by PHP workers
for p in $(pgrep -f 'php-fpm|php-cgi|apache2|httpd'); do
  pgrep -l -P "$p"
done | grep -Ew 'sh|bash|dash|python[0-9.]*|perl|curl|wget|nc|ncat|socat'

No output is the answer you want. If something prints, look at the full command line before you panic:

ps -o pid,ppid,user,etime,args -p <PID>
ps -eo pid,ppid,user,etime,args --forest | less

Two honest caveats. Some extensions do call out to other programs from a web request (PDF generators, image optimizers, an old ERP connector calling curl through exec()). You should know which ones you run. And Magento cron is a different story: php bin/magento cron:run is PHP CLI, started by cron, and it can start shell processes for queue consumers. That is normal. The check is about the web workers.

Same check for Docker

Containers share the host kernel, so the loop above already sees processes inside your containers when you run it on the host as root. I still look per container, because it makes the parent chain easier to read:

docker ps --format '{{.Names}}' | grep -i php
docker top <php-container> -eo pid,ppid,user,etime,args

On my VPS I ran both the host loop and docker top against both PHP containers. No worker had a shell or download tool as a child.

Check 2: what does the access log say?

Exploit attempts usually show up in the web access log, even the failed ones. Grep for the strings Huntress listed plus a few classic webshell parameters. Include the URL-encoded forms, because a lot of payloads arrive encoded:

# CloudPanel keeps per-site logs under /home/<site-user>/logs/nginx/
zgrep -iE '/dev/tcp/|%2fdev%2ftcp|fsockopen|mkfifo|whoami|uname(\+|%20| )-a|cmd=|shell_exec' \
  /var/log/nginx/access.log* | less

Adjust the path to your setup. zgrep also reads the rotated .gz files, which matters, because nobody waits for you to look on the same day. Today my nginx access logs had zero hits for any of those strings.

It helps to know what a probe looks like when you see one. In September 2026 my store got two requests aimed at the Magento StyleSmuggler bug. Both ended in HTTP 500, both were logged, and nothing was written to disk. After that I added an nginx rule that blocks any request containing <? or styles[. A probe is a request that hits an odd URL with odd parameters and fails. A breach is when that request is followed by a new file, a new admin, or a worker spawning a shell. The full story is in my StyleSmuggler and SessionReaper guide.

Check 3: which PHP files changed, and should they have?

A webshell is usually a small PHP file dropped somewhere the web server will execute it. Finding recently changed PHP files is easy. Deciding which ones are fine is the real work, because a normal Magento deploy touches a lot of files.

cd /path/to/magento
find . -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \) -ctime -14 \
  -not -path './generated/*' -not -path './var/cache/*' \
  -not -path './pub/static/*' -not -path './vendor/*' \
  -printf '%CY-%Cm-%Cd %CH:%CM  %p\n' | sort

I use -ctime instead of -mtime on purpose. Anyone with shell access can fake the modification time with touch, but the change time is set by the kernel. Fourteen days is a starting window, widen it if you have doubts.

Then compare against what you know you shipped. If your app/code and themes live in git, this is the fastest filter:

git status --porcelain
git ls-files --others --exclude-standard | grep -E '\.(php|phtml|phar)$'

Untracked PHP files are the ones to read first. On my store every recently changed PHP file matched a commit from my own deploys. I excluded vendor/ above only to cut noise. An attacker can write there too, so if anything else looks wrong, rebuild vendor/ from your composer.lock on a clean machine and diff it.

PHP files where none should exist

Some folders should never contain PHP at all. pub/media is the big one on Magento, and wp-content/uploads if a WordPress blog shares the server:

find pub/media pub/static -type f \( -iname '*.php*' -o -iname '*.phtml' -o -iname '*.phar' \)
find / -path '*/wp-content/uploads/*' -type f -iname '*.php*' 2>/dev/null
ls -la pub/

The first two should print nothing. For pub/ itself, a stock Magento 2.4 has cron.php, get.php, health_check.php, index.php and static.php at the top level, plus the errors/ folder. Anything else with a PHP extension needs an explanation. Across every site on my box, there were no PHP files in media or upload folders.

Check 4: has anything changed inside Magento?

An attacker with code execution on a Magento server rarely stops at a webshell. They want a way back in that survives a cleanup, and on a store they often want card data. These queries are all SELECTs, safe to run on production.

Admin users and API access

SELECT user_id, username, email, created, modified, logdate, is_active
FROM admin_user ORDER BY created DESC;

SELECT integration_id, name, status, created_at, updated_at
FROM integration ORDER BY created_at DESC;

SELECT entity_id, consumer_id, admin_id, type, revoked, created_at
FROM oauth_token ORDER BY created_at DESC LIMIT 20;

Look for a username you do not recognize, a created date that lines up with nothing you did, or an old account whose modified date jumped recently (a password reset by someone else looks like that). Same for integrations and tokens.

Cron jobs

crontab -l -u <site-user>
ls -la /etc/cron.d/ /var/spool/cron/crontabs/ 2>/dev/null
docker exec <php-container> crontab -l
SELECT job_code, COUNT(*) AS runs, MAX(scheduled_at) AS last_scheduled
FROM cron_schedule GROUP BY job_code ORDER BY job_code;

System crontab is where persistence usually lives: a line that runs curl or wget piped into a shell every few minutes. In cron_schedule, look for a job_code that does not belong to Magento core or a module you installed.

env.php, skimmer config and bin/magento

stat app/etc/env.php
diff <(ssh backup-host cat /backups/env.php) app/etc/env.php   # if you keep a copy

env.php holds the DB password and the crypt key, and it is not in git, so its change time and a diff against a known copy are what you have. Then the classic card skimmer spots:

SELECT config_id, scope, scope_id, path, updated_at, LEFT(value, 300) AS value
FROM core_config_data
WHERE path IN ('design/head/includes', 'design/footer/absolute_footer')
   OR value LIKE '%<script%'
ORDER BY updated_at DESC;

SELECT block_id, identifier, update_time FROM cms_block
WHERE content LIKE '%<script%' ORDER BY update_time DESC;

Every <script> in there should be one you or your team added on purpose, like a tag manager snippet. A script loading from a domain you have never heard of is a skimmer until proven otherwise.

Finally, think like someone with a shell on your box. The bin/magento commands I would expect an attacker to use are admin:user:create, config:set (to inject a script or change a payment setting), module:enable for a planted module, and cache:flush to make changes show. Check shell history for the site user and root (~/.bash_history), and list modules that are enabled but not in your repo:

bin/magento module:status --enabled | grep -v '^Magento_'

What should you do if you find something?

The instinct is to delete the webshell and move on. Please do not do that first. Deleting the file removes your best evidence and leaves whatever else they set up.

  1. Isolate. Put the store in maintenance, or block public traffic at your firewall or CDN. Keep SSH open for yourself.
  2. Preserve. Copy web server logs, PHP-FPM logs, var/log, the suspicious files and a database dump to somewhere off the server. Note timestamps.
  3. Find the scope. Run all four checks again with a wider window. One webshell usually means there is a second one.
  4. Rotate everything. The Magento crypt key (bin/magento encryption:key:change on current 2.4 releases), every admin password, your Adobe Marketplace keys in auth.json, the database password, SSH keys, and any payment gateway API keys stored in config.
  5. Restore from known-good. Rebuild code from git and composer.lock, not from the server. Restore the database from a backup older than the first sign of compromise, then replay real orders if needed.
  6. Patch. Upgrade PHP to the fixed release for your branch from php.net or your distro, and make sure Magento itself is current. My APSB26-153 October patch guide covers the Magento side.

Adobe's own security best practices for Commerce point the same way: enable two-factor authentication for the Admin and SSH, give admin users only the permissions they need, run the free Commerce Security Scan against your store, and have a disaster recovery plan before you need one.

Does a clean result mean my server is safe?

No, and I want to be straight about this. These checks catch the common, noisy patterns. A careful attacker can clean logs, run code only in memory, or wait. Process checks only see what is running at that moment. And with php2shell specifically, nobody had published confirmed indicators when I wrote this, so we are all looking for expected behavior, not known signatures.

What a clean result does give you is a baseline. Run it again after you patch PHP, and again in a week. For ongoing coverage on a Linux server without endpoint detection, Huntress suggests Florian Roth's free process-watcher in observe mode, which watches for exactly the PHP-worker-spawns-shell pattern from Check 1.

If you would rather have a second pair of eyes on it, I do this as part of a security checkup for Magento stores: the four passes above, a written report of what I found, and a patch plan. You can hire me for it at $25/hr, or if you already see something wrong, use urgent Magento bug fixing or message me on WhatsApp.

FAQ

How do I check if my Magento server is hacked?

Run four read-only passes: look for PHP workers with shell or download-tool child processes, grep access logs for exploit strings like /dev/tcp/ and shell_exec, list recently changed PHP files and compare them to git, and query admin_user, integration, cron and core_config_data for changes you did not make.

What does php-fpm spawning a shell mean?

A php-fpm worker with sh, bash, python, curl or wget as a child process usually means PHP code ran a system command from a web request. A few extensions do that on purpose, so check the full command line, but on most Magento stores it should never happen.

Are there known indicators of compromise for php2shell?

Not at the time of writing. Huntress listed IOCs as TBD and had not seen in-the-wild exploitation. Detection so far is about expected behavior: PHP workers starting shells or download tools, reverse shell strings, and recon commands such as whoami or uname -a.

Which PHP versions fix CVE-2026-109618 and CVE-2026-109619?

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, released on 21 October 2026. Check php.net and your distribution or hosting provider for the fixed build for your branch instead of trusting a version number from a blog.

Why do so many PHP files change after a Magento deploy?

setup:upgrade, di:compile and static content deploy write into generated/, var/ and pub/static, and composer updates rewrite vendor/. That is why the file check excludes those folders and compares the rest against git or your last deploy.

Should I delete a webshell as soon as I find it?

Not first. Isolate the server, copy the logs and the file somewhere safe, then look for the rest of what the attacker left. Deleting one file while a cron job or a second shell remains just tells them you noticed.

Which keys should I rotate after a Magento compromise?

The crypt key in env.php, all admin passwords, Adobe Marketplace keys in auth.json, the database password, SSH keys, and any payment or shipping API keys stored in Magento config. Assume anything readable by the web user was read.

Can a Magento extension or malware scanner do this for me?

Scanners help, and Adobe's Commerce Security Scan is free, but they look from outside or for known signatures. Process, log and database checks on the server itself catch things a remote scan cannot see.