Home
Explore
Contact
Sign in
Emergency Fixes & Security

Trace and Block PHP Webshell Malware on Your Server

Persistent PHP backdoors keep rewriting your site? Learn how to SSH in, watch public folders with inotifywait, pin the writer with lsof, and stop immortal malware loops—practical steps for Australian site owners.

Fixwebnode Support
Fixwebnode Support
11 min read 18 views
Trace and Block PHP Webshell Malware on Your Server

If your homepage keeps getting reinfected, odd PHP files reappear after you delete them, or CPU spikes while nothing legitimate is running, you are dealing with persistent website malware—not a one-off bad upload. This guide walks Australian homeowners and small-business operators through live file monitoring, process isolation, and safe cleanup on a Linux host you control. Fixwebnode provides direct remote website support for this exact problem; start at website repair across Australia when you need a specialist on the session with you.

Scope assumes SSH root or sudo on a typical LEMP/LAMP VPS (Ubuntu/Debian-style paths), PHP-FPM or Apache PHP, and a public web root such as /var/www/html or a vhost document root. You will deploy inotifywait, correlate writers with lsof, terminate unauthorized loops, harden write paths, and verify the infection is gone—not craft or test attack payloads.

Why immortal PHP backdoors matter on Australian sites

“Immortal” malware is usually a combination of a webshell dropper, a cron or long-running PHP/CLI process that rewrites files, and weak permissions on the public tree. Deleting the visible shell is not enough if something still holds a write handle or respawns from /tmp, a compromised plugin, or a stolen FTP/SFTP credential. Under pressure you need kernel-level create/modify events on the document root, then the exact PID so you can stop the writer before you restore clean code.

Remote delivery fits most Australian operators: diagnostics run over SSH; Fixwebnode works nationwide as a direct specialist provider (see all service areas), including deeper WordPress and legacy PHP work when the infection is tangled with outdated stacks.

How do I find which process keeps dropping PHP webshells on my Australian VPS?

SSH in as root or a sudo user, watch the public web root with inotifywait for create and modify events, then run lsof on the path that just changed to get the PID and kill only that unauthorized writer after you confirm it is not your deploy user or PHP-FPM pool master doing a legitimate request. If shells return after reboot or from multiple directories, book Fixwebnode for a full remote purge and permission lock-down rather than repeating partial deletes.

SymptomQuick defensive checkCall Fixwebnode when
PHP file reappears minutes after deleteinotifywait on docroot + lsof on new pathMultiple writers, unknown binaries, or reinfect after reboot
High CPU, odd php/bash under www-dataps/lsof -p, inspect cwd and open filesProcess names spoof system tools or hide in /tmp
New eval/base64 files in uploads or cacheRead-only quarantine, grep indicators, fix permsCMS core is rewritten or admin users were added

Common issues with persistent webshells and file rewrite loops

1. Dropper rewrites index.php or a hidden .php in uploads within minutes

You remove a suspicious file; shortly a near-identical shell or a one-line eval(base64_decode(...)) loader returns. Often a background PHP CLI script, compromised cron, or hijacked CMS task still has write access to the document root.

2. Unauthorized process loop holds open handles on public folders

Load is high, ps shows repeated short-lived php or sh -c children, and static assets change mtime without deployments. The writer may chdir into uploads or cache and recreate shells faster than manual cleanup.

3. “Clean” restore fails because permissions and sticky drop paths remain

You restore from backup, but www-data (or the PHP-FPM user) still owns world-writable directories, so the next hit on a residual mailer or plugin dropper reinfects. No live monitor was left running, so you never saw the first rewrite.

4. Webshell only appears under obscure paths (cache, old backups, unused themes)

Scanners miss files outside the theme you checked. Attackers favour wp-content/uploads, tmp inside the vhost, and forgotten .bak copies that remain executable by the web server.

Prerequisites before you touch production

Take a filesystem snapshot or tarball of the vhost first. Prefer read-only investigation until you know the PID. Do not run untrusted PHP from the infected tree. Confirm you have console or out-of-band access if you restart PHP-FPM or the web server.

sudo mkdir -p /root/incident-$(date +%F)
sudo tar -C /var/www -czf /root/incident-$(date +%F)/www-pre-clean.tgz html
# adjust html to your actual document root name

Issue 1 — Live-trace file creates and modifies with inotifywait

Goal: see creations and modifications in public folders as they happen so you stop guessing which path the dropper uses.

Step 1 — Install inotify tools

sudo apt-get update
sudo apt-get install -y inotify-tools lsof psmisc

Step 2 — SSH and identify the real document root

sudo nginx -T 2>/dev/null | grep -E 'root |server_name'
# or for Apache:
sudo apache2ctl -S 2>/dev/null
ls -la /var/www/

Step 3 — Deploy a live watch on public trees (recursive)

DOCROOT=/var/www/html
sudo inotifywait -m -r -e create,modify,move,delete \
 --timefmt '%F %T' --format '%T %e %w%f' \
 "$DOCROOT" \
 | tee /root/incident-$(date +%F)/inotify-docroot.log

Leave this session open. In another SSH window, browse the site or wait. When a line shows CREATE or MODIFY on a .php under uploads or an unexpected path, note the full path immediately.

Step 4 — Optional focused watch on high-risk folders only

sudo inotifywait -m -r -e create,modify \
 /var/www/html/wp-content/uploads \
 /var/www/html/wp-content/cache \
 /tmp /var/tmp \
 | tee /root/incident-$(date +%F)/inotify-hotspots.log

Verification: touch a test file as your deploy user; you should see one event. Then delete the test file. Real malware events often create random short names or modify index.php without your deploy key in the process tree.

When to call Fixwebnode: events fire from dozens of paths at once, or creates continue after you stop PHP-FPM—suggests a non-PHP binary or second host path. Book remote incident help via Fixwebnode website repair.

Issue 2 — Isolate the exact PID with lsof and terminate the writer

Goal: map the path from inotify to a process, confirm it is unauthorized, then stop the loop without indiscriminate kills.

Step 1 — On the path that just changed, list writers

SUSPECT=/var/www/html/wp-content/uploads/odd-shell.php
sudo lsof -nP "$SUSPECT"
sudo lsof -nP +D /var/www/html/wp-content/uploads 2>/dev/null | head -n 50

Step 2 — Inspect the PID

PID=12345 # replace with PID from lsof
sudo ps -fp "$PID"
sudo ls -l /proc/"$PID"/exe /proc/"$PID"/cwd
sudo tr '\0' ' ' < /proc/"$PID"/cmdline; echo
sudo lsof -nP -p "$PID" | head -n 80

Legitimate PHP-FPM workers serve requests briefly; long-lived CLI PHP with cwd inside uploads, network sockets to unknown hosts, or exe under /tmp is hostile. Record PID, PPID, and cmdline in your incident folder before killing.

Step 3 — Terminate only the confirmed malicious PID (then its parent if it respawns)

sudo kill -TERM "$PID"
sleep 2
sudo kill -KILL "$PID" 2>/dev/null
# if it respawns, inspect parent:
sudo pstree -ap | head -n 100
# stop a bad user cron temporarily only after review:
sudo crontab -u www-data -l
sudo ls -la /etc/cron.* /var/spool/cron/crontabs

Step 4 — Quarantine the dropped file (do not execute it)

sudo mkdir -p /root/incident-$(date +%F)/quarantine
sudo mv "$SUSPECT" /root/incident-$(date +%F)/quarantine/
sudo chmod 000 /root/incident-$(date +%F)/quarantine/*
sudo grep -R --include='*.php' -nE 'eval\s*\(|base64_decode\s*\(|gzinflate\s*\(|shell_exec\s*\(|passthru\s*\(' /var/www/html 2>/dev/null | head -n 40

Step 5 — Restart application runtimes cleanly after the writer is gone

sudo systemctl restart php8.2-fpm 2>/dev/null || sudo systemctl restart php-fpm
sudo systemctl reload nginx 2>/dev/null || sudo systemctl reload apache2
sudo systemctl status php8.2-fpm nginx --no-pager 2>/dev/null

Adjust the PHP-FPM unit name to what systemctl list-units 'php*' shows on your host.

When to call a pro: cmdline impersonates nginx/apache, the binary is deleted on disk (deleted exe still running), or kill storms dozens of PIDs. That is full incident response territory for Fixwebnode, not endless manual whack-a-mole.

Issue 3 — Break reinfection: permissions, crons, and residual drop paths

Goal: after the live writer is stopped, remove persistence so restores stay clean.

Step 1 — Find world-writable web paths

sudo find /var/www/html -type d -perm -0002 -ls 2>/dev/null
sudo find /var/www/html -type f -name '*.php' -perm -0002 -ls 2>/dev/null

Step 2 — Tighten ownership (example: deploy user + PHP group; adapt names)

sudo chown -R deploy:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;
# only keep write where the app truly needs it, e.g. uploads—not theme PHP

Step 3 — Hunt persistence outside the docroot

sudo grep -R --line-number -E 'curl\|wget\|php -r|/var/www' /var/spool/cron /etc/cron.d /etc/crontab 2>/dev/null
sudo ls -la /tmp /var/tmp | head
sudo find /tmp /var/tmp -type f \( -name '*.php' -o -name '*.so' \) -ls 2>/dev/null

Step 4 — Re-run monitor for a soak period

sudo timeout 600 inotifywait -m -r -e create,modify /var/www/html \
 --format '%T %e %w%f' --timefmt '%F %T' \
 | tee /root/incident-$(date +%F)/soak.log

No unexpected PHP creates for the soak window is a minimum bar before you call the incident closed.

When DIY is not enough: core CMS files differ from a known-good release, admin users you did not create appear, or mail spam continues—schedule a structured purge and hardening pass.

Issue 4 — Explicit PHP backdoor patterns: locate, quarantine, verify

Goal: systematically find classic webshell indicators without executing them.

Step 1 — Name and time anomalies

sudo find /var/www/html -type f -name '*.php' -mtime -3 -ls
sudo find /var/www/html -type f \( -name '*.php5' -o -name '*.phtml' -o -name '.*.php' \) -ls

Step 2 — Content indicators (read-only grep)

sudo grep -R --include='*.php' -nE 'FilesMan|c99|r57|assert\s*\(|preg_replace\s*\(.*/e|\\x[0-9a-fA-F]{2}' /var/www/html 2>/dev/null | head -n 50

Step 3 — Confirm web server error and access clues

sudo tail -n 200 /var/log/nginx/error.log 2>/dev/null
sudo tail -n 200 /var/log/apache2/error.log 2>/dev/null
sudo grep -E '\.php' /var/log/nginx/access.log 2>/dev/null | awk '{print $7}' | sort | uniq -c | sort -rn | head

Spike URLs under uploads that return 200 are strong leads. Quarantine matching files, redeploy clean application code from version control or a trusted package, then rotate CMS and panel passwords and invalidate all sessions.

When DIY is enough vs when to book Fixwebnode

DIY is reasonable when: you have SSH, a single obvious drop path, inotify shows one writer, lsof names a clear PID, quarantine plus permission fixes stop new creates during a soak watch, and CMS core matches a clean copy.

Book Fixwebnode when: reinfection survives reboot; binaries run from deleted inodes; multiple vhosts share a compromised user; WordPress/legacy PHP needs coordinated upgrade and audit; or you cannot afford prolonged downtime while learning the toolchain under pressure. Fixwebnode is a direct specialist for remote website support—not a freelance marketplace. Deep stack work is available through the same practice, including WordPress PHP 8+ upgrade and legacy fixes with Carlton site security focus and Melbourne CBD legacy code refactoring and security audits when outdated code is part of how the shell keeps landing.

Troubleshooting quick reference

inotifywait: command not found → install inotify-tools as above.

inotify queue overflow → narrow watches to uploads/cache; raise fs.inotify.max_user_watches only if you understand the host baseline:

cat /proc/sys/fs/inotify/max_user_watches
# temporary raise example (review permanence later):
echo 524288 | sudo tee /proc/sys/fs/inotify/max_user_watches

lsof +D is slow → use the exact file path from inotify instead of walking the whole tree.

Killing PHP-FPM workers does nothing lasting → the parent pool or a cron re-spawns work; fix persistence and code, then restart the pool unit once.

Closing: get remote help on the incident

Persistent PHP webshells fall to a simple pressure workflow: snapshot, watch the public tree with inotifywait, pin the writer with lsof, terminate the unauthorized PID, quarantine indicators, lock permissions, and soak-test until creates stop. Australian sole traders and small teams can finish many single-vector cases with that runbook; multi-vector or business-critical sites should not stay half-clean.

Ready to walk through a live session on your server? Start a conversation with Fixwebnode via https://fixwebnode.com.au/website-repair-australia and outline the symptoms (reinfection timing, hosting type, CMS). We work directly with individuals, sole traders, and local operators across Australia—practical remote website support until the rewrite loop is actually dead.

Share this article
Fixwebnode Support
Fixwebnode Support

Hey there!
I am your assistant for Fixwebnode. Ask about our services, quotes, packages, orders, or how to get support.
While you wait
What’s your name and best email? We’ll reply even if you leave.