Qualche giorno fa guardando i log di apache ho notato parecchie linee tipo queste:
mysite.com:443 BAD_GUY_IP - - [23/Sep/2019:06:22:40 +0200] "POST /wp-login.php HTTP/1.1" 200 5839 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
mysite.com:443 BAD_GUY_IP - - [23/Sep/2019:06:22:40 +0200] "POST /xmlrpc.php HTTP/1.1" 200 4036 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
E con parecchie intendo:
grep wp-login.php other_vhosts_access.log* |wc -l
34290
grep xmlrpc.php other_vhosts_access.log* |wc -l
8802
Tamponare velocemente con apache
La soluzione nel mio caso (pochi siti, unico admin) è stata quella di permettere solo ai miei IP fissi di accedere alle pagine di login di wordpress. In pratica, in ogni virtual host ho aggiunto:
<Files wp-login.php>
order deny,allow
Deny from all
# allow access from my IP address
allow from IP1
allow from IP2
</Files>
<Files xmlrpc.php>
order deny,allow
Deny from all
# allow access from my IP address
allow from IP1
allow from IP2
</Files>
Poi ho ricaricato la conf di apache:
service apache2 reload
Solo IP1 e IP2 possono accedere alla pagina di login di wordpress. Certo io posso farlo perché ho pochi siti e sono l’unico admin. Per chi ha centinaia o migliaia di siti con diversi IP (magari pure dinamici) questa strada non è percorribile.
Risultato:
mysite.com:443 BAD_GUY_IP - - [05/Oct/2019:11:18:06 +0200] "POST /wp-login.php HTTP/1.1" 403 3426 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0" mysite.com:443 BAD_GUY_IP - - [05/Oct/2019:11:18:07 +0200] "POST /xmlrpc.php HTTP/1.1" 403 3424 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
Adesso apache risponde correttamente con 403 (Forbidden) invece che 200 (OK) ed impedisce all’attaccante di provare a fare il login. Problema risolto? Assolutamente no ma almeno ho aggiunto una difficoltà in più per chi ci prova. Anche se devo dire che è veramente scoraggiante osservare impotenti mentre provano a violare il tuo sito.
A questo punto mi è venuta l’idea di bloccare chi prova a fare il login se non è nella whitelist inserita prima in apache. Vediamo come farlo con fail2ban.
Apache
In debian c’è un’opzione carina da abilitare che permette di avere tutti i log in un unico file in aggiunta a quelli specifici per ogni virtualhost:
root@ares [19:16][0] ~ # cat /etc/apache2/conf-available/other-vhosts-access-log.conf
# Define an access log for VirtualHosts that don't define their own logfile
CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
La prima riga ci dice che questa opzione definisce un file di log per tutti i VirtualHosts che non definiscono il loro log file. Quindi affinché funzioni vi basterà definire i log file dei vostri virtual host al di fuori delle direttive “VirtualHost”:
CustomLog [...]
<VirtualHost *:80>
[...]
</VirtualHost>
<VirtualHost *:443>
[...]
</VirtualHost>
Abilitiamola e ricarichiamo la configurazione di apache:
a2enconf other-vhosts-access-log
service apache2 reload
Ora in /var/log/apache2/other_vhosts_access.log dovreste trovare le richieste di tutti i vostri virtual host.
Fail2ban
Intanto cerchiamo di capire cosa vogliamo fare. L’intenzione è di bloccare la richiesta (o meglio, l’attacco) il prima possibile.
richiesta --> firewall --> apache --> log --> fail2ban --> ban
Appena fail2ban si accorge che qualcuno accede a wp-login.php e non è tra i miei IP allora viene bannato. L’ideale sarebbe farlo appena passa dal firewall per evitare che rompa le scatole ad apache ma non è una cosa così semplice. Credo che installando un IDS/IPS (Intrusion Detection/Prevention System) come snort o suricata si possa fare, bisogna “solo” scrivere una regola ad hoc per l’occasione. Non proprio una passeggiata.
Invece noi scriveremo una regola per fail2ban (molto più abbordabile), anche se in questo caso la richiesta viene comunque “evasa” da apache, nel senso che comunque apache risponde. Quindi ha poco senso in caso di DDoS. Ma facciamolo lo stesso, può essere istruttivo.
Scrivere la regola
Per aiutarci a trovare la regola possiamo usare fail2ban-regex, che può essere utilizzato in vari modi:
fail2ban-regex 'linea di log' 'regex'
fail2ban-regex '/percorso/file/di/log' 'percorso/regola/fail2ban'
oppure una combinazione delle due. Secondo me è più comodo definire un file log di esempio con le linee che ci interessano e scrivere la regex sulla riga di comando. Il file di log di esempio è:
mysite.com:443 1.1.1.1 - - [05/Oct/2019:10:24:16 +0200] "GET /wp-login.php HTTP/1.1" 200 5986 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
mysite.com:443 2.2.2.2 - - [05/Oct/2019:10:24:17 +0200] "GET /wp-includes/css/dashicons.css?ver=5.2.3 HTTP/1.1" 200 33790 "https://www.mysite.com/wp-login.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
mysite.com:443 3.3.3.3 - - [05/Oct/2019:11:18:25 +0200] "GET /wp-login.php HTTP/1.1" 403 3562 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
mysite.com:443 4.4.4.4 - - [05/Oct/2019:11:18:27 +0200] "POST /wp-login.php HTTP/1.1" 403 3562 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
mysite.com:443 5.5.5.5 - - [05/Oct/2019:11:18:07 +0200] "POST /xmlrpc.php HTTP/1.1" 403 3424 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
mysite.com:443 6.6.6.6 - - [05/Oct/2019:11:14:47 +0200] "HEAD /en/ HTTP/1.1" 200 4094 "https://mysite.com/en/" "Mozilla/5.0+(compatible; UptimeRobot/2.0; http://www.uptimerobot.com/)"
mysite.com:443 7.7.7.7 - - [15/Oct/2019:18:50:20 +0200] "GET /wp-login.php?redirect_to=https%3A%2F%2Fmattia.mysite.it%2Fwp-admin%2F&reauth=1 HTTP/1.1" 403 541 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0"
La regex potrebbe essere:
^\S* <HOST> .* "(GET|POST) /(wp-login.php|xmlrpc.php).*" 403 .*$
[Scrivere regex non è proprio il mio forte, questa può essere sicuramente migliorata]
Per testare possiamo usare fail2ban-regex:
root@ares [19:41][0] ~ # fail2ban-regex --print-all-matched /tmp/apache.log '^\S* <HOST> .* "(GET|POST) /(wp-login.php|xmlrpc.php).*" 403 .*$'
Running tests
=============
Use failregex line : ^\S* <HOST> .* "(GET|POST) /(wp-login.php|xmlrpc.p...
Use log file : /tmp/apache.log
Use encoding : UTF-8
Results
=======
Failregex: 4 total
|- #) [# of hits] regular expression
| 1) [4] ^\S* <HOST> .* "(GET|POST) /(wp-login.php|xmlrpc.php).*" 403 .*$
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [7] Day(?P<_sep>[-/])MON(?P=_sep)ExYear[ :]?24hour:Minute:Second(?:\.Microseconds)?(?: Zone offset)?
`-
Lines: 7 lines, 0 ignored, 4 matched, 3 missed
[processed in 0.03 sec]
|- Matched line(s):
| mysite.com:443 3.3.3.3 - - [05/Oct/2019:11:18:25 +0200] "GET /wp-login.php HTTP/1.1" 403 3562 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
| mysite.com:443 4.4.4.4 - - [05/Oct/2019:11:18:27 +0200] "POST /wp-login.php HTTP/1.1" 403 3562 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
| mysite.com:443 5.5.5.5 - - [05/Oct/2019:11:18:07 +0200] "POST /xmlrpc.php HTTP/1.1" 403 3424 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
| mysite.com:443 7.7.7.7 - - [15/Oct/2019:18:50:20 +0200] "GET /wp-login.php?redirect_to=https%3A%2F%2Fmattia.mysite.it%2Fwp-admin%2F&reauth=1 HTTP/1.1" 403 541 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0"
`-
|- Missed line(s):
| mysite.com:443 1.1.1.1 - - [05/Oct/2019:10:24:16 +0200] "GET /wp-login.php HTTP/1.1" 200 5986 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
| mysite.com:443 2.2.2.2 - - [05/Oct/2019:10:24:17 +0200] "GET /wp-includes/css/dashicons.css?ver=5.2.3 HTTP/1.1" 200 33790 "https://www.mysite.com/wp-login.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
| mysite.com:443 6.6.6.6 - - [05/Oct/2019:11:14:47 +0200] "HEAD /en/ HTTP/1.1" 200 4094 "https://mysite.com/en/" "Mozilla/5.0+(compatible; UptimeRobot/2.0; http://www.uptimerobot.com/)"
`-
Ok sembra funzionare. Prende le linee che ci interessano mentre scarta correttamente le altre, in particolare:
- la prima (1.1.1.1) deve ignorarla perché riguarda un accesso fatto da un IP permesso (200)
- le seconda è conseguenza della prima, quindi OK
- la sesta non c’entra niente e quindi deve ignorarla
Mi raccomando usate percorsi assoluti con fail2ban-regex! Scrivere regex è un’arte oscura difficile da padroneggiare, è come cercare di recitare delle parole magiche: difficile da eseguire, basta una virgola perché non funzioni, ma quando lo fa succede la magia 🙂 Certo ci vuole moooolta pazienza. Alcuni documenti in caso non funzioni:
Mettere tutto a sistema
Bene, siamo a metà dell’opera! Mettete la regola di fail2ban che comprende la regex in /etc/fail2ban/filter.d/wplogin.conf
# Fail2Ban filter for wordpress login attemps
[Definition]
failregex = ^\S* <HOST> .* "(GET|POST) /(wp-login.php|xmlrpc.php).*" 403 .*$
e in /etc/fail2ban/jail.d/wplogin.conf:
[wp-login]
# Ban for 1 hour if it fails once within 30 minutes
enabled = true
port = all
filter = wplogin
logpath = /var/log/apache2/other_vhosts_access.log
maxretry = 1
bantime = 1h
findtime = 30m
banaction = iptables-allports
Banna l’IP per 1 ora se qualcuno tenta di fare il login 1 volta in 30 minuti. Si, è abbastanza aggressiva. Aggiustatela con le vostre preferenze.
Fate ripartire fail2ban:
service fail2ban restart
Controllate che sia tutto a posto:
fail2ban-client status
Nella lista delle jail deve esserci anche la nostra ‘wp-login’.
Per testare potete accedere alla vostra pagina di login con il browser tor oppure usando un proxy online. Nel file di log di fail2ban troverete qualcosa di simile:
fail2ban.actions [25025]: NOTICE [wp-login] Ban 1.1.1.1
That’s all folks!