
Hunting PHP Web Shells in Magento: Files, Logs, and Patch Verification
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:
| Point | Status |
|---|---|
| Magento and Adobe Commerce stores were targeted | confirmed by the report |
| The issue was described as unpatched | confirmed by the report |
| The observed outcome was a backdoor on online stores | confirmed by the report |
What I would not assume from the snippet alone:
| Point | Status |
|---|---|
| The CVE number | unconfirmed |
| The exact vulnerable version range | unconfirmed |
| The precise initial access path | unconfirmed |
| Whether the shell lived in a core file, theme, or custom module | unconfirmed |
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:
- checking only the core package version, and
- 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
.phpfile 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/, ormedia/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
POSTrequests, 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/codeapp/designpubvarmedia- 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
POSTrequests 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/, ormedia/
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:
| Evidence | Status | What it means |
|---|---|---|
| A suspicious PHP file appeared at a specific time | confirmed if timestamped and preserved | someone or something wrote it |
| The same client fetched that file | confirmed if the access log shows it | the file was likely used |
| The same client uploaded that file | inferred | needs request sequence or app logs |
| The request that caused the upload is the exploit | inferred | needs payload or server-side tracing |
| The compromise came from the reported zero-day | inferred 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:
- isolate the host from the network if you can do it safely
- preserve disk and log evidence
- capture hashes and timestamps for suspicious files
- identify which secrets may have been exposed
- rotate credentials after containment
- 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:
| Control | What it catches |
|---|---|
| File integrity monitoring | unexpected changes in core or custom PHP |
Baseline diff of app/ and pub/ | new files where none should exist |
| Web log anomaly rules | odd POST bursts, admin probing, unusual user agents |
| Alerting on PHP in writable paths | shells dropped into media, var, or pub |
Scheduled audit of composer.lock and package versions | drift 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.


