Hacked site on Apache: contain, investigate, clean and recover
If your site is hacked, the order of steps matters more than speed. Stop the damage, save the evidence, find out how the attacker got in, then rebuild from a clean state and close that hole. If you skip straight to deleting suspicious files, you destroy the clues, and the attacker usually comes back through the same door.
Signs of a compromise
- Visitors from search engines or on phones are redirected to spam or scam sites, while you see a normal site when you type the address.
- Pages you never wrote appear in search results (try
site:example.comwith spam words), or spam links appear in your pages. - Google Search Console's Security Issues report, or a browser, warns about your site.
- Unknown PHP files appear in upload or cache folders, or .htaccess files appear or change.
- Unexplained CPU load, outgoing traffic, mail sent from the server, or processes running as
www-data/apachethat you do not recognize.
Google's report sorts hacked content into three kinds: code injection (malicious code in your pages, often redirects), content injection (spam text or links added to your pages) and URL injection (new spam pages created on your site).
1. Contain the damage
Take the site out of public view, but keep the server running so you do not lose evidence. Answer every visitor except you with 503 Service Unavailable, the HTTP status for a temporary outage. In the site's virtual host:
RewriteEngine On
RewriteCond "%{REMOTE_ADDR}" "!=203.0.113.10"
RewriteRule "^" "-" [R=503,L]
ErrorDocument 503 "Site under maintenance"
Header always set Retry-After "3600"
Replace 203.0.113.10 with your own IP. This needs mod_rewrite and mod_headers. Then run sudo apachectl configtest and reload. If you suspect the attacker has a shell on the server itself (unknown processes, new users, changed system files), go further. Block outgoing traffic, or take the machine off the network through your provider's console, and treat the whole server as untrusted.
2. Preserve evidence
Copy the logs and the site files before you change anything. Log rotation deletes old logs on a schedule, so do this first.
sudo mkdir -m 700 /root/incident
# a list of every file with modify time, change time, owner and mode
sudo find /var/www/example -printf '%TY-%Tm-%Td %TH:%TM %CY-%Cm-%Cd %CH:%CM %u %m %p\n' \
| sudo tee /root/incident/file-times.txt > /dev/null
sudo tar -czf /root/incident/www.tar.gz /var/www/example
sudo tar -czf /root/incident/logs.tar.gz /var/log/apache2 /var/log/auth.log* # RHEL: /var/log/httpd /var/log/secure*
sudo sh -c 'cd /root/incident && sha256sum *.tar.gz file-times.txt > SHA256SUMS'
On systems that log only to the systemd journal (Debian 12 and later do not install rsyslog by default, so there may be no auth.log), also save the journal with sudo journalctl --since "30 days ago" | sudo tee /root/incident/journal.txt > /dev/null. Copy /root/incident to another machine. If the server is a virtual machine, take a snapshot as well. Do not reboot first, because running processes and network connections disappear on reboot. Note down times and what you saw as you go. If you need professional incident response, or must report the breach, this record is what they will ask for.
3. Find out how they got in
Recently changed files
# content modified in the last 14 days
sudo find /var/www/example -type f -mtime -14 -ls
# inode changes in the last 14 days (harder to fake than mtime)
sudo find /var/www/example -type f -ctime -14 -ls
Attackers can reset a file's modification time with touch, but they cannot easily set the change time (ctime). A file with an old mtime and a recent ctime is suspicious.
Web shells and injected code
# scripts in folders that should hold only uploads (also catches x.php.jpg)
sudo find /var/www/example/uploads -type f -iname '*.php*'
# common obfuscation patterns
sudo grep -RlE --include='*.php*' \
'eval[[:space:]]*\([[:space:]]*(base64_decode|gzinflate|str_rot13|gzuncompress)|assert[[:space:]]*\([[:space:]]*\$_(POST|GET|REQUEST|COOKIE)' \
/var/www/example
Expect some false positives, and do not expect to find everything. Web shells are often heavily disguised. Compare suspicious files with a clean copy of the same CMS version, plugin or theme instead of judging each file by eye.
.htaccess injections and redirects
A favorite trick is an .htaccess file that redirects only visitors who arrive from a search engine, or only mobile browsers, so the owner never sees it:
sudo find /var/www -name '.htaccess' -ctime -30 -ls
sudo grep -RniE --include='.htaccess' \
'HTTP_REFERER|HTTP_USER_AGENT|auto_prepend_file|auto_append_file|AddHandler|SetHandler|php_value' /var/www
Look for RewriteCond %{HTTP_REFERER} lines matching google or bing, handlers that make image files run as PHP, and auto_prepend_file settings that load a hidden script before every page. Check the server configuration too. A changed vhost or an unknown LoadModule line means the attacker had root.
sudo find /etc/apache2 /etc/httpd -type f -mtime -30 -ls 2>/dev/null
sudo apachectl -M
The access log
Find the first request to each malicious file. The lines just before it often show the hole: a vulnerable plugin URL, an upload form, a stolen admin login.
# POST targets, most frequent first (combined log format)
sudo awk '$6 == "\"POST" {print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -30
# every request to a shell you found, including rotated logs
sudo zgrep -h '/uploads/2026/10/img.php' /var/log/apache2/access.log* | head
Note the attacker's IP addresses and search the logs for everything else they requested. Log analysis has more techniques.
The rest of the server
ps -u www-data -o pid,lstart,cmd # apache on RHEL
sudo crontab -l -u www-data
sudo ls -la /etc/cron.d /var/spool/cron
sudo ss -tnp
sudo debsums -c # Debian, Ubuntu (apt install debsums)
sudo rpm -Va # RHEL family; changed config files are normal
Programs started by the web user, cron jobs it owns, or changed system binaries mean the attack went beyond the website. In that case, rebuild the server.
4. Clean or rebuild
- Restore from a backup taken before the first malicious change, which is why step 3 needs the dates. Then reapply only content you trust, such as database exports, after checking them.
- Reinstall code from official sources: download the CMS core, plugins and themes again rather than "cleaning" the infected copies. Remove plugins you do not use.
- Check the database for injected scripts in posts or options, and for admin accounts you did not create.
- If the attacker had root, or you cannot tell, reinstall the operating system on a fresh server and move only clean data across. You cannot reliably clean a rooted machine.
5. Close the hole
- Update Apache, PHP, the CMS and every plugin. Check your Apache version against Vulnerabilities (CVEs).
- Deny scripts in upload folders and switch off .htaccess where you can (
AllowOverride None). See Hardening checklist. - Make code files unwritable by the web server user, so a future bug cannot plant files.
- Consider a WAF in front of the application (ModSecurity WAF).
6. Rotate every secret
Assume the attacker read everything the web server could read. Change:
- CMS admin and user passwords, and end all sessions (WordPress: new salts in
wp-config.php). - Database passwords in config files and
.envfiles. - API keys, SMTP passwords and payment-gateway keys stored on the server.
- SSH keys and passwords, hosting control panel and FTP accounts.
- The TLS private key, if the attacker had root or could read it. Revoke the certificate and get a new one with a new key (see HTTPS and TLS).
7. Go back online and keep watching
Remove the 503 rule, reload, and test the site as a visitor from a search engine and on a phone. If Google flagged the site, fix every listed page, then choose Request Review in the Security Issues report. Explain what happened, what you fixed and the result. Google says reviews take from a few days to a few weeks. Do not resubmit while a request is pending.
For the following weeks, watch the access and error logs for the attacker's IPs and URLs, and run the find commands above again. If personal data may have been exposed, check your legal duty to notify users or a data-protection authority. Many laws set short deadlines. Ask a lawyer or the relevant regulator, because this site cannot give legal advice.
Mistakes to avoid. Deleting files before copying the logs. Restoring a backup that already contains the back door. Cleaning the site but not updating the plugin that let the attacker in. Changing the CMS password but not the database password the attacker read from the config file. Going back online the same day without knowing how the break-in happened.
Sources
- Security Tips (Watching Your Logs, permissions), Apache HTTP Server 2.4 documentation
- Apache core features (AllowOverride, Options), httpd 2.4 documentation
- Security Issues report, Google Search Console Help
- Malware and unwanted software, Google Search Central
- Apache HTTP Server 2.4 vulnerabilities, Apache HTTP Server Project
- Debian 12 release notes: rsyslog no longer installed by default, Debian