แจก SpamAssassin Plugin URL2BADIP

ระยะหลังๆ มานี่จะมี Spam แบบแปลกๆ เข้ามีโดยจะใช้ชื่อโดเมน .us, .me เป็นส่วนใหญ่ ตามตัวอย่าง

Received: from pure-garcinia-work.me (1974.clients.serverdeals.com [198.50.26.2])
by x (mailgw2) with ESMTP id SVE9H429848
for <x>; Sun, 01 Jun 2014 15:24:51 +0700
Date: Sun, 01 Jun 2014 01:23:47 -0700
Message-ID: <9017172.15147579@pure-garcinia-work.me>
From: MagicFatBurner <secretslim@pure-garcinia-work.me>
Content-Type: text/plain
To: <x>
Subject: 2014-Miracle, Dr.Oz-PerfectBody 100% Satisfaction Guaranteed.
Mime-Version: 1.0

It’s like turning your body into a calorie burning factory
without any of the extra work.

This One-Instant Trick Can Make The Difference

A More Fit You Is Possible if you go here now:

http://safe. pure-garcinia-work. me

และจะเป็นโดเมนที่เพิ่งจะจดมาได้ไม่นาน ซึ่งมันได้ฝ่าด่านเข้ามาใน Inbox จนได้เลยต้องหาวิธีสร้าง Rule ของ SpamAssassin ขึ้นมา แต่แล้วก็ไปเจอทางที่ง่ายกว่า คือ ไม่ว่าจะเปลี่ยนชื่อโดเมนเป็นอะไร หรือจดมาเมื่อไหร่ มันจะ Link ไปที่เว็บหลักของมันที่ IP เดิมๆ เพื่อ Redirect ต่อไปอีกที ก็เลยหาเขียน Plugin นี้ขึ้นมา เลยเอามาแบ่งกัน

สร้างไฟล์ URL2BadIP.pm

package Mail::SpamAssassin::Plugin::URLBadIP;

use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Constants qw(:ip);
#use Mail::SpamAssassin::Logger;
#use Data::Dumper;

use strict;
use warnings;
use bytes;
use re 'taint';

use vars qw(@ISA);
@ISA = qw(Mail::SpamAssassin::Plugin);

# constructor: register the eval rule
sub new {
  my $class = shift;
  my $mailsaobject = shift;

  # some boilerplate...
  $class = ref($class) || $class;
  my $self = $class->SUPER::new($mailsaobject);
  bless ($self, $class);

  # the important bit!
  $self->register_eval_rule("check_for_http_ip");

  return $self;
}

###########################################################################


sub check_for_http_ip {
  my ($self, $pms, $rule) = @_;
  return 0;
}

sub check_post_dnsbl {
  #my ($self,$pms, $rule) = @_;
  my ($self, $opts) = @_;
  my $pms = $opts->{permsgstatus};
  my $msg = $opts->{msg};
  my @a;
  my $a;
  my $b;
  my $di;
  my @urls;
  my @ips;
  my @hostip;
  my @uniqueips;

  my $IP_ADDRESS = IP_ADDRESS;

  @urls = $pms->get_uri_list();

  my @uniqueurls = uniq(@urls);

  foreach my $uri (@uniqueurls) {
      if ($uri =~ m{^(https?://)([^/]+?)((?::\d*)?\/.*)?$}i) {
      my($proto, $host, $rest) = ($1,$2,$3);

      dbg("debug:Found $host");
      return 0 unless defined $host;
      return 0 unless $pms->is_dns_available();
      $self->load_resolver();

      if ($host =~ /($IP_ADDRESS)/) {
        push(@ips,$host);
      } else {
        my @hip = $self->lookup_a($host);
        push(@ips, @hip);
      } 
    } 
  } 

  @uniqueips = uniq(@ips);

  foreach my $rule (keys(%{$pms->{conf}->{uri2badip}})) {
    foreach my $ip (@uniqueips) {
        @a = split /\./, $ip;
        $di = getIp(@a);
        if ($pms->{conf}->{uri2badip}->{$rule} =~ /\/\d/) { # search ip in CIDR range
         ($a,$b) = getNetwork($pms->{conf}->{uri2badip}->{$rule});
         if (($di >= $a) && ($di <= $b)){
          dbg ("hit rule: $rule");
          $pms->got_hit($rule);
          return 1;
         }
        } else {
          if ($ip =~ /$pms->{conf}->{uri2badip}->{$rule}/) {
                dbg("hit $rule");
                $pms->got_hit($rule);
                return 1;
          }
        }
     }
  }
  return 0;
}

sub parse_config {
  my ($self, $opts) = @_;
  my $key = $opts->{key};
  if ($key eq 'uri2badip') {
    if ($opts->{value} =~ /^(\S+)\s+(\S+)\s*$/) {
      my $rulename = $1;
      my $ip = $2;
      dbg("registering $rulename $ip");
      $opts->{conf}->{uri2badip}->{$rulename} = $ip;
      $self->inhibit_further_callbacks(); return 1;
    }
  }
  return 0;
}

sub uniq {
    my %seen;
    grep !$seen{$_}++, @_;
}

sub getIp {
        return ($_[0]*256*256*256) + ($_[1]*256*256) + ($_[2]*256) + $_[3];
}

sub getNetwork {
        my @a = split(/[\/|\.]/, +shift);
        return (getIp(@a[0 .. 3]), (getIp(@a[0 .. 3]) + (2 ** (32 - $a[4]))));
}

sub lookup_a {
  my ($self, $name) = @_;

  return undef unless $self->load_resolver();
  if ($self->{main}->{local_tests_only}) {
    dbg("dns: local tests only, not looking up A records");
    return undef;
  }

  return if ($self->server_failed_to_respond_for_domain ($name));

  dbg("dns: looking up A records for '$name'");
  my @addrs;

  if (exists $self->{dnscache}->{A}->{$name}) {
    my $addrptr = $self->{dnscache}->{A}->{$name};
    @addrs = @{$addrptr};

  } else {
    eval {
      my $query = $self->{resolver}->send($name);
      if ($query) {
        foreach my $rr ($query->answer) {
          if ($rr->type eq "A") {
            push (@addrs, $rr->address);
          }
        }
      }
      $self->{dnscache}->{A}->{$name} = [ @addrs ];
      1;
    } or do {
      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
      dbg("dns: A lookup failed horribly, perhaps bad resolv.conf setting? (%s)", $eval_stat);
      return undef;
    };
  }

  dbg("dns: A records for '$name': " . join(' ',@addrs));
  return @addrs;
}

sub load_resolver {
  my ($self) = @_;
  $self->{resolver} = $self->{main}->{resolver};
  return $self->{resolver}->load_resolver();
}


sub server_failed_to_respond_for_domain {
  my ($self, $dom) = @_;
  if ($self->{dns_server_too_slow}->{$dom}) {
    dbg("dns: server for '$dom' failed to reply previously, not asking again");
    return 1;
  }
  return 0;
}

sub set_server_failed_to_respond_for_domain {
  my ($self, $dom) = @_;
  dbg("dns: server for '$dom' failed to reply, marking as bad");
  $self->{dns_server_too_slow}->{$dom} = 1;
}

sub dbg { Mail::SpamAssassin::Plugin::dbg ("URLBadIP: @_"); }

1;

สร้างไฟล์ URL2BadIP.cf

ifplugin Mail::SpamAssassin::Plugin::URLBadIP

uri2badip uri2badip_192.31.186.3/32 192.31.186.3/32
header URI2BADIP1 eval:check_for_http_ip("192.31.186.3/32") 
describe URI2BADIP1 URL pointed to spam ip
score URI2BADIP1 50.00

uri2badip uri2badip_66.96.243.38 66.96.243.38
header URI2BADIP2 eval:check_for_http_ip("66.96.243.38") 
describe URI2BADIP2 URL pointed to spam ip
score URI2BADIP2 20.00

endif

เพิ่มบรรทัดในไฟล์ local.cf

loadplugin Mail::SpamAssassin::Plugin::URL2BadIP URL2BadIP.pm

ขอบคุณครับ +1 ครับผม

ขอบคุณครับ ถ้าเมล์เยอะๆ อาจต้องระวังเวลาที่ใช้ในการ lookup ip ด้วยนะครับ

แก้ไข code เนื่องจาก SpamAssassin เวอร์ชั่น 3.4.0 ตัดเอา function lookup_a ออกไปจาก Mail::SpamAssassin::Dns จึงต้องเอา function มาใส่ไว้ใน module เลย
และได้เพิ่ม feature บางอย่างเข้ามา คือ

  • ลดขั้นตอนการทำงาน ไม่ได้ทำงานซ้ำซ้อน เพราะ code ก่อนหน้านี้ จะทำงานซ้ำๆ ตามจำนวน rule ที่มี
  • ทำงานหลังจาก DNSBL เพื่อไม่ให้ไปขัดจังหวะ การ query dns blacklist ต่างๆ
  • เพิ่มการตรวจสอบทั้งแบบ IP เดียว (x.x.x.x) และแบบ IP ชุด (x.x.x.x/x)

พบปัญหาในการใช้งาน อย่างไรก็แจ้งมาได้ครับ เพราะผมเขียนขึ้นมาใช้เอง และเอามาแจกให้ได้ใช้กัน

ขอบคุณครับ +1

แก้ไขเพิ่มเติมหลังจาก ติดตั้งเวอร์ชั่น 3.4.2 ล่าสุดแล้ว ปลั๊กอินนี้ไม่ทำงาน
และแม้ว่า 3.4.2 จะมีปลั๊กอินชื่อ URILocalBL มาให้แต่พบว่าไม่สามารถตรวจสอบได้ตรงกับความจริง
เช่น เนื้อหาอีเมล์ส่ง url มาเป็น http:// spam. url.com แต่ปลั๊กอินที่ว่านี้ไป ตรวจสอบ url.com แทน
ผมเลยประยุกต์และแก้ให้เพิ่มเติม ตามที่ผมเคยเขียนไว้ก่อนหน้านี้ และทำการแก้ไขชื่อนิดหน่อย และก็
เพิ่ม config ใหม่เช่น

สามารถกำหนดรูปแบบของ rule ดังนี้

uri_bl_cidr SYMBOLIC_TEST_NAME a.a.a.a b.b.b.b/cc d.d.d.d-e.e.e.e

สามารถยกเว้นสำหรับบาง url ได้เช่น

uri_block_exclude TEST1 www .baidu.com

สามารถกำหนดรูปแบบ rule ตามประเทศได้ เช่น

uri_bl_cc TEST1 cn

หรือตามชื่อ ISP ได้เช่น

uri_bl_isp TEST3 “ColoCrossing”

*** เครื่องจะต้องลง GeoIP C library 1.6.3 และ GeoIP perl API 1.4.4
และจะต้องมี GEOIP_ISP_EDITION

สร้างไฟล์ URLBLIP.pm





package Mail::SpamAssassin::Plugin::URLBLIP;
use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Util qw(untaint_var);

use Geo::IP;
use Net::CIDR::Lite;
use Socket;

use strict;
use warnings;
use bytes;
use re 'taint';
use version;

# need GeoIP C library 1.6.3 and GeoIP perl API 1.4.4 or later to avoid messages leaking - Bug 7153
my $gic_wanted = version->parse('v1.6.3');
my $gic_have = version->parse(Geo::IP->lib_version());
my $gip_wanted = version->parse('v1.4.4');
my $gip_have = version->parse($Geo::IP::VERSION);

use vars qw(@ISA);
@ISA = qw(Mail::SpamAssassin::Plugin);

sub new {
  my $class = shift;
  my $mailsaobject = shift;
  $class = ref($class) || $class;
  my $self = $class->SUPER::new($mailsaobject);
  bless ($self, $class);

  my $flags = 0;
  eval '$flags = Geo::IP::GEOIP_SILENCE' if ($gip_wanted >= $gip_have);

  if ($flags && $gic_wanted >= $gic_have) {
    $self->{geoip} = Geo::IP->new(GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE | $flags);
    $self->{geoisp} = Geo::IP->open_type(GEOIP_ISP_EDITION, GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE | $flags);
  } else {
    open(OLDERR, ">&STDERR");
    open(STDERR, ">", "/dev/null");
    $self->{geoip} = Geo::IP->new(GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE);
    $self->{geoisp} = Geo::IP->open_type(GEOIP_ISP_EDITION, GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE);
    open(STDERR, ">&OLDERR");
    close(OLDERR);
  }

  $self->register_eval_rule("check_uri_bl_ip");
  $self->set_config($mailsaobject->{conf});
  return $self;
}

sub set_config {
  my ($self, $conf) = @_;
  my @cmds;

  my $pluginobj = $self;      

  push (@cmds, {
    setting => 'uri_bl_cc',
    is_priv => 1,
    code => sub {
      my ($self, $key, $value, $line) = @_;

      if ($value !~ /^(\S+)\s+(.+)$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $name = $1;
      my $def = $2;
      my $added_criteria = 0;

      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{countries} = {};

      # this should match all country codes including satellite providers
      while ($def =~ m/^\s*([a-z][a-z0-9])(\s+(.*)|)$/) {
        my $cc = $1;
        my $rest = $2;

        #dbg("config: uri_bl_cc adding %s to %s
", $cc, $name);
        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{countries}->{uc($cc)} = 1;
        $added_criteria = 1;

        $def = $rest;
      }

      if ($added_criteria == 0) {
        warn "config: no arguments";
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      } elsif ($def ne '') {
        warn "config: failed to add invalid rule $name";
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }

      dbg("config: uri_bl_cc added %s
", $name);

      $conf->{parser}->add_test($name, 'check_uri_bl_ip()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
    }
  }) if (defined $self->{geoip});

  push (@cmds, {
    setting => 'uri_bl_isp',
    is_priv => 1,
    code => sub {
      my ($self, $key, $value, $line) = @_;

      if ($value !~ /^(\S+)\s+(.+)$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $name = $1;
      my $def = $2;
      my $added_criteria = 0;

      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{isps} = {};

      # gather up quoted strings
      while ($def =~ m/^\s*"([^"]*)"(\s+(.*)|)$/) {
        my $isp = $1;
        my $rest = $2;

        dbg("config: uri_bl_isp adding \"%s\" to %s
", $isp, $name);
        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{isps}->{$isp} = 1;
        $added_criteria = 1;

        $def = $rest;
      }

      if ($added_criteria == 0) {
        warn "config: no arguments";
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      } elsif ($def ne '') {
        warn "config: failed to add invalid rule $name";
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }

      $conf->{parser}->add_test($name, 'check_uri_bl_ip()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
    }
  }) if (defined $self->{geoisp});

  push (@cmds, {
    setting => 'uri_bl_cidr',
    is_priv => 1,
    code => sub {
      my ($self, $key, $value, $line) = @_;

      if ($value !~ /^(\S+)\s+(.+)$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $name = $1;
      my $def = $2;
      my $added_criteria = 0;

      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr} = new Net::CIDR::Lite;

      # match individual IP's, subnets, and ranges
      while ($def =~ m/^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2}|-\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?)(\s+(.*)|)$/) {
        my $addr = $1;
        my $rest = $3;

        dbg("config: uri_bl_cidr adding %s to %s
", $addr, $name);

        eval { $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr}->add_any($addr) };
        last if ($@);

        $added_criteria = 1;

        $def = $rest;
      }

      if ($added_criteria == 0) {
        warn "config: no arguments";
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      } elsif ($def ne '') {
        warn "config: failed to add invalid rule $name";
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }

      # optimize the ranges
      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr}->clean();

      $conf->{parser}->add_test($name, 'check_uri_bl_ip()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
    }
  });

  push (@cmds, {
    setting => 'uri_block_exclude',
    is_priv => 1,
    code => sub {
      my ($self, $key, $value, $line) = @_;

      if ($value !~ /^(\S+)\s+(.+)$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $name = $1;
      my $def = $2;
      my $added_criteria = 0;

      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{exclusions} = {};

      # match individual IP's, or domain names
      while ($def =~ m/^\s*((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(([a-z0-9][-a-z0-9]*[a-z0-9](\.[a-z0-9][-a-z0-9]*[a-z0-9]){1,})))(\s+(.*)|)$/) {
        my $addr = $1;
        my $rest = $6;

        dbg("config: uri_block_exclude adding %s to %s
", $addr, $name);

        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{exclusions}->{$addr} = 1;

        $added_criteria = 1;

        $def = $rest;
      }

      if ($added_criteria == 0) {
        warn "config: no arguments";
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      } elsif ($def ne '') {
        warn "config: failed to add invalid rule $name";
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }

      $conf->{parser}->add_test($name, 'check_uri_bl_ip()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
    }
  });
  
  $conf->{parser}->register_commands(\@cmds);
}  

sub check_uri_bl_ip {
  my ($self, $permsg) = @_;

  my @urls = $permsg->get_uri_list();
  my $test = $permsg->{current_rule_name}; 
  my $rule = $permsg->{conf}->{uri_local_bl}->{$test};

  dbg("check: uri_local_bl evaluating rule %s
", $test);

  my @uniqueurls = uniq(@urls);

  foreach my $uri (@uniqueurls)  {

    if ($uri =~ m{^(https?://)([^/]+?)((?::\d*)?\/.*)?$}i) {
       my($proto, $host, $rest) = ($1,$2,$3);

       $host = lc($host);

      # skip if the domain name was matched
      if (exists $rule->{exclusions} && exists $rule->{exclusions}->{$host}) {
        dbg("check: uri_local_bl excludes %s as *.%s
", $host);
        next;
      }
      my @addrs = gethostbyname($host);

      @addrs = map { inet_ntoa($_); } @addrs[4..$#addrs];

      dbg("check: uri_local_bl %s addrs %s
", $host, join(', ', @addrs));

      for my $ip (@addrs) {
        # skip if the address was matched
        if (exists $rule->{exclusions} && exists $rule->{exclusions}->{$ip}) {
          dbg("check: uri_local_bl excludes %s(%s)
", $host, $ip);
          next;
        }

        if (exists $rule->{countries}) {
          dbg("check: uri_local_bl countries %s
", join(' ', sort keys %{$rule->{countries}}));

          my $cc = $self->{geoip}->country_code_by_addr($ip);

          dbg("check: uri_local_bl host %s(%s) maps to %s
", $host, $ip, (defined $cc ? $cc : "(undef)"));
          next unless defined $cc;

          next unless (exists $rule->{countries}->{$cc});

          dbg("check: uri_bl_cc host %s(%s) matched
", $host, $ip);

          if (would_log('dbg', 'rules') > 1) {
            dbg("check: uri_bl_cc criteria for $test met");
          }
      
          $permsg->test_log("Host: $host in $cc");
          $permsg->got_hit($test);

          return 0;
        }

        if (exists $rule->{isps}) {
          dbg("check: uri_local_bl isps %s
", join(' ', map { '"' . $_ . '"'; } sort keys %{$rule->{isps}}));

          my $isp = $self->{geoisp}->isp_by_name($ip);

          dbg("check: uri_local_bl isp %s(%s) maps to %s
", $host, $ip, (defined $isp ? '"' . $isp . '"' : "(undef)"));
          next unless defined $isp;
          next unless (exists $rule->{isps}->{$isp});

          dbg("check: uri_bl_isp host %s(%s) matched
", $host, $ip);

          if (would_log('dbg', 'rules') > 1) {
            dbg("check: uri_bl_isp criteria for $test met");
          }
      
          $permsg->test_log("Host: $host in \"$isp\"");
          $permsg->got_hit($test);

          return 0;
        }

        if (exists $rule->{cidr}) {
          dbg("check: uri_bl_cidr list %s
", join(' ', $rule->{cidr}->list_range()));

          next unless ($rule->{cidr}->find($ip));

          dbg("check: uri_bl_cidr host %s(%s) matched
", $host, $ip);

          if (would_log('dbg', 'rules') > 1) {
            dbg("check: uri_bl_cidr criteria for $test met");
          }

          $permsg->test_log("Host: $host as $ip");
          $permsg->got_hit($test);

          return 0;
        }
      }
    }
  }

  dbg("check: uri_local_bl %s no match
", $test);
  return 0;
}

sub uniq {
    my %seen;
    grep !$seen{$_}++, @_;
}

ไฟล์


ifplugin Mail::SpamAssassin::Plugin::URLBLIP

uri_bl_cidr URI2BADIP1 192.31.186.3/32
header URI2BADIP1 eval:check_uri_bl_ip()
describe URI2BADIP1 Found blacklist ip on URL
score URI2BADIP1 10.00

uri_bl_cidr URI2BADIP2  158.69.56.6        
header URI2BADIP2 eval:check_uri_bl_ip()
describe URI2BADIP2 Found blacklist ip on URL
score URI2BADIP2 10.00

endif


เพิ่มบรรทัดในไฟล์ local.cf


loadplugin Mail::SpamAssassin::Plugin::URLBLIP URLBLIP.pm