Dejo un script en perl que sirve para banear mediante iptables a quienes incordien. Su funcionamiento es sencillo, revisa los logs del sistema que le digas y banea ips en función de coincidencias de cadenas en las líneas de dichos logs, destinado a servidores caseros.
En principio está destinado a quien tenga un poco experiencia, hay que saber un poco de perl y expresiones regulares, más que nada para saber qué se quiere machear y banear. Cada sistema es un mundo y es difícil adaptar algo para cualquiera. Tampoco está destinado para servidores de gran tráfico sino para los caseros, el típico que tiene el ssh y un par de servicios para uso propio o de poco tráfico. Para servidores serios hay otras herramientas tipo fail2ban (creo que se llama así) mucho más apropiadas.
El script (uno parecido) lo tengo corriendo desde hace más de 1 año en mi servidor (lo lanzo con cron cada minuto) y me ha quitado de encima a docenas de bots, principalmente para ssh y postfix, nada simpáticos.
Si alguien quiere más aclaraciones estoy dispuesto a darlas,
Un saludo!
Nota. Se requiere la utilidad «geoiplookup» si se quiere usar para saber de dónde viene la ip, si no se instala (es un pequeño paquete) hay que modificar el script
Código: Seleccionar todo
#!/usr/bin/perl
# -------------------------------------------------------
# Script para vigilar logs del sistema relacionados con
# entradas desde internet hacia ssh, postfix, webserver..
#
# Funciona de forma que sólo lee las líneas nuevas que haya
# desde la última lectura, las pasa por expresiones regulares
# y en caso de encontrar alguna cadena sospechosa predefinida
# recupera si puede la ip de origen y la guarda en un hash
# junto con el número de intentos sospechosos ya encontrados
# para la misma ip.
# Si el número de intentos sobrepasa el límite de aguante
# banea con iptables al que incordia (si no lo está ya).
use strict;
use warnings;
# -------------------------------------------------------
# ---- CONFIGURACION ------------------------------------
# -------------------------------------------------------
# archivos para registrar los baneados y el log general
my $file_baneados = 'banwtf.log';
my $file_loggral = 'vigwtf.log';
# array de logs a vigilar, añadir los que se deseen
my @logs = ();
$logs[0] = '/var/log/auth.log';
$logs[1] = '/var/log/maillog';
# número máximo de puntos de aguante
# 5 sería poco aguante, 20 aguante medio, 50 aguante top
# por defecto se irrita enseguida
my $aguante = 5;
# ips que no hay que banear, ejemplos
my @allows = qw('192.168.1.10' '192.168.1.18');
# para modo test poner a 1, para activar wtf poner a 0
my $test = 1;
# -------------------------------------------------------
# ---- REGLAS -------------------------------------------
# -------------------------------------------------------
# hash de frases WTF y expresiones regulares.
# el principio es simple, busca en la línea de log una
# cadena sospechosa, wtf, le pasa el regexp para ver si
# puede sacar la ip de origen y si es así la penaliza con
# el valor que se le dé
my %hash_wtf = (
'0001' => { # ssh
'wtf' => 'Invalid user', # cadena WTF
'reg' => '(\d+\.\d+\.\d+\.\d+)$', # regexp
'pen' => 5, # penalización
'act' => 1, # activo o no
},
'0002' => { # ssh
'wtf' => 'User root from \d+\.\d+\.\d+\.\d+ not',
'reg' => 'User root from (\d+\.\d+\.\d+\.\d+) not',
'pen' => 10,
'act' => 1,
},
'0003' => { # ssh
'wtf' => 'Did not receive identification string from \d+\.\d+\.\d+\.\d+',
'reg' => 'Did not receive identification string from (\d+\.\d+\.\d+\.\d+)',
'pen' => 10,
'act' => 1,
},
'0004' => { # postfix
'wtf' => 'tried to use disabled plaintext auth',
'reg' => 'rip\=(\d+\.\d+\.\d+\.\d+),',
'pen' => 10,
'act' => 1,
},
'0005' => { # postfix
'wtf' => 'Relay access denied',
'reg' => '\[(\d+\.\d+\.\d+\.\d+)\]:',
'pen' => 2,
'act' => 1,
},
'0006' => { # postfix
'wtf' => 'non-SMTP command from',
'reg' => '\[(\d+\.\d+\.\d+\.\d+)\]:',
'pen' => 10,
'act' => 1,
},
'0007' => { # postfix
'wtf' => 'Connection rate limit exceeded',
'reg' => '\[(\d+\.\d+\.\d+\.\d+)\]:',
'pen' => 10,
'act' => 1,
},
'0008' => { # postfix
'wtf' => 'Connection concurrency limit exceeded',
'reg' => '\[(\d+\.\d+\.\d+\.\d+)\]:',
'pen' => 10,
'act' => 1,
},
'0009' => { # postfix
'wtf' => 'therichsheickc',
'reg' => '\[(\d+\.\d+\.\d+\.\d+)\]:',
'pen' => 10,
'act' => 1,
},
);
# -------------------------------------------------------
# ---- CUERPO DEL SCRIPT --------------------------------
# -------------------------------------------------------
# coge la opción de nivel de log --log
my $opt = $ARGV[0];
# si no hay level_log se establece a 2
$opt = "--log=2" unless $opt;
my $level_log = ($opt =~m/=(\d)$/,$1);
# por si acaso,
$level_log = 1 unless $level_log;
# nivel 2 para el test
$level_log = 2 if $test;
# hora de arranque del script, se loguea si level_log > 1
loguear ("#### Inicio script ####") if $level_log > 1;
# recupero el número de líneas a leer de cada log a vigilar
my @num_lin = (); # líneas de cada log
my $cont = 0; # contador
my $lin_tot = 0; # líneas totales
foreach (@logs) {
# comprueba que existe el archivo log antes de contar líneas
print "No existe el archivo log $_\n" and next if (!-f $_);
$num_lin[$cont] = devuelve_num_lineas_a_leer($logs[$cont]);
$lin_tot += $num_lin[$cont];
$cont++;
}
# sale si no hay nada nuevo que revisar
if ($lin_tot == 0) {
loguear ("#### Fin script ####") if $level_log > 1;
exit;
}
# sigo si hay algo, ....
loguear ("## leyendo $lin_tot líneas ##") if $level_log > 0;
# construyo la cadena de comando
my $cadena_cmd = "";
my @res = ();
$cont = 0;
my $and = "";
foreach (@logs) {
$and = ' && ' if $cont > 0;
$cadena_cmd .= $and .'tail -n ' . $num_lin[$cont] . ' ' . $logs[$cont];
$cont++;
}
# lanza la cadena comando y recupero la salida en un array
@res = `$cadena_cmd`;
# recupera el hash de baneados guardado en la anterior lectura
# de esta forma no pierde los intentos anteriores
my %hash_cazados = devuelve_hash();
my $cont_lineas = 0;
# revisa las líneas devueltas
foreach (@res) {
my $cazado;
my $linea = $_;
foreach my $regla (sort keys %hash_wtf){
if ($linea =~ m/$hash_wtf{$regla}{wtf}/ and $hash_wtf{$regla}{act}==1){
$cazado = ($linea =~m/$hash_wtf{$regla}{reg}/,$1);
$hash_cazados{$cazado}{intentos} += $hash_wtf{$regla}{pen} if $cazado;
loguear ("wtf: $hash_wtf{$regla}{wtf}") if $level_log > 1;
last;
}
}
# si hay algún intruso...
if ($cazado) {
$hash_cazados{$cazado}{ip} = $cazado;
loguear ("Cazado: $cazado Intentos: $hash_cazados{$cazado}{intentos} Linea: $_") if $level_log > 0;
}
# cuenta las líneas que va leyendo
$cont_lineas++;
}
# loguea el número de líneas leídas totales si level_log > 1
loguear ("Líneas leídas: $cont_lineas") if $level_log > 1;
# ahora pasamos a banearlos si es necesario ...
foreach my $ip (sort keys %hash_cazados){
# nos saltamos algunas ips que no queramos banear nunca...
next if ("@allows" =~m /$ip/);
# repasa el hash, si alguna ip tiene más de $aguante intentos sigue
if ($hash_cazados{$ip}{intentos} > $aguante ) {
# si ya está anotada como baneada salta al siguiente
next if $hash_cazados{$ip}{baneado};
# se asegura así y todo que no esté en las reglas de iptables
my $esta_o_no = "-";
$esta_o_no = `iptables -nL | grep $ip` if (!$test);
# si no está ...
if ($esta_o_no !~m/$ip/) {
# no está en las reglas, se mira origen y
# se escribe en el log de baneados
my $origen = `geoiplookup $ip | head -n 1 | sed -e 's/^.*://'`;
chomp ($origen);
open "FILE",">>","$file_baneados";
my $hora = `date +"%d-%m-%Y %H:%M:%S"`;chomp($hora);
print FILE "#$hora Baneado: $origen\n$ip\n";
close (FILE);
if (!$test) {
my $comando_baneo_input = "iptables -I INPUT -p all -s $ip -j DROP";
my $salida_baneo_input = `$comando_baneo_input`;
my $comando_baneo_output = "iptables -I OUPUT -p all -d $ip -j DROP";
my $salida_baneo_output = `$comando_baneo_output`;
}
# se escribe en el log de baneador
loguear ("Baneado: $ip , intento: $hash_cazados{$ip}{intentos}") if $level_log > 0;
# coloco el flag baneado a 1 para que no intente banearlo de nuevo
$hash_cazados{$ip}{baneado} = 1;
}
}
else {
# si ya está baneada ni caso
next if $hash_cazados{$ip}{baneado};
# avisando
if ($hash_cazados{$ip}{intentos} > 1){
loguear ("Hacia el baneo: $ip , intentos: $hash_cazados{$ip}{intentos}") if $level_log > 1;
};
}
}
# guarda el hash de cazados para leerlo la próxima vez
guarda_hash(%hash_cazados);
sub devuelve_num_lineas_a_leer {
my $archivo_log = shift;
# recupera el número de líneas actual
my $lineas_actuales = `wc -l $archivo_log | sed -e 's/ .*\$//'`;
chomp($lineas_actuales);
# coge el nombre del archivo para apuntar después última línea
my $nom_file = ($archivo_log =~ m/.*\/(.*)$/,$1);
# coge la última línea leída
my $ultima_linea_leida = 1;
$ultima_linea_leida = `cat .ult_$nom_file` if (-f ".ult_$nom_file");
chomp ($ultima_linea_leida) if $ultima_linea_leida;
# hace el cálculo de líneas a leer, primero mira si no se ha
# rotado y es más corto que en la anterior lectura
$ultima_linea_leida = 1 if ($lineas_actuales < $ultima_linea_leida);
my $num_lin = $lineas_actuales - $ultima_linea_leida;
# escribe el número total de líneas actuales para la próxima lectura
open "FILE",">",".ult_$nom_file";print FILE $lineas_actuales;close (FILE);
loguear ("Lineas en $nom_file $lineas_actuales, última leída anterior: $ultima_linea_leida, líneas a leer: $num_lin") if $level_log > 1;
return $num_lin;
}
sub devuelve_hash {
my %hash_guardado ;
# si no hay file no hay hash todavía
return %hash_guardado if !(-f (".hash.dat"));
my @contenido = `cat .hash.dat`;
foreach (@contenido) {
next if ($_ =~ m/^#/);
next if ($_ eq "");
my ($ip,$intentos) = split(" ",$_);
$hash_guardado{$ip}{ip} = $ip;
$hash_guardado{$ip}{intentos} = $intentos;
$hash_guardado{$ip}{intentos} = 0 unless $intentos;
$hash_guardado{$ip}{baneado} = 0;
}
return %hash_guardado;
}
sub guarda_hash {
my %hash_ips = @_;
open "FILE",">",".hash.dat";
foreach my $ip (sort keys %hash_ips){
# no guarda los ya baneados en el hash
next if ($hash_ips{$ip}{baneado});
print FILE "$hash_ips{$ip}{ip} $hash_ips{$ip}{intentos}\n";
}
close (FILE);
}
sub loguear {
my $rec_log = shift;
chomp ($rec_log) if $rec_log =~/\n/;
my $hora = `date +"%d-%m-%Y %H:%M:%S:%N"`;chomp($hora);
open "FILE",">>","$file_loggral";
print FILE "## $hora # $rec_log\n";
close (FILE);
}
loguear ("## saliendo ##") if $level_log > 1;
loguear ("#### Fin script ####") if $level_log > 1;