Skip to content

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.

Published on 7 min read

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?

FactorFavours rebuildFavours clean-in-place
Intruder privilegeSYSTEM/root or admin achievedConfined to low-privilege worker identity
Scope confidenceGaps in telemetry or logsFull EDR and log coverage for the period
Persistence foundServices, tasks, modules, accountsOnly the shell file(s)
Deployment maturityServer can be redeployed from pipeline/IaCLegacy, hand-configured server
ApplianceVendor recommends factory reset / reimageVendor 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

  1. Close the entry point — patch the vulnerability, disable the abused feature, or remove the compromised credential before or simultaneously with removal.
  2. 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.
  3. Remove attacker accounts — local, directory and application-level (CMS admins).
  4. 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.
  5. Invalidate sessions and tokens issued while the intruder was present.
  6. 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

ControlWhy
Store uploads outside the webroot or in object storageUploaded content can never be requested as code
Serve downloads through a handler with Content-Type from an allow-list and Content-Disposition: attachment where appropriateBrowser 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 namesPrevents extension and path tricks
Enforce size limits and scan with AV/CDRLimits abuse and catches known malware
Set X-Content-Type-Options: nosniffPrevents 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.

PlatformPractice
LinuxCode 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
IISUse per-site ApplicationPoolIdentity; grant IIS AppPool\<name> read on code, modify only on data folders; never run pools as LocalSystem
JavaRun the servlet container as a dedicated unprivileged user; make webapps/ non-writable at runtime; disable auto-deploy in production
ContainersRead-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_t confined; leave booleans like httpd_execmem and httpd_unified off unless required; label upload directories httpd_sys_rw_content_t and code httpd_sys_content_t so the server can write only where intended.
  • AppArmor — profile PHP-FPM or the application runtime to deny ix/px execution of shells and utilities.
  • systemd sandboxing — NoNewPrivileges=yes, ProtectSystem=strict with explicit ReadWritePaths=, PrivateTmp=yes for the application service.
  • PHP — use disable_functions to remove process-execution functions the application does not need, and open_basedir to 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:

ControlSignal to alert on
Read-only codeAccess-denied writes by worker identity
No-exec uploadsRequests for script extensions under upload paths
Allow-listingBlocked child process of a worker
FIMAny executable file change outside a deployment
WAFBlocked uploads and injection attempts, especially followed by a success

Hardening quick reference

LayerMinimum baseline
PatchingEmergency process for known-exploited CVEs on internet-facing systems
UploadsOutside webroot, allow-listed, renamed, served as static
FilesystemCode read-only to runtime; write only to data dirs
RuntimeDedicated low-privilege identity; no LocalSystem/root
ExecutionWDAC/AppLocker or SELinux/AppArmor preventing worker → shell
PerimeterWAF with virtual patching; admin interfaces not public
MonitoringFIM, 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.