A PHP web shell is a script planted on a web server that lets an attacker run commands through ordinary web requests, and on PHP hosting it is the most common way a single exploited bug turns into lasting control. PHP's flexibility works against defenders here: any writable directory that the interpreter will execute, any per-directory configuration file and any plugin installer can become a persistence point. This guide is a defender's reference for PHP environments specifically (Apache with mod_php, Nginx or Apache with PHP-FPM, WordPress, Drupal, Magento, Laravel and similar): the indicators to look for, how to hunt across files, logs and processes, how to check integrity against a known-good build, how to remove a shell without leaving the attacker inside, which credentials to rotate, and the PHP and server settings that make the next shell harder to plant or run. It contains no shell code. For the incident workflow, roles and communications, use the Web Shell Response Playbook; this page is the PHP-specific technical depth behind it.
Key takeaways
- Compare against known-good, do not trust timestamps. NSA and the Australian Signals Directorate put comparison with a verified clean copy first, and warn that attackers alter file times (ATT&CK T1070.006 Timestomp).
- Watch the PHP process, not just the files. A PHP-FPM worker or Apache child launching a shell, network tools or reconnaissance commands is the strongest single signal on Linux.
- Look beyond .php files. Per-directory configuration (
.user.iniunder FastCGI,.htaccessunder mod_php), backdoor plugins and hidden admin accounts give the same access and survive file clean-ups. - Removal is the start, not the end. NSA and ASD stress that removing a shell without scoping the intrusion lets the attacker return; rebuild from clean code and rotate every secret the PHP user could read.
- Stop PHP executing where users can write. The most effective hardening is a web root the PHP user cannot write to and upload directories where PHP will not run;
disable_functionsandopen_basedirhelp but PHP's own manual says they are not security boundaries.
How PHP web shells get in and where they hide
ATT&CK tracks web shells as T1505.003 under persistence. The shell is rarely the entry point: it is planted after another weakness, and knowing the route tells you where else to look.
| Entry route | Typical PHP example | Where to look first |
|---|---|---|
| Exploited public-facing application (T1190) | Vulnerable plugin, theme, CMS core or the PHP runtime itself, as with the PHP2Shell campaign | Access logs around the vulnerable endpoint; files created shortly after |
| Unrestricted file upload | Upload form or media library that accepts a file the server later executes as PHP | Upload and media directories, cache and temp directories inside the web root |
| Stolen deployment or admin credentials (T1078) | Leaked sFTP, SSH, control-panel or CMS administrator passwords | sFTP and control-panel logs, CMS admin logins, new plugin installs |
| Compromised update or management channel | Stolen remote-management keys used to push a "plugin" through the normal installer | Plugin and must-use plugin directories, plugin install logs, new admin users |
| Supply chain | Backdoored package or nulled (pirated) theme or plugin | Third-party code compared against the vendor's published release |
The UpdraftCentral Key Theft 2026 case shows the last two rows: stolen connection keys were used to send validly signed plugin installs, the backdoor plugins created hidden accounts, and the advice was to treat affected sites as fully compromised and restore from a backup taken before the theft. No vulnerability was exploited and no standalone PHP file had to be dropped. See the PHP2Shell guide for a runtime-level entry route and the PHP and WordPress Security Guide for platform hardening in general.
Common hiding places
- Upload, cache and temp directories inside the document root, where PHP files have no business being.
- Plugin, theme and module directories, including must-use plugin folders that load automatically and do not appear in the normal plugin list.
- Legitimate files with appended code, such as a theme's functions file or a core bootstrap file, so the file name looks normal.
- Per-directory configuration:
auto_prepend_fileandauto_append_fileareINI_PERDIRsettings, so they can be set in a.user.ini(read by the CGI and FastCGI SAPI, scanned from the script's directory up to the document root) or in.htaccessunder the Apache module, causing a hidden file to run before or after every page. - Files with misleading extensions or names that the server is configured to execute as PHP, and files named to look like images, logs or licence files.
- The database: CMS options, widgets or plugin settings that store code later evaluated by the application, and administrator accounts that need no file at all.
Indicators of a PHP web shell
| Indicator type | What to look for | Reliability |
|---|---|---|
| Integrity difference | Files added or changed compared with the clean release or your deployment artefact | High; every difference needs review |
| Process lineage | php-fpm, apache2 or httpd spawning a shell (T1059.004), network utilities, package managers or reconnaissance commands | High on servers where the application never does this |
| Rare URIs | PHP files requested by very few user agents or IP addresses, often with missing or unusual referer headers | Medium; NSA's log scripts always return results, so verify each |
| Unexpected outbound connections | The web server connecting out to unfamiliar hosts, or downloading tools (T1105) | Medium to high where egress is normally limited |
| File content patterns | Dynamic code execution and decoding functions combined with request parameters; very long encoded strings; matches from YARA rule sets | Core signatures catch known unmodified shells; extended ones catch obfuscation but raise false positives |
| Behaviour | Unusually large responses, off-peak access, requests from unexpected geographies | Low on its own; NSA calls these subjective |
| Account and config changes | New CMS administrators, new SSH keys for the web user, new cron entries, modified .user.ini or .htaccess | High |
Hunting: files, logs and processes
Files and integrity
- Get a trusted baseline. Use your deployment artefact or a fresh download of the exact versions of the CMS, plugins and themes. NSA's Mitigating-Web-Shells project shows a recursive directory diff on Linux and a PowerShell directory checker on Windows for this comparison.
- Verify platform checksums. Most CMSs and package managers can verify installed files against published hashes (for example WP-CLI's core and plugin checksum verification, or the distribution's package verification for system PHP files). Treat any mismatch as a lead.
- List PHP where it should not be. Search upload, media, cache and temp directories for any file the server will execute as PHP. On most sites the expected count is zero.
- Review per-directory config. Find every
.user.iniand.htaccessand check forauto_prepend_file,auto_append_fileor handler directives that make new extensions executable. - Scan with signatures. Run the NSA project's YARA rules or your EDR's scanner over the web root and backups. Core rules have few false positives; extended rules surface obfuscated code for manual review.
- Do not rely on modification times. NSA and ASD note attackers may timestomp; use integrity comparison and file-creation events instead.
Web server logs
- Rare-URI analysis: rank PHP URIs by the number of distinct client IPs and user agents. NSA provides a Splunk query and a Python script for Apache logs. Investigate the tail, especially POST requests to files outside normal routes.
- Timeline around entry: for each suspicious file, find the first request to it and the requests just before it from the same source; that often reveals the upload or exploit.
- Control-panel and sFTP logs: uploads by deployment accounts outside release windows, and logins from new locations.
- Retention: make sure access logs are retained and shipped off the host for long enough to cover the likely dwell time.
Processes and the host
- Audit what the web user runs: the NSA project includes Linux auditd guidance for logging commands executed by the Apache user. Apply the same to the PHP-FPM pool users and alert on reconnaissance tools.
- EDR parent-child rules: alert when php-fpm, apache2 or httpd is the parent of a shell or interpreter. Many PHP applications never legitimately do this.
- Network: NSA and ASD recommend looking for unexpected network flows, including hosts answering HTTP that should not be web servers. The project includes a Snort rule for this pattern.
- Persistence outside the web root: cron entries and systemd units created by the web user, SSH keys in its home directory, and new local accounts.
Removing a PHP web shell safely
NSA and ASD are clear that defenders should not stop at removing the shell: they need to determine how far the attacker moved, or incomplete clean-up lets them back in. Coordinate the steps below with the incident lead using the Web Shell Response Playbook.
- Preserve evidence first. Copy the suspicious files, the per-directory config, the web, PHP-FPM and system logs and, where possible, a disk and memory image before changing anything. See the Evidence Handling and Chain of Custody Guide.
- Contain without tipping off. Block the shell's URIs at the WAF or reverse proxy, restrict outbound traffic from the host, and put the site in maintenance mode if needed. Avoid deleting files one at a time while the attacker is still watching.
- Scope the intrusion. Determine the entry route, every file and account the attacker created, what data and secrets they could read as the PHP user, and whether they reached the database server, other sites on the same host, or the internal network.
- Rebuild, do not clean. Redeploy the application from a trusted artefact onto a clean or rebuilt host, rather than editing infected files in place. Restore content (uploads, database) from a backup taken before the earliest evidence of compromise, and check that backup for the same indicators.
- Close the entry route. Patch or remove the vulnerable component, fix the upload handling, or revoke the stolen management keys, before the site goes back online.
- Rotate credentials (next section), then remove CMS accounts, SSH keys, cron jobs and plugins the attacker added.
- Return to service under watch. Turn on heightened monitoring for at least the next few weeks: integrity checks, process alerts and rare-URI reviews daily.
Rollback: keep the preserved compromised image offline. If the rebuild breaks the site, roll back to the previous clean release, never to the compromised copy.
Credentials to rotate
A PHP shell runs as the PHP user and can read everything that user can, which on most sites includes the application's configuration files and environment (ATT&CK T1552.001 Credentials In Files). Assume all of it was read.
| Secret | Usually stored in | Action |
|---|---|---|
| Database credentials | CMS config file, framework .env, environment variables | Create a new database user and password, update the app, drop the old user; review database for added admin rows |
| Application secret keys and salts | Framework app key, CMS authentication salts | Regenerate; this ends existing sessions and invalidates forged cookies |
| Third-party API keys | Plugin settings in the database, config files | Rotate at each provider: payment, email, storage, search, analytics |
| Cloud and storage keys | Config, environment, backup plugin settings | Revoke and reissue; check provider logs for use from unknown addresses |
| sFTP, SSH and control-panel credentials | Hosting account, deployment tools | Reset; replace passwords with keys where possible; remove unknown keys |
| TLS private keys | Web server config readable by the service | Reissue certificates where the key was readable by the PHP user |
| CMS administrator accounts | Database | Reset passwords, end sessions, reset two-factor enrolment, remove unknown admins |
The GoDaddy Managed WordPress Breach 2021 shows the scale of this list on hosting: sFTP, database and admin passwords and some SSL private keys were all exposed and had to be reset or reissued together. Use the Secrets Rotation Runbook Template to work through it, and check the Incomplete rotation breaches guide for what happens when one key is missed.
Where identity fits
Every secret in the table above is a non-human identity: a database account, an API key, a cloud key, a deployment credential. Web shells matter so much because PHP applications hold these identities in readable files. Reduce the blast radius by giving each site its own database user with only the privileges it needs, preferring short-lived or workload-issued cloud credentials over static keys, keeping secrets outside the web root, and inventorying which keys each site holds so rotation is a list, not a search. See the Environment Variables and .env Files guide and the Secrets Management Guide.
Hardening PHP servers against web shells
| Control | Setting or approach | What it stops, and its limits |
|---|---|---|
| Read-only code | Application code owned by a deploy user; PHP user can read but not write the web root | NSA and ASD recommend preventing web applications writing to web-accessible directories. Stops most shell drops; needs a deployment process instead of in-browser updates |
| No PHP in writable directories | Web server rule denying PHP execution in upload, cache and temp paths; uploads stored outside the document root where possible | Stops uploaded files running even if a filter is bypassed |
| Per-directory config locked down | Set user_ini.filename to empty if .user.ini is not used; restrict AllowOverride under Apache | Removes the auto_prepend_file persistence route |
disable_functions | php.ini only, for command-execution functions the application does not use | Raises the bar; PHP's manual warns it can be circumvented and is not sufficient on shared hosting |
open_basedir | Limit file access to the application and its temp directory | The manual calls it "just an extra safety net"; it does not restrict Unix domain sockets |
expose_php = Off | php.ini | Stops advertising the PHP version in headers; reduces targeting, not a fix |
| Separate pools per site | One PHP-FPM pool and Unix user per site | A shell on one site cannot read another site's files or secrets |
| File integrity monitoring | FIM or HIPS rules on web directories, starting in alert mode then blocking | Detects or blocks new files; tune exceptions for legitimate updates |
| WAF with OWASP Core Rule Set | In front of the application | May stop initial exploits and malicious uploads; NSA notes it is unlikely to catch web shell traffic itself |
| Egress restriction | Allow the web server to reach only required destinations | Limits tool downloads and data theft after compromise |
| Fast patching | Automatic updates where safe; supported PHP versions only | NSA and ASD note attackers often exploit web vulnerabilities within 24 hours of release |
See the Web Application Firewall Guide and the File Integrity Monitoring Guide for those controls in depth.
Small sites versus large estates: a single business site on managed hosting can get most of the benefit from read-only code, no PHP in uploads, automatic updates and the host's malware scanning. Agencies and hosting providers running many sites need per-site pools and users, central log collection, fleet-wide integrity checks and a rotation process that can reset credentials for hundreds of sites at once.
Rolling out detection and hardening
- Inventory every PHP site, its PHP version, CMS, plugins, hosting model, and the secrets it holds.
- Baseline each site's code from its deployment artefact and start daily integrity comparison.
- Pilot read-only code and no-PHP-in-uploads on a staging copy; test updates, uploads and admin functions.
- Turn on process and file monitoring in alert-only mode, tune for two to four weeks, then switch file rules to blocking where the deployment process allows.
- Roll out by site group, keeping the previous web server and PHP configuration for rollback.
- Exercise the removal and rotation steps once a year on a test site.
Detection and measurement
| Metric | What it shows | Target |
|---|---|---|
| Sites with daily integrity comparison against a known-good build | Coverage of the strongest detection | 100 percent of internet-facing PHP sites |
| Sites where the PHP user cannot write to code directories | Prevention coverage | Rising each quarter, all new sites |
| PHP files found in upload or cache directories | Exposure to upload-based shells | Zero |
| Web-server-spawned shell alerts investigated within one working day | SOC response to the strongest host signal | 100 percent |
| Secrets rotated after a confirmed shell, as a share of secrets the PHP user could read | Completeness of recovery | 100 percent |
| Sites on supported PHP versions | Patch exposure | 100 percent |
Common failure patterns
- Deleting the file and moving on. The entry route stays open and the attacker's other footholds remain, exactly what NSA and ASD warn against.
- Cleaning only the file system. Backdoor plugins, hidden admin accounts and database-stored code survive a file scan, as the UpdraftCentral case showed.
- Restoring a compromised backup. Restoring from a backup taken after the shell arrived reinstalls it; check the backup date against the earliest evidence.
- Skipping rotation. Database passwords, app keys and API keys in readable config files keep working for the attacker after the shell is gone.
- Trusting disable_functions. PHP's own documentation says it can be circumvented; it is a speed bump, not a wall.
- Shared PHP user across sites. One compromised site on a shared server reads every other site's configuration.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| MITRE ATT&CK | T1505.003, T1190, T1078, T1059.004, T1105, T1552.001, T1070.006 | Web shell persistence, entry routes, command execution, tool transfer, credential theft and timestomping |
| NIST SP 800-53 Rev 5 | SI-7, SI-7(1), CM-5 | Integrity checks on web code; restricting who can change it |
| NIST SP 800-53 Rev 5 | SI-3, SI-4, AU-12 | Malicious code scanning, host and log monitoring |
| NIST SP 800-53 Rev 5 | IR-4, RA-5, SC-7 | Incident handling, vulnerability scanning, egress and boundary controls |
| NIST CSF 2.0 | DE.CM-09, PR.PS-02, RC.RP-01 | Monitoring runtime environments, maintaining software, executing recovery |
| ISO/IEC 27001:2022 Annex A | A.8.7 Protection against malware; A.8.8 Management of technical vulnerabilities; A.5.26 Response to information security incidents | Prevention, patching and response |
Practitioner checklist
- Keep a known-good build for every PHP site and compare production against it daily.
- Make code directories read-only to the PHP user and block PHP execution in upload and cache paths.
- Review every
.user.iniand.htaccessfor prepend, append and handler directives. - Alert when php-fpm, apache2 or httpd spawns a shell or network tool.
- Run rare-URI analysis on access logs weekly and ship logs off the host.
- On a confirmed shell, preserve evidence, scope, rebuild from clean code and restore pre-compromise content.
- Rotate every secret the PHP user could read, and remove attacker accounts, keys, plugins and cron jobs.
- Run one PHP-FPM pool and user per site, and keep PHP on supported versions.
Standards and references
- NSA and ASD: Detect and Prevent Web Shell Malware
- NSA Cybersecurity: Mitigating Web Shells repository
- MITRE ATT&CK: T1505.003 Web Shell
- PHP Manual: Description of core php.ini directives
- PHP Manual: .user.ini files
- NIST: SP 800-61 Rev. 3 Incident Response Recommendations
- NIST: SP 800-53 Rev. 5 Security and Privacy Controls
Related NHI Mgmt Group resources: Web Shell Response Playbook · PHP2Shell · PHP and WordPress Security Guide · Secrets Rotation Runbook Template · File Integrity Monitoring Guide