Hunting PHP Web Shells in Magento: Files, Logs, and Patch Verification

Hunting PHP Web Shells in Magento: Files, Logs, and Patch Verification

pr0h0
magentophp-securityweb-shellsadobe-commerce
AI Usage (75%)

What the report says, and what I would not assume yet

The report says an unpatched Magento and Adobe Commerce zero-day was used to plant a backdoor on online stores. That is enough for me to treat it as a real incident, but not enough to pretend the full exploit chain is known.

What I would treat as confirmed from the report:

PointStatus
Magento and Adobe Commerce stores were targetedconfirmed by the report
The issue was described as unpatchedconfirmed by the report
The observed outcome was a backdoor on online storesconfirmed by the report

What I would not assume from the snippet alone:

PointStatus
The CVE numberunconfirmed
The exact vulnerable version rangeunconfirmed
The precise initial access pathunconfirmed
Whether the shell lived in a core file, theme, or custom moduleunconfirmed

My view is straightforward: when you are hunting PHP web shells in Magento, you usually learn more by checking suspicious files and logs than by staring at the version banner. Version checks still matter, but they do not prove the absence of a backdoor.

Why Magento and Adobe Commerce compromises often show up as file and log problems

Magento is a PHP application with a lot of moving parts: code under app/, public assets under pub/, generated and writable content under var/, and customizations that often live in modules or themes. That means a compromise usually leaves traces in places the application touches every day.

The two most common mistakes I see in incident response are:

  1. checking only the core package version, and
  2. assuming the shell must be in an obvious, dramatic file.

In practice, a backdoor can hide in a tiny PHP stub, a rewritten template, a compromised custom module, or even a writable directory that the web server should never execute from. If the store is configured badly, pub/media or var/ can become an execution path instead of just storage.

Backdoor placement patterns that matter in a PHP app

The patterns worth checking first are usually boring:

  • a recently modified .php file in a writable tree
  • a PHP file with a name that looks like an image, cache artifact, or temp file
  • a file under pub/, var/, or media/ that should not contain executable PHP
  • a change inside a custom module where attacker code can blend in
  • a small loader that fetches the real payload from another endpoint or database entry

The shell itself is often just the entry point. The more useful clue is the trail around it: who wrote it, when it appeared, and which request pattern lines up with the timestamp.

What a real compromise chain usually leaves behind

A Magento web shell incident usually leaves at least one of these:

  • access logs with odd POST requests, especially to admin-looking or upload-looking paths
  • file timestamps clustered around the first suspicious request
  • application logs with unexpected exceptions around the same time
  • config or environment file changes, especially if database credentials were exposed
  • follow-on persistence, such as cron jobs, dropped helper files, or modified templates

That is why I prefer a trail-based hunt. One file is evidence. A cluster of file writes, requests, and config changes is the actual story.

Start with suspicious PHP files, but keep the filter tight

I would begin with the paths most likely to be writable or customized, then narrow from there.

High-signal paths to inspect first: app, pub, var, media, and custom modules

Start here:

  • app/code
  • app/design
  • pub
  • var
  • media
  • any custom theme or extension path

I would not spend the first pass on every file under vendor/ unless the rest of the hunt points there. That tree is noisy, and attackers usually prefer somewhere they can alter without a clean deploy process noticing.

Safe example commands for finding recent, writable, or unusual PHP files

A tight first pass:

find app pub var media app/code -type f -name '*.php' \
  -printf '%TY-%Tm-%Td %TT %m %u:%g %p\n' | sort -r | head -n 100

If you want to narrow to files changed recently:

find app pub var media app/code -type f -name '*.php' -mtime -14 \
  -printf '%TY-%Tm-%Td %TT %m %u:%g %p\n' | sort -r

If you suspect web-facing execution from writable locations, look there first:

find pub var media -type f -name '*.php' -o -name '*.phtml'

Then scan for common loader behavior:

grep -RIn --include='*.php' -E \
  'base64_decode|gzinflate|str_rot13|eval\s*\(|assert\s*\(|shell_exec|passthru|system\s*\(' \
  app pub var media app/code

That last command is intentionally noisy. I do not treat a match as proof of compromise. I treat it as a reason to inspect the file and compare it with the expected package or module source.

What makes a file suspicious: timestamps, permissions, naming, and content cues

A file becomes interesting when several weak signals line up:

  • timestamps: it appeared right when the logs got noisy
  • permissions: it is writable by the web server or deploy user when it should not be
  • naming: it looks like an image, cache file, or temp artifact but contains PHP
  • content: it uses obfuscation, remote fetches, or command execution primitives
  • location: it lives somewhere that should not execute PHP at all

A tiny file like image.php, shell.php, or cache.php under a writable path is not automatically malicious. In a Magento incident, though, I would inspect it before almost anything else.

Read the logs as a timeline, not as a pile of events

Logs are where the compromise becomes legible. I usually want two timelines: what the web server saw, and what the application or host changed afterward.

Web access clues: odd POSTs, admin route probes, and unusual user agents

Search for:

  • repeated POST requests to admin or upload endpoints
  • requests to /admin, /backend, /setup, /rest, /graphql, or other high-value routes
  • suspicious user agents that do not look like a browser or a normal bot
  • a burst of 404s or 403s followed by one successful request
  • requests for a newly created PHP file under pub/, var/, or media/

A quick pass over access logs:

grep -E 'POST|/admin|/setup|/rest|/graphql|/checkout|/customer' access.log | tail -n 200

If the logs use the common Apache or nginx timestamp format, I usually narrow around the file modification window:

grep '05/Sep/2026:14:' access.log

That search is blunt, but it gets you to the right minute quickly.

Server and application logs: correlating file timestamps with request bursts

The useful correlation is not “there were lots of requests.” The useful correlation is “a file appeared at 14:23, and the same IP or user agent hit the app at 14:21, 14:22, and 14:23.”

I would check:

  • web server access logs
  • web server error logs
  • PHP-FPM logs
  • Magento var/log/system.log
  • Magento var/log/exception.log

If you have file timestamps and log timestamps in the same timezone, line them up by hand. If not, normalize them first. That is tedious, but it is better than building a false story around mismatched time sources.

What log evidence is confirmed versus what is only inferred

This distinction matters:

EvidenceStatusWhat it means
A suspicious PHP file appeared at a specific timeconfirmed if timestamped and preservedsomeone or something wrote it
The same client fetched that fileconfirmed if the access log shows itthe file was likely used
The same client uploaded that fileinferredneeds request sequence or app logs
The request that caused the upload is the exploitinferredneeds payload or server-side tracing
The compromise came from the reported zero-dayinferred unless your logs or patch state prove it

I would be careful not to overstate causality. A timeline can show proximity. It does not always show the exact exploit.

Verify the patch instead of trusting the reported version

If a report says “unpatched,” I still want to verify what is actually installed and whether the code is really fixed.

Check installed package versions against the Adobe advisory or release notes

Start with the installed product and module versions:

composer show magento/product-community-edition --locked
composer show magento/product-enterprise-edition --locked
bin/magento --version

Then compare those values to the relevant Adobe security advisory or release note. A version string tells you what was installed. It does not always tell you what was backported.

Confirm code-level change, not just composer metadata, when possible

This is the part many teams skip. They look at composer.lock, see a “fixed” version, and move on. I would not.

If you have a clean reference build, compare the suspect files:

diff -ruN clean-magento/vendor/magento/module-backend vendor/magento/module-backend | less

If the installation came from a release archive rather than a git checkout, compare file hashes or package checksums against a known-good artifact from the same release. That catches the cases where metadata looks right but the code tree is not what you expect.

How to handle cases where a backport makes version checks misleading

Backports make simple version checks unreliable. A managed platform may patch a file without changing the public version number in the way you expect. That is why I treat version checks as a starting point, not the finish line.

If the package version says one thing but the code diff says another, trust the code diff. If you cannot verify the code diff, assume the patch state is unproven and keep hunting.

If you find a web shell, containment comes before cleanup

The biggest operational mistake is deleting the file first and asking questions later. That destroys evidence and can leave the rest of the intrusion in place.

Isolate the host, preserve evidence, and rotate credentials in the right order

My usual order is:

  1. isolate the host from the network if you can do it safely
  2. preserve disk and log evidence
  3. capture hashes and timestamps for suspicious files
  4. identify which secrets may have been exposed
  5. rotate credentials after containment
  6. rebuild from a known-good source if the scope is unclear

If app/etc/env.php was touched, assume database credentials may be exposed. If the web server could write into executable paths, assume the filesystem trust boundary is already broken.

When deleting the file is not enough and a rebuild is the safer call

I would rebuild instead of clean in any of these cases:

  • the shell reappears after deletion
  • you cannot explain how it was placed
  • there are multiple suspicious files across different directories
  • admin credentials, DB credentials, or deploy keys may have been exposed
  • you cannot trust the integrity of the Magento code tree

In other words: if you cannot reconstruct the attack path, you do not yet know the true blast radius.

Hardening steps that reduce repeat compromise

Patch cadence, admin MFA, least-privilege file ownership, and upload restrictions

The controls I care about most here are:

  • fast patching of Magento and Adobe Commerce releases
  • MFA on admin accounts
  • least-privilege ownership for application files
  • no write access from the web server to executable code paths
  • strict upload handling so uploaded files cannot become executable

If I had to rank them, I would fix file ownership and execution boundaries before I argued about scan frequency. A store that can write and execute PHP from a public path is already too permissive.

Detection ideas for future hunts: integrity checks, file monitoring, and log baselines

Good recurring detections are boring but effective:

ControlWhat it catches
File integrity monitoringunexpected changes in core or custom PHP
Baseline diff of app/ and pub/new files where none should exist
Web log anomaly rulesodd POST bursts, admin probing, unusual user agents
Alerting on PHP in writable pathsshells dropped into media, var, or pub
Scheduled audit of composer.lock and package versionsdrift from expected patch state

The point is not to catch every attacker trick. The point is to make the next compromise noisy enough that you see it early.

What I confirmed from public reporting, and what still needs local validation

What the public report gives us is narrow but useful:

  • Magento and Adobe Commerce were the affected platforms
  • the issue was described as unpatched
  • the observed outcome was a backdoor on online stores

What still needs local validation on any real system is the part that matters operationally:

  • whether your installation matches the affected build
  • whether any PHP files appeared in writable or public paths
  • whether the logs show the same request burst that lines up with file creation
  • whether the patch is present in code, not just in metadata

That is the real hunt. Not “is there a shell file,” but “what trail proves how it got there, what it touched, and whether the patch state is actually trustworthy?”

Conclusion: the real lesson is to hunt for the trail, not just the shell

My take is that Magento and Adobe Commerce incidents like this should be handled as filesystem-and-log investigations first, version checks second. The shell is the symptom. The trail is the evidence.

If you only delete the file, you lose the story. If you only read the version number, you may miss the compromise entirely. The practical win is to line up file writes, access logs, and patch verification, then decide whether you are dealing with one bad artifact or a wider rebuild event.

Share this post

More posts

Comments