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
withauto_prepend_file
orauto_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
with200
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
- 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.
- 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.
- 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).
- 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).
- 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.
- 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.
- 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.