Eradicating web shells and hardening web servers
Remove web shells without leaving the intruder a way back, then harden: safe upload handling, no-exec directories for nginx, Apache and IIS, WAF, least privilege and application allow-listing.
Deleting the file is the easy part. Real eradication means removing every foothold, closing the door the intruder used, invalidating what they stole, and making the next attempt fail or get caught quickly. This guide covers both halves: the response and the long-term hardening.
Part 1 — Eradication
Preconditions
Do not start until the investigation has answered, with reasonable confidence:
- How the shell was planted (the vulnerability or credential).
- Every shell and persistence mechanism found across the estate.
- Which credentials and secrets were exposed.
- Whether the intruder moved beyond the web tier.
Then execute eradication as a single coordinated action. Removing footholds one at a time over several days warns the intruder and gives them time to re-establish access.
Rebuild or clean?
| Factor | Favours rebuild | Favours clean-in-place |
|---|---|---|
| Intruder privilege | SYSTEM/root or admin achieved | Confined to low-privilege worker identity |
| Scope confidence | Gaps in telemetry or logs | Full EDR and log coverage for the period |
| Persistence found | Services, tasks, modules, accounts | Only the shell file(s) |
| Deployment maturity | Server can be redeployed from pipeline/IaC | Legacy, hand-configured server |
| Appliance | Vendor recommends factory reset / reimage | Vendor provides verified clean procedure |
When in doubt, rebuild from known-good media and redeploy the application from source control — never from a backup of the compromised webroot, which may carry the shell.
Eradication checklist
- Close the entry point — patch the vulnerability, disable the abused feature, or remove the compromised credential before or simultaneously with removal.
- Remove all shells and implants — files, injected code, malicious config directives, registered IIS/Apache modules, Java agents/filters, scheduled tasks, services, cron jobs, startup items.
- Remove attacker accounts — local, directory and application-level (CMS admins).
- Rotate secrets — service account passwords, database credentials, API keys, cloud keys, TLS keys if exposed; for ASP.NET and SharePoint, rotate machine keys, since stolen keys allow forged ViewState payloads to regain code execution even after the shell is gone.
- Invalidate sessions and tokens issued while the intruder was present.
- Verify — rerun hash/YARA sweeps, confirm FIM baseline, and watch process and log detections closely for at least several weeks.
Part 2 — Hardening
The goal is to break the write or execute precondition described in how web shells get planted, and to make any remaining attempt noisy.
Secure upload handling
| Control | Why |
|---|---|
| Store uploads outside the webroot or in object storage | Uploaded content can never be requested as code |
Serve downloads through a handler with Content-Type from an allow-list and Content-Disposition: attachment where appropriate | Browser and server never treat files as executable |
| Allow-list extensions and validate content (magic bytes, decode images server-side) | Deny-lists always miss an alias |
| Rename to random identifiers; discard user-supplied names | Prevents extension and path tricks |
| Enforce size limits and scan with AV/CDR | Limits abuse and catches known malware |
Set X-Content-Type-Options: nosniff | Prevents MIME sniffing of served files |
No-execute upload directories
If uploads must live under the webroot, ensure the server will never execute scripts there.
nginx (PHP handled via FastCGI) — deny script execution in upload paths before the generic PHP location:
location ^~ /uploads/ {
# Serve as static files only; never pass to PHP-FPM
location ~* \.(php|phtml|phar|php\d)$ { return 403; }
try_files $uri =404;
}
Also set cgi.fix_pathinfo=0 in php.ini (or ensure try_files $uri =404; in the PHP location) so crafted paths cannot be routed to the interpreter.
Apache — disable the PHP engine and handlers in the upload directory:
<Directory "/var/www/example/uploads">
Options -ExecCGI -Indexes
AllowOverride None
<IfModule mod_php.c>
php_admin_flag engine off
</IfModule>
<FilesMatch "\.(php|phtml|phar|php\d|pl|py|cgi|sh)$">
Require all denied
</FilesMatch>
</Directory>
AllowOverride None also prevents an uploaded .htaccess from re-enabling execution.
IIS — remove script handlers and block executable extensions for the upload folder, and set its access policy to read-only:
<!-- web.config placed in the upload directory (or a <location> in the site's web.config) -->
<configuration>
<system.webServer>
<handlers accessPolicy="Read" />
<security>
<requestFiltering>
<fileExtensions allowUnlisted="true">
<add fileExtension=".aspx" allowed="false" />
<add fileExtension=".ashx" allowed="false" />
<add fileExtension=".asmx" allowed="false" />
<add fileExtension=".asp" allowed="false" />
<add fileExtension=".config" allowed="false" />
</fileExtensions>
</requestFiltering>
</security>
</system.webServer>
</configuration>
Lock the handlers section at server level where possible so a dropped web.config cannot override it. Tomcat: keep the default servlet readonly (true by default) and never deploy with PUT enabled.
Read-only code, least-privilege runtime
The runtime identity should read code and write only to specific data directories.
| Platform | Practice |
|---|---|
| Linux | Code owned by a deploy user (e.g. root:deploy, 0755/0644); web user (www-data, nginx) has write access only to upload/cache dirs; consider read-only bind mounts or immutable container images |
| IIS | Use per-site ApplicationPoolIdentity; grant IIS AppPool\<name> read on code, modify only on data folders; never run pools as LocalSystem |
| Java | Run the servlet container as a dedicated unprivileged user; make webapps/ non-writable at runtime; disable auto-deploy in production |
| Containers | Read-only root filesystem (readOnlyRootFilesystem: true in Kubernetes), tmpfs for scratch, drop capabilities, run as non-root |
With code directories read-only, many exploitation paths can no longer persist a file at all — and attempts show up as access-denied errors worth alerting on.
Application allow-listing and confinement
Stop worker processes from launching interpreters, turning the process-tree detection into prevention:
- Windows Defender Application Control (WDAC) / AppLocker — restrict what can execute on web servers; Microsoft publishes recommended block rules for commonly abused binaries. Attack Surface Reduction rules in Defender for Endpoint add further web-shell-relevant protections on supported servers.
- SELinux — keep
httpd_tconfined; leave booleans likehttpd_execmemandhttpd_unifiedoff unless required; label upload directorieshttpd_sys_rw_content_tand codehttpd_sys_content_tso the server can write only where intended. - AppArmor — profile PHP-FPM or the application runtime to deny
ix/pxexecution of shells and utilities. - systemd sandboxing —
NoNewPrivileges=yes,ProtectSystem=strictwith explicitReadWritePaths=,PrivateTmp=yesfor the application service. - PHP — use
disable_functionsto remove process-execution functions the application does not need, andopen_basedirto confine file access. These are defence-in-depth, not a boundary.
Web application firewall
A WAF (for example ModSecurity or Coraza with the OWASP Core Rule Set, or a cloud WAF) contributes:
- Virtual patching — block exploitation of a known vulnerability while the real patch is tested.
- Upload inspection — reject requests uploading script content or disallowed extensions.
- Anomaly scoring on command-injection and code-injection patterns.
Limitations to plan for: TLS must be terminated at or before the WAF; encrypted or custom-encoded payloads to an already-planted shell are not reliably detected; and aggressive rules need tuning to avoid breaking the application. Treat WAF logs as a detection source and forward them to your SIEM.
Reduce exposure
- Keep management interfaces (CMS admin, server consoles, appliance admin pages) off the internet — behind VPN, zero-trust access or IP restrictions — and enforce MFA.
- Remove unused features: WebDAV, sample applications, default pages, unused handlers and virtual directories.
- Maintain an inventory of internet-facing applications and appliances with owners and patch SLAs; treat known-exploited vulnerabilities as emergencies.
Detection as part of hardening
Hardening is complete only when failures are visible:
| Control | Signal to alert on |
|---|---|
| Read-only code | Access-denied writes by worker identity |
| No-exec uploads | Requests for script extensions under upload paths |
| Allow-listing | Blocked child process of a worker |
| FIM | Any executable file change outside a deployment |
| WAF | Blocked uploads and injection attempts, especially followed by a success |
Hardening quick reference
| Layer | Minimum baseline |
|---|---|
| Patching | Emergency process for known-exploited CVEs on internet-facing systems |
| Uploads | Outside webroot, allow-listed, renamed, served as static |
| Filesystem | Code read-only to runtime; write only to data dirs |
| Runtime | Dedicated low-privilege identity; no LocalSystem/root |
| Execution | WDAC/AppLocker or SELinux/AppArmor preventing worker → shell |
| Perimeter | WAF with virtual patching; admin interfaces not public |
| Monitoring | FIM, central web logs, process telemetry, WAF logs |
None of these controls is sufficient alone. Together they make a web shell hard to plant, hard to use and quick to find.