Let's assume that you inherited hundreds of WordPress sites installed over 4 hosts during last 20 years and that from time to time these WordPress installations gets infected by malicious actors who somehow acquired the password of an administrator user on the installation and are using these newly acquired privileges to install additional plugins, create new users and insert spam in Google (using User-Agent to emit spam content to Google and normal content to usual pages).
You are also not an administrator on those sites, so your tool of choice is WP-CLI and command-line together with some code snippets which extend it's functionality described here.
Logging user logins
The first logical question is: how can I know which user logged in into WordPress and infected it? Unfortunately, WordPress doesn't emit any log files.
wp-fail2ban
Several year ago, I found another plugin which sends logs to syslog
WP-Fail2Ban.
However, this plugin tries to insert the site name into the syslog tag field which is limited
in length, so generated logs are less useful if the site name gets truncated.
Newer version are also overly complex, since they include full WordPress interface,
with additional tables in each word press installation which we don't want or need.
So I decided to keep using an older, simpler version of WP-Fail2Ban before it had any interface,
which is simple enough to audit and modified it to produce
more useful syslog messages and implement everything in a
single PHP file wp-fail2ban-ffzg.php.
Logs are then sent to central syslog server, which runs fail2ban and inserts firewall rules, but
here we are mostly concerned about log generation for user logins.
jeepers-peepers
Honorable mention is
jeepers-peepers
very interesting plugin
with very strange php coding style, which does generate logs on disk,
but it uses current user to generate files, so logs from web server
will be by nobody user, so wp-cli commands from other users won't
be able to update logs.
Even worse, if you run wp-cli under any other user than nobody first,
you will create log files which are not updatable from web server.
I really wanted central syslog logging, so this was not suitable
solution. It also didn't support multi-site WordPress installations
which I also had.
mu-plugins
This seems good so far, but installing a plugin to hundreds of sites is somewhat involved
and I want to minimize modifications which I have to do on each site.
WordPress
Must Use plugins
which are automatically loaded and activated is perfect for such task.
Even better, we can have one mu-plugins
directory which is then symlinked to all sites making installation nice and simple.
Mitigation on infected sites
When a site gets infected, WP-CLI can help us find modified files using
wp core verify-checksums wp plugin verify-checksums --allPlugin verification works for most plugins, but some paid ones (like WPML) don't have checksums upstream which is a shame.
Disabling compromised user
When a compromised user is identified, it's good to remove administrator privileges from it (which might be somewhat involved if this is a WordPress Multisite, so wp super-admin list might be useful).
wp eval-file
disable-user.php
login
will display current capabilities, reset password, remove all capabilities from the user,
list and destroy user sessions, regenerate WordPress salts,
iterate over all sites if wordpress is multisite and remove administrator privileges.
Salts regeneration with
wp config shuffle-salts is useful because all users are forced
to login again, thus invalidating saved logins, but for that script has to be run under correct user,
owner of
wp-config.php, so there is wrapper script
wp-disable-user.sh
which ensures that.
Auditing Logins with Wordfence
If you have Wordfence installed, it tracks user logins in user metadata, which can be invaluable for forensics even if you don't have wp-fail2ban logs.You can use wp eval to extract this information across your sites (based on wp-wordfence-login.sh): This snippet lists users who have logged in, showing their username, last login timestamp, and IP address, sorted by last_login descending.
Scanning WordPress using Wordfence CLI
We have daily backups of all WordPress sites, so an alternative is to check at the backup server which files are changed. However, we can also use wordfence-cli to check if there are exploits using
wordfence malware-scan --match-engine=vectorscan -q -a --output-format csv --output-path malware.csv /path/to/wordpressThis works well on the backup server (vectorscan engine which is much faster requires SSE capable CPU) but, vuln-scan which checks known vulnerabilities in installed plugins works only on WordPress installation and not on plain backup files.
You should really examine all warnings from malware-scan, but gzip fonts will be reported as possible compromise: zamd/cluster/pauk.ffzg.hr/2/www/ffzg.hr/fonet2/eufonija/public_html/wp-content/plugins/easy-digital-downloads/includes/libraries/fpdf/font/c67085188799208adeb5b784b9483ad0_droidserif-italic.z,7741,IOC:ZIP/CompressedZlib.7741,Raw compressed zlib file - occasionally used to store fonts or exports but may be an IOC (Indicator of Compromise), which can be safely ignored.
Finding content created in some time range
If you want to examine content on site to see if there where any spam content added, you
can use:
wp eval-file find-modified-content.php 2025-12-05 1025-12-08
syslog geolocation of logins
Best way which I found to detect infected sites is to geolocate all logins to wordpress,
and send mails for logins which are outside Croatia. Common pattern is to see several
logins from different IPs and countries, which is then trigger for closer investigation.
For that there is simple script
tail-wordpress-accepted.sh
which in turn uses
geolocate_ips.sh
to geolocate IPs using geoiplookup and web API which usually has more
precise country, town and ISP data.