Why
I've recently came across a little presentation on how easy is to hack WordPress1. I'd known it's pretty easy, but unfortunately there are tools making this possible for nearly anyone.
To test how problematic my sites are, I've downloaded WPScan2 and ran it against my sites. The result was surprising: The website is unreachable.
After the initial panic that (I've killed my own site!) I realized that my long forgotten nginx rules in combination with fail2ban banned the testing IP within the first 2 seconds of the scanning attempt.
You may want to introduce something similar to block scanners on your WordPress. ( Or ask you hosting provider to do so. )
I've not written all of these rules myself but unfortunately I cannot recall all the sources I've used during the years to collect this, therefore my thanks go to everyone who ever did anything I've read on this.
Please bear in mind that security is never bullet-proof - this setup will not save you from someone really attacking your site. There are many layers you need to use and always keep an eye out, revisit the logs, supervise what is happening with your sites.
nginx3
In nginx you need to add a new log format. This will contain all the blocked requests, including the IP address; this should go into the http {} block.
/etc/nginx/nginx.conf
, http
section:
log_format blocked '$time_local: Blocked request from $remote_addr $request';
You'll also need the following rules in the server {} block for the site you're setting the protection for:
# note: if you have posts with title matching these, turn them off or fine-tune
# them to exclude those
## Block SQL injections
location ~* union.*select.*\( {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* union.*all.*select.* {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* concat.*\( {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
## Block common exploits
location ~* (<|%3C).*script.*(>|%3E) {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* base64_(en|de)code\(.*\) {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* (%24&x) {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* (%0|%A|%B|%C|%D|%E|%F|127\.0) {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* \.\.\/ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* ~$ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* proc/self/environ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* /\.(htaccess|htpasswd|svn) { log_not_found off;
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
## Block file injections
location ~* [a-zA-Z0-9_]=(\.\.//?)+ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* [a-zA-Z0-9_]=/([a-z0-9_.]//?)+ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
## Block access to internal WordPress assets that isn't queried under normal
## circumstances
location ~* wp-config.php {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* wp-admin/includes {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* wp-app\.log {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* (licence|readme|license)\.(html|txt) {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
## In case you have anything using sqlite as database you probably want to block
## direct access to those as well
location ~* \.sqlite$ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* ^/(SQLite|sqlite) {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
location ~* \.sqlite-journal$ {
access_log /var/log/nginx/blocked.log blocked;
deny all;
}
fail2ban4
Add this jail to the jails config:
/etc/fail2ban/jail.conf
[nginx-blocked]
enabled = true
port = 80,443
filter = nginx-blocked
logpath = /var/log/nginx/blocked.log
bantime = 3600
maxretry = 3
backend = auto
findtime = 86400
banaction = iptables-multiport
protocol = tcp
chain = INPUT
Note: the logpath accepts * as wildcard, in case you have more blocked logs.
And also add the filter
/etc/fail2ban/filter.d/nginx-blocked.conf
[Definition]
failregex = ^.* Blocked request from <HOST>.*$
ignoreregex =
For the nat banaction
, please see fail2ban for NAT
hosts5.
Optional: syslog
In case you have more than one webserver, you probably want to
centralize this. The easiest way is to have fail2ban on the
loadbalancer, nginx logging into syslog, syslog pushed to the
loadbalancer's syslog. Rsyslog is a deep topic, so please keep
in mind that you'll most probably need to read more about it. This is an
example for those who are familiar with syslog. You can replace
the access_log
lines in the collection above with something
similar to:
access_log syslog:server=unix:/dev/log,facility=local7,tag=nginx,severity=warn blocked;
in the nginx.conf and:
local7.warn /var/log/nginx/blocked.log
in you rsyslog.conf.
Troubleshooting
aaronpk6 started to use this setup, but had some troubles verifying it's running and happy.
Note: these all should be run as root.
Check if fail2ban is running
ps aux | grep fail2ban
should show something like
/usr/bin/python /usr/bin/fail2ban-server
(or similar).
bantime
and
findtime
In my example, bantime is set for an hour and findtime for a day. You may need to tune this according to your needs and traffic.
verify your regex
If you run
fail2ban-regex -v [path-to-log] [path-to-filter-rule]
it
should output something like this:
Running tests
=============
Use failregex file : /etc/fail2ban/filter.d/nginx-blocked.conf
Use log file : /var/log/nginx.blocked.log
Results
=======
Failregex: 1452 total
|- #) [# of hits] regular expression
| 1) [1452] ^.*?Blocked request from <HOST> .*$
| 51.255.65.41 Sun Feb 19 06:31:05 2017
[ ... lots of IPs here ...]
| 157.55.39.20 Thu Feb 23 12:30:23 2017
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [2125] ISO 8601
| [2] Year/Month/Day Hour:Minute:Second
| [0] WEEKDAY MONTH Day Hour:Minute:Second[.subsecond] Year
| [0] WEEKDAY MONTH Day Hour:Minute:Second Year
| [0] WEEKDAY MONTH Day Hour:Minute:Second
| [0] MONTH Day Hour:Minute:Second
| [0] Day/Month/Year Hour:Minute:Second
| [0] Day/Month/Year2 Hour:Minute:Second
| [0] Day/MONTH/Year:Hour:Minute:Second
| [0] Month/Day/Year:Hour:Minute:Second
| [0] Year-Month-Day Hour:Minute:Second[,subsecond]
| [0] Year-Month-Day Hour:Minute:Second
| [0] Year.Month.Day Hour:Minute:Second
| [0] Day-MONTH-Year Hour:Minute:Second[.Millisecond]
| [0] Day-Month-Year Hour:Minute:Second
| [0] Month-Day-Year Hour:Minute:Second[.Millisecond]
| [0] TAI64N
| [0] Epoch
| [0] Hour:Minute:Second
| [0] <Month/Day/Year@Hour:Minute:Second>
| [0] YearMonthDay Hour:Minute:Second
| [0] Month-Day-Year Hour:Minute:Second
`-
Lines: 2127 lines, 0 ignored, 1452 matched, 675 missed
If not, the regex is not working.
Query fail2ban
Fail2ban offers a program names fail2ban-client
to ask
for status, so:
fail2ban-client status nginx-blocked
Status for the jail: nginx-blocked
|- filter
| |- File list: /var/rlog/mekare.petermolnar.eu/nginx.blocked.log
| |- Currently failed: 68
| `- Total failed: 586
`- action
|- Currently banned: 6
| `- IP list: 122.167.147.40 176.93.112.88 193.154.116.218 203.118.160.75 79.122.16.39 157.55.39.20
`- Total banned: 139
If this is returning some error, it's either the client not being
able to talk to the server (check the socket, maybe add
-s [socketpath]
to the client), or the service/jail is not
running'.
Dump iptables
When you're certain an IP should have been or is blocked, you can
also check iptables with iptables-save
.
(Oh, by the way: this entry was written by Peter Molnar, and originally posted on petermolnar dot net.)