|
~ To be, or not to be, or to take a lunch anyway. ~ 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` ...まぁ、いたちごっこ、ではありますが。 |
|