Detecting and Removing PHP Webshells: Tools, Indicators & Real Case Studies

Compromised PHP sites often hide webshells – small scripts that give attackers remote command execution, file management, database access, and persistence. This guide walks you through how webshells stick around, IOCs to scan for, concrete commands/YARA patterns to detect them, and a step-by-step cleanup methodology you can follow (or hand to your incident responder).

Primary keywords: remove PHP webshell, webshell removal service, php backdoor removal
CTA: Free webshell scan + emergency cleanup quote.


What a Webshell Is (and Why They Persist)

A PHP webshell is a backdoor uploaded or injected into your codebase—often via vulnerable plugins, weak credentials, or insecure upload handlers. Persistence is the attacker’s goal. Common tactics:

  • File scattering & masquerade: random file names in /uploads, /cache, /tmp, or vendor folders; extensions like .php, .phtml, .php5, or even images (.png, .ico) that accept HTTP POST.
  • Obfuscation layers: base64_decode, gzinflate/gzuncompress, str_rot13, XOR loops, long strings concatenated across variables, or compression of payloads.
  • Execution side doors: .user.ini / php.ini with auto_prepend_file or auto_append_file; .htaccess rewrite rules funneling traffic to a loader; include chains via environment-specific configs.
  • Out-of-band persistence: crontab, systemd timers, atd jobs, or PHP sessions that keep re-seeding files; writable plugin/theme updaters; supply-chain via composer packages.
  • Log/noise control: changing timestamps to blend in, or deleting logs.

High-Signal Indicators of Compromise (IOCs)

Look for these fast triage signals:

  • Suspicious PHP functions: eval, assert, preg_replace('/e', ...), system, shell_exec, passthru, popen, proc_open, curl_exec, fsockopen.
  • Obfuscation hints: base64_decode, gzinflate, gzuncompress, str_rot13, unusual variable variables (${"GLOBALS"}[$x]).
  • Unexpected POST targets: image or icon files receiving POSTs (e.g., /uploads/2025/07/logo.png with 200 on POST).
  • Odd file locations/timestamps: new .php in /uploads, /wp-includes, /vendor, /storage, /public, /tmp; recently changed files without a deployment.
  • Persistence files: .user.ini or .htaccess referencing unknown loaders; cron entries executing PHP; systemd --user timers.
  • Access anomalies: admin logins at strange hours/IPs; spikes in 500/403/404; user agents like curl/python-requests hitting .php endpoints.

Shell-Hunter Cheat Sheet (Commands You Can Run)

Run on a copy of the server or after isolating the site. Replace /var/www/html with your docroot.

1) Find “too new” or “odd size” PHP files

# Recently modified PHP under web root (last 7 days)
find /var/www/html -type f -name "*.php" -mtime -7 -printf "%TY-%Tm-%Td %TT %p\n"

# Unusually small or huge PHP files (often loaders or dumps)
find /var/www/html -type f -name "*.php" \( -size -2k -o -size +2M \) -print

2) Grep for dangerous functions/obfuscation

grep -R --line-number --binary-files=without-match -E \
'(eval\s*\(|assert\s*\(|system\s*\(|shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|popen\s*\(|base64_decode\s*\(|gzinflate\s*\(|gzuncompress\s*\(|str_rot13\s*\(|preg_replace\s*\(.*/e)' \
/var/www/html

3) Look for stealthy persistence

# .user.ini or php.ini abuse
grep -R --line-number -E 'auto_(prepend|append)_file' /var/www/html /etc/php* 2>/dev/null

# .htaccess rewrites to unknown loader
grep -R --line-number -E 'RewriteRule|php_value|php_flag' /var/www/html/.htaccess /var/www/html/**/.htaccess 2>/dev/null

# Cron / timers
crontab -l
ls -la /etc/cron.* /var/spool/cron 2>/dev/null
systemctl list-timers --all | grep -i php

4) Scan uploads & odd extensions

# PHP masquerading as images or in uploads
find /var/www/html -type f \( -name "*.php*" -o -iname "*.phtml" -o -iname "*.ico" -o -iname "*.png" \) \
  -path "*/uploads/*" -print

5) YARA snippet for common PHP obfuscation

rule PHP_Webshell_Generic_Obf
{
  meta:
    description = "Generic PHP webshell obfuscation signals"
  strings:
    $a = /base64_decode\s*\(/
    $b = /gzi(nflate|nuncompress|uncompress)\s*\(/
    $c = /preg_replace\s*\(.+\/e.+\)/
    $d = /assert\s*\(/
    $e = /(shell_exec|system|passthru|proc_open|popen)\s*\(/
  condition:
    uint16(0) == 0x3c3f and 2 of ($a,$b,$c,$d,$e)
}

Step-by-Step Cleanup Methodology

  1. Isolate & Stabilize
  • Switch to maintenance mode, block logins and file uploads, restrict by IP if possible.
  • Put a WAF/CDN in “high security” to cut active command/control attempts.
  1. Snapshot & Preserve Evidence
  • Take an image/backup of files + DB + logs. Note server time, timezone, and versions.
  • Document findings (file hashes, paths) for RCA and insurance.
  1. Enumerate IOCs
  • Run the grep/find triage above; review web server logs for POSTs to strange endpoints; check .user.ini, .htaccess, cron, timers, and writable dirs (/uploads, /tmp, cache dirs).
  1. Clean by Replacement, Not Surgery
  • Replace core CMS files (WordPress/Drupal/etc.) from vendor packages.
  • Replace themes/plugins from trusted sources—do not “edit out” malicious lines and keep the rest.
  • Remove unknown files; quarantine suspicious ones.
  • Clear composer/vendor and reinstall with locked hashes (composer install --no-dev --prefer-dist --no-scripts if applicable).
  • Purge caches (OPcache, application cache, CDN).
  1. Database & User Accounts
  • Search DB for malicious wp_options autoload payloads, rogue admin users, injected templates, webhooks.
  • Remove unknown admin accounts; rotate all credentials (DB, SFTP/SSH, control panel, CMS).
  • Invalidate sessions and reset salts/keys.
  1. Re-test & Restore Functionality
  • Run YARA/grep again; confirm no suspicious hits.
  • Browse critical paths, submit test forms, and watch logs for anomalies.
  • Bring uploads/logins back online.
  1. Post-Incident Hardening
  • Enforce least privilege (no write perms on code dirs, separate writable uploads).
  • Add a WAF with virtual patching, bot controls, and rate limits .
  • Enable automatic backups (daily for content sites, hourly for stores) and test restores quarterly.
  • Implement a patch cadence with staging & rollback.

Mini Case Studies

Case #1 — Hidden in Uploads
A brochure WP site intermittently redirected mobile users. Triage found a 1.2 KB invoice.php inside /wp-content/uploads/2025/09/ plus .user.ini setting auto_prepend_file=./invoice.php. Replacing core + themes/plugins, deleting uploads-PHP, and removing .user.ini ended reinfections. Adding WAF rules blocked follow-up POSTs to image paths.

Case #2 — Vendor Library Backdoor
Custom app with a bundled vendor/ shipped a tainted utility containing an obfuscated eval(gzinflate(base64_decode(...))). Re-installing vendors from clean composer lock, restricting write perms, and pinning package versions eliminated persistence. A scheduled YARA scan catches similar patterns now.

Case #3 — Cron-Seeded Loader
Attackers planted /tmp/.k.php and a cron job */10 * * * * php /tmp/.k.php to restore shells under /public/. After removal, the reinfection loop persisted until cron, timers, and at-jobs were scrubbed and SSH keys rotated.


Quick “Do/Don’t” Checklist

Do

  • Replace from known-good sources; keep quarantined copies for RCA.
  • Rotate all secrets; enforce MFA.
  • Separate writable directories from code; deploy read-only where possible.
  • Add WAF, rate limits, and security headers.

Don’t

  • Edit malicious lines and keep the file. Replace it.
  • Assume one shell = one problem. Expect multiple persistence points.
  • Forget to test restores—untested backups are wishful thinking.

Need Help Fast?

Get a emergency cleanup quote. We’ll assess indicators, confirm compromise, and give you a fixed-price, time-boxed remediation plan—plus hardening so it doesn’t happen again.