~ To be, or not to be, or to let it be. ~ null-i.net |
Linux/静的IPフィルタの作成その2 | |
自力でログを見て、アクセス元にフィルタをかける例(2019-04-22) アクセスログを、perlと正規表現で読み込んでみようという例です 特定のIPアドレスからのアクセスを止める例†うちのCentOS 7 環境で試した例ですが、 ※事前に /home/hoge/ban_list.txt ファイルに、対象IPを列挙して下さい 初回は ipsetを新しく作ります # firewall-cmd --permanent --new-ipset=my_ban --type=hash:ip # firewall-cmd --reload my_banに対して、 drop するIPアドレス一覧を登録します # firewall-cmd --add-rich-rule='rule source ipset=my_ban drop' --permanent # firewall-cmd --ipset=my_ban --add-entries-from-file=/home/hoge/ban_list.txt --permanent # firewall-cmd --ipset=my_ban --get-entries # firewall-cmd --reload これを削除するときは、上記add-entries を remove-entries に変えて実行します。 もし、 fail2ban-postfix が有効で、そこにIPを追加する場合 # fail2ban-client set fail2ban-postfix banip 対象のアドレス なので、前述のリストをshellで全部渡すなら、こう? for ip in `cat /home/hoge/ban_list.txt`; do fail2ban-client set fail2ban-postfix banip $ip done この場合は、そのルール(上記だとfail2ban-postfix)で設定した bantimeを経過すると解除されるのと、
あとは、私の環境の場合は squidを使っているので、そこでIPアドレス一覧を読み込んで、 ログからアクセス元IPを絞り込む†はじめに†結局、市販のログ収集・監視ツールでもフリーのやつでも、 ログからIPアドレスを拾っていく例†では、 perl のスクリプトの例です。read_log.pl として、 $ ./read_log.pl /var/log/maillog 37.49.xxx.xxx 1420 4 190421 185.100.xx.xxx 640 4 190421 107.170.xxx.xx 310 4 190421 出力は、IPアドレス(37.49.x.x)、得点(1420点)、フラグ(4)、日付(4/21)、です。 #!/usr/bin/perl -w # # ログ監視/集計ツールの簡易版みたいなことをやります # 想定している厄介ログから、IPと頻度を集計します # # そのまま > なり tee なりでファイルに落として別ツールで使う想定 # ファイルに落とさない情報は、STDERRに出力します # sub dbg_printf{ # デバッグ文、作成過程では入れまくってます。 #return; # ここをコメントアウト #printf STDERR "dbg:"; printf STDERR @_ ; $|=1; } # 月表示を数字二桁にもどすやつ my %m2d = ('Jan'=>'01', 'Feb'=>'02', 'Mar'=>'03', 'Apr'=>'04', 'May'=>'05', 'Jun'=>'06', 'Jul'=>'07', 'Aug'=>'08', 'Sep'=>'09', 'Oct'=>'10', 'Nov'=>'11', 'Dec'=>'12'); sub M2D { my $month = shift; return $m2d{$month}; } # IPアドレス毎に以下の3つを集計してみます my %ip_last_access; # 最終検出日 yymmdd (ただしyyの部分は固定値。ログに無い場合があるので) my %ip_score; # 点数。厄介に応じて加点 my %ip_flag; # フラグ。ログやアクセスの種類の記録 sub setIP { # 上記のIPアドレスと3つの集計を、各ログから書き込むための関数 my ($ip, $time_mmdd, $score, $flag) = @_; $time = "19" . $time_mmdd; # TODO: ログに西暦が無い場合もあるので、ひとまず固定値。 if(! defined($ip_last_access{$ip}) or $time > $ip_last_access{$ip}){ $ip_last_access{$ip} = $time; } $ip_score{$ip} = (defined $ip_score{$ip} ? ($ip_score{$ip} + $score) : $score); if($ip_score{$ip} > 99999){ $ip_score{$ip} = 99999; # 一定数以上は集計しない } $ip_flag{$ip} = (defined $ip_flag{$ip} ? $ip_flag{$ip} | $flag : $flag); } #--- 自分で任意の基準を作りましょう(ここから)-------------------- my %FLAGS; # ログやアクセス種別やら情報 # 2の倍数でフラグを立てて、(LOGIN | MAIL) の両方で検出みたいに記録する $FLAGS{"LOGIN"} = 2; $FLAGS{"MAIL"} = $FLAGS{"LOGIN"} * 2; $FLAGS{"WEB"} = $FLAGS{"MAIL"} * 2; my %LV; # 加点。まぁ、てきとうに。 $LV{"WARN"} = 100; # 警告 $LV{"ERROR"}= 500; # エラー $LV{"CRIT"} = 10000; # 致命的 #--- 自分で任意の基準を作りましょう(ここまで)-------------------- #--- log_~の関数をひたすら増やしていく(ここから)---------------- sub log_mail01{ # 他にもいっぱいありますが、一例としてこんなログ... # # postfix/smtpd[xx]: NOQUEUE: reject: RCPT from unknown[AA.BB.CC.DD]: 554 5.7.1 <xx@xx.xx>: Relay access denied; from=xx # おそらく第三者中継狙いじゃないかな # if(/^(\S+)\s+(\d+) .+ postfix\/(smtpd|smtps\/smtpd)\[\d+\]: NOQUEUE: reject: RCPT from.+\[([\d\.]+)\]: .*Relay access denied;/){ my $mmdd = sprintf("%s%02d", M2D($1), $2); setIP($4, $mmdd, $LV{"ERROR"}, $FLAGS{"MAIL"}); return 1; # ヒットした場合は 1以上を返す } # Mar 17 04:55:50 xx postfix/smtps/smtpd[xx]: connect from unknown[AA.BB.CC.DD]: xx # unknown で何度も接続されることはありえない、という場合に # if(/^(\S+)\s+(\d+) .+ (connect|disconnect) from unknown\[([\d\.]+)/){ my $mmdd = sprintf("%s%02d", M2D($1), $2); setIP($4, $mmdd, 5, $FLAGS{"MAIL"}); return 1; # ヒットした場合は 1以上を返す } # # 上記と同様の手順で、「正常な」アクセスも if 文で拾って、 # その場合は何もカウントせずに return 1; する # } #--- log_~の関数をひたすら増やしていく(ここまで)---------------- #-------------------- # ここからメイン処理 #-------------------- if(! defined $ARGV[0]){ # /var/log/maillog* のように、ログファイルを渡して起動する printf(STDERR "no log files\n"); exit; } my @old_logs=(); my $is_next_oldlog=0;; #--- 指定したファイルを読み込む # 次に他のファイル&フィルタ、と繰り返していく foreach my $logfile (@ARGV){ if($logfile eq "-o"){ # "-o 古いログ" で前回の結果を渡された場合、今回分とマージする $is_next_oldlog++; next; } if($is_next_oldlog > 0){ push(@old_logs, $logfile); $is_next_oldlog=0; next; } open($list, "< $logfile") or die "$! [$logfile]\n"; while(<$list>){ $hit=0; if($logfile =~ /maillog/){ # ログファイル名に合わせてフィルタをかける $hit += log_mail01 $_; #$hit += log_mail02 $_; # どんどん追加 } #if($logfile =~ /access.log/){ # ログファイル名に合わせてフィルタをかける # $hit += log_web01 $_; #} if($hit<=0){ # 想定外のログがあった場合は、必要に応じてフィルタを追加する printf(STDERR "%s\n", $_); } } } my %old_last_access; # 過去の最終検出日 my %old_score; # 過去の点数 my %old_flag; # フラグ foreach my $old_logfile (@old_logs){ if(defined($old_logfile)){ # 過去の統計とマージ open($old_file, "< $old_logfile") or die "$! [$old_file]\n"; while(<$old_file>){ if(/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)/){ my ($o_ip, $o_score, $o_flag, $o_access) = ($1, $2, $3, $4); #printf("%16s, %6s, %3s, %s\n", $o_ip, $o_score, $o_flag, $o_access); # 過去分と今回分をマージする # 運用方法に合わせて更新するのだけど、今回の例としては # 最高点、最終アクセス日で更新、フラグはマージする if( defined($ip_score{$o_ip})){ if($ip_score{$o_ip} < $o_score){ $ip_score{$o_ip} = $o_score; } if($ip_last_access{$o_ip} < $o_access){ $ip_last_access{$o_ip} = $o_access; } $ip_flag{$o_ip} = (0+$ip_flag{$o_ip}) | (0+$o_flag); # 0+は強制的に文字から数字に戻す意図です }else{ # 古い方にしかない場合は、そのまま今回分に追加 $ip_score{$o_ip} = $o_score; $ip_last_access{$o_ip} = $o_access; $ip_flag{$o_ip} = $o_flag; } }else{ printf("ERR: %s\n", $_); } } } } # 結果の出力。以下ではタブ区切り(\t)だけど、 # あとで加工しやすい書式で出す。これを tee や > で、ファイルへ残す while ( ( $ip, $score ) = each ( %ip_score ) ) { printf("%s\t%d\t%d\t%s\n", $ip, $score, $ip_flag{$ip}, $ip_last_access{$ip}); } 長くなってしまいましたが、肝心なところは、
という作業です。 ./read_log.pl /var/log/maillog > list.txt 想定外の(if文で拾えていない)ログはSTDERRに出るので、 IPアドレスの偏りっぷりを確認してみる例†上記スクリプトで作ったログを、サブネットで集約を目指します。 国ごとのIPアドレス割り振り情報として、事前にRIR一覧をwgetなりで取得して、 ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest 以下がIPを集約してみようの例です。 #!/usr/bin/perl -w # read_log.pl で作った以下のログ情報 # printf("%s\t%s\t%s\t%s\n", $ip, $score, $ip_flag{$ip}, $ip_last_access{$ip}); # # これに国情報の付与と、サブネット集約を試みます # 出力はそのまま > などでファイルにリダイレクトする想定 # 集計情報以外は STDERR に分けて出力しています(これもファイル出力なら 2>&1 する) # my $SUBNET_INFO=8; # 情報として表示するネットマスク集約最小値 my $SUBNET_BAN=28; # これより大きいネットマスクで集約する sub dbg_printf{ # デバッグ文、作成過程では入れまくってます。 #return; # ここをコメントアウト #printf STDERR "dbg:"; printf STDERR @_ ; $|=1; } sub number2ip { # 数値変換したIPアドレスを文字列へ戻す my $b = shift; my $ip = sprintf("%d.%d.%d.%d", unpack("C", pack("B8", substr($b, 0, 8))), unpack("C", pack("B8", substr($b, 8,16))), unpack("C", pack("B8", substr($b,16,24))), unpack("C", pack("B8", substr($b,24,32))) ); return $ip; } #-------------------- # ここからメイン処理 #-------------------- if(! defined($ARGV[0])){ printf("no args\n"); exit; } # まず、サブネット 国名 一覧を読み込む use File::Basename; my $base_dir = dirname(__FILE__) . "/list/"; @rir_lists = ( $base_dir . "delegated-afrinic-extended-latest", $base_dir . "delegated-apnic-extended-latest", $base_dir . "delegated-arin-extended-latest", $base_dir . "delegated-lacnic-extended-latest", $base_dir . "delegated-ripencc-extended-latest" ); foreach my $r_list (@rir_lists){ open($list, "< $r_list") or die "$! [r_list]\n"; while(<$list>){ # 上記の、こんな感じのファイルを読みたい # apnic|JP|ipv4|1.0.16.0|4096|20110412|allocated|A92D9378 if(/^[^\|]+\|(\w+)\|ipv4\|([\d\.]+)\|(\d+)\|/){ $ip=$2; $num=$3; $cnt=$1; if($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){ $bin=$1*256*256*256 +$2*256*256 +$3*256 +$4; # IP表記から数値に戻す $ip_range{$bin} = $num; $ip_cntry{$bin} = $cnt; } } } } my %ip_n; my %ip_text; my %score; my %flag; my %access; my %country; # 国判定を行わない場合は、この変数に関する処理を削ります dbg_printf("make country table\n"); open($old_file, "< $ARGV[0]") or die "$! [$old_file]\n"; while(<$old_file>){ if(/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/){ my ($o_ip, $o_score, $o_flag, $o_access) = ($1, $2, $3, $4); my $o_bin; if($o_ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){ $o_bin=$1*256*256*256 +$2*256*256 +$3*256 +$4; # IP表記から数値に戻す } my $where = "??"; foreach my $start (keys(%ip_range)){ if($start <= $o_bin && $o_bin <= ($start + $ip_range{$start})){ $where = $ip_cntry{$start}; last; } } $bin32 = sprintf("%032b", $o_bin); # 二進数表記へ $ip_n{$bin32} = $o_bin; $ip_text{$bin32} = $o_ip; $score{$bin32} = $o_score; $flag{$bin32} = $o_flag; $access{$bin32} = $o_access; $country{$bin32} = $where; }else{ printf(STDERR "ERR: %s\n", $_); } } dbg_printf("sort and dump netmask\n"); my %subnet_mask; my %subnet_32bit; my %subnet_range; my %subnet_score; my %subnet_flag; my %subnet_access; my $last_ip = sprintf("%032b", -1); for my $ip (sort keys %score) { my ($pre, $suf, $mask) = ("", $ip, ""); my $i; for($i=$SUBNET_INFO; $i<=32; $i++){ #---- ここでサブネット集約する範囲を指定 #--- ソート済みの状態から、 #--- 一つ前のIPアドレスと二進数で文字列比較する if(substr($last_ip, 0, $i) eq substr($ip, 0, $i)){ #---- 一致するなら、集約 $pre = substr($ip, 0, $i); #--- 一致部分 $suf = substr($ip, $i, 32); #--- 不一致部分 $mask = "0" x (32 - $i); #--- 不一致部分をゼロでパディング } else { last; } } my $subnet; if(length($pre) > 0){ $subnet = number2ip($pre . $mask) . "/" . $i; }else{ $subnet = number2ip($suf); } if(length($pre) >= $SUBNET_BAN){ # 赤で表示(\e[31m)サブネットを添えて dbg_printf("\e[31m%s\e[0m%s %19s %s\t%s\t%s\t%s\n", $pre, $suf, $subnet, $country{$ip}, $score{$ip}, $flag{$ip}, $access{$ip}); }else{ # 緑で表示(\e[32m)IPアドレスを添えて dbg_printf("\e[32m%s\e[0m%s %19s %s\t%s\t%s\t%s\n", $pre, $suf, $ip_text{$ip}, $country{$ip}, $score{$ip}, $flag{$ip}, $access{$ip}); } if(length($pre) > $SUBNET_BAN){ if($subnet =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/.+/){ my $sub_n=$1*256*256*256 +$2*256*256 +$3*256 +$4; # サブネットを数値にして、ハッシュのキーとして使用 $subnet_mask{$sub_n} = $subnet; $subnet_32bit{$sub_n} = $pre . $suf; $subnet_range{$sub_n} = length($pre); $subnet_flag{$sub_n} = (defined $subnet_flag{$sub_n} ? $subnet_flag{$sub_n} | $flag{$ip} : $flag{$ip}); if(! defined $subnet_score{$sub_n} || $subnet_score{$sub_n} < $score{$ip}){ # 集約する場合は大きい方のスコアで更新。...あるいは合算でも、お好みで。 $subnet_score{$sub_n} = $score{$ip}; } if(! defined $subnet_access{$sub_n} || $subnet_access{$sub_n} < $access{$ip}){ # 最終アクセス日に更新 $subnet_access{$sub_n} = $access{$ip}; } } } $last_ip = $ip; } # まず、サブネット同士で集約する my %s_mask; my %s_32bit; my %s_range; my %s_score; my %s_flag; my %s_access; foreach my $sa (keys %subnet_mask){ my $hit=0; foreach my $sb (keys %subnet_mask){ if($sa == $sb){ next; # 自分自身と比較はしない } my $len=$subnet_range{$sb}; if($subnet_range{$sa} <= $len){ next; # 自分の方が短い場合、比較はしない } if(substr($subnet_32bit{$sa}, 0, $len) eq substr($subnet_32bit{$sb}, 0, $len)){ dbg_printf("# %s -> %s\n", $subnet_mask{$sa}, $subnet_mask{$sb}); # ここはあとで削除。確認用。 $hit++; # 他のサブネットで集約される last; } } if($hit==0){ $s_mask{$sa} = $subnet_mask{$sa}; $s_32bit{$sa} = $subnet_32bit{$sa}; $s_range{$sa} = $subnet_range{$sa}; $s_score{$sa} = $subnet_score{$sa}; $s_flag{$sa} = $subnet_flag{$sa}; $s_access{$sa} = $subnet_access{$sa}; } } my %s_include; # 集約範囲がいくつの(不正な?)IPアドレスを含むかカウントする for my $ip (sort keys %ip_n) { # ここはsortで無くても可。処理速度に応じて。 my $hit=0; foreach my $mask (keys %s_mask){ my $len=$s_range{$mask}; if(substr($ip, 0, $len) eq substr($s_32bit{$mask}, 0, $len)){ dbg_printf("# %s -> %s\n", $ip_text{$ip}, $s_mask{$mask}); # ここはあとで削除。確認用。 $s_include{$mask} = (defined($s_include{$mask}) ? $s_include{$mask} +1 : 1); $hit++; last; } } if($hit>0){ # 既に集約済みならば、出力せずにスキップ next; } printf("%s\t%s\t%s\t%s\n", $ip_text{$ip}, $score{$ip}, $flag{$ip}, $access{$ip}); } for my $n (sort keys %s_mask) { # ここはsortで無くても可。処理速度に応じて。 printf("%s\t%s\t%s\t%s\t(%d)\n", $s_mask{$n}, $s_score{$n}, $s_flag{$n}, $s_access{$n}, $s_include{$n}); } なんだか、長くなっちゃいましたね... (イメージです。実際は赤と緑で表示されます) make country table sort and dump netmask 01000111000001101110100000000100 xx.x.232.4 US 640 12 190320 01000111000001101110100000000101 xx.x.232.4/32 US 630 4 190418 01000111000001101110100000000111 xx.x.232.4/31 US 4 8 190402 (中略) 37.49.227.xxx/32 2130 4 190418 (2) 37.49.230.xxx/30 710 4 190312 (2) 前半で、集約が発生した場合に(赤か緑で)色をつけながらエラー出力(STDERR)へ表示して、 awk '{print $1;}' ログファイル名 > /home/hoge/ban_list.txt のように、一番左のIP列だけ抜き出してもいいですし、 スパムリファラーのIPアドレスを抜き出す例長くなってしまいますが、さらにもう少し... index.html --+--- skin/pukiwiki.css.php (モバイルだと keitai.css.php) | -> link rel=stylesheet で読み込んでいる部分 +--- image/rss.png | -> サイトのフッターに、img src で表示している画像 +--- ... 大抵のWebサイトなら、各ページで共通するCSSや画像を読み込む作りになっていると思いますが なので、
みたいに、IPアドレス毎にカウントアップ or ダウンすれば、 例として、最新2ファイルを拾いたい場合。-1 は数字の1です。Lではなく。 $ cat `ls -1r /var/log/httpd/access_log* | tail -2` ...まぁ、いたちごっこ、ではありますが。 |
|