package Iodef::Pb::Format::Snort; use base 'Iodef::Pb::Format'; use strict; use warnings; use Snort::Rule; use Regexp::Common qw/net/; use Parse::Range qw(parse_range); sub write_out { my $self = shift; my $args = shift; my $config = $args->{'config'}; my $array = $self->SUPER::to_keypair($args); return '' unless(exists(@{$array}[0]->{'address'})); ## TODO -- push this to the client my @config_search_path = ('claoverride', $args->{'query'}, 'client' ); # allow override of snort rule params my $tag = $self->SUPER::confor($config, \@config_search_path, 'snort_tag', undef); my $pri = $self->SUPER::confor($config, \@config_search_path, 'snort_priority', undef); my $sid = $self->SUPER::confor($config, \@config_search_path, 'snort_startsid', 5000000); my $thresh = $self->SUPER::confor($config, \@config_search_path, 'snort_threshold', 'type limit,track by_src,count 1,seconds 3600'); my $classtype = $self->SUPER::confor($config, \@config_search_path, 'snort_classtype', undef); my $srcnet = $self->SUPER::confor($config, \@config_search_path, 'snort_srcnet', 'any'); my $srcport = $self->SUPER::confor($config, \@config_search_path, 'snort_srcport', 'any'); my $msg_prefix = $self->SUPER::confor($config, \@config_search_path, 'snort_msg_prefix', ''); my $rules = ''; foreach (@$array){ next unless($_->{'address'}); if(exists($_->{'rdata'}) && defined($_->{'rdata'})){ $_->{'portlist'} = 53; } my $portlist = ($_->{'portlist'}) ? $_->{'portlist'} : 'any'; my $priority = 1; for(lc($_->{'severity'})){ $priority = 5 if(/medium/); $priority = 9 if(/high/); } my $dstnet = 'any'; my $dstport = 'any'; my $urlhost = undef; my $dnsdomain = undef; my ($urlport, $urlfile); if (isipv4($_->{'address'})) { $dstnet = $_->{'address'}; $dstport = $portlist; } elsif (isdomain($_->{'address'})) { #$_->{'protocol'} = 17 unless($_->{'protocol'}); # override this for now, regardless of what's in $PROTOCOL # most of these will be looking at udp packets # if anything it should be set to undef $_->{'protocol'} = 17; $dstport = 53; $dstnet = 'any'; $dnsdomain = $_->{'address'}; } else { ($urlhost, $urlport, $urlfile) = ishttpurl($_->{'address'}); if (defined($urlhost)) { my $urlisip = isipv4($urlhost); $_->{'protocol'} = 6; # TCP by definition $dstnet = ($urlisip ? $urlhost : 'any'); # $EXTERNAL_NET? $dstport = $urlport || '$HTTP_PORTS'; } else { $rules .= "### sorry. not sure what to do with address: " . $_->{'address'} . " so i'm skipping this one.\n\n"; next; } } my $r = Snort::Rule->new( -action => 'alert', -proto => translate_proto($_->{'protocol'}), -src => $srcnet, -sport => join(',', (($srcport =~ /^[,\-\d]+$/) ? parse_range($srcport) : $srcport)), -dst => $dstnet, -dport => join(',', (($dstport =~ /^[,\-\d]+$/) ? parse_range($dstport) : $dstport)), -dir => '->', ); my $reference = make_snort_ref($_->{'alternativeid'}); $r->opts('msg',$msg_prefix . $_->{'restriction'}.' - '.$_->{'assessment'}.' '.$_->{'description'}); $r->opts('threshold', $thresh) if $thresh; $r->opts('tag', $tag) if $tag; $r->opts('classtype', $classtype) if $classtype; $r->opts('sid', $sid++); $r->opts('reference',$reference) if($reference); $r->opts('priority', $pri || $priority); #alert tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS (Msg: "Mal_URI #www.badsite.com/malware.pl"; flow: to_server, established; #content:"Host|3A| www.basesite.com|0D 0A|"; nocase; #content:"/malware.pl"; http_uri; nocase; sid:23424234;) # avoid # FATAL ERROR: ... ParsePattern() dummy buffer overflow, make a smaller pattern please! (Max size = 2047) my $skip_this_rule = 0; if ($urlhost) { $rules .= "# $urlhost [urlhost rule]\n"; $r->opts('flow', 'to_server'); if (!isipv4($urlhost)) { if (length($urlhost) > 2047) { $rules .= "# Skipping rule for $urlhost because the length exceeds snort's content limit of 2047\n\n"; $skip_this_rule = 1; } # http://stackoverflow.com/questions/5757290/http-header-line-break-style $r->opts('content', 'Host|3A| ' . escape_content($urlhost) . "|0D 0A|"); # add \r\n so eg www.foo.co doesnt also match www.foo.co $r->opts('http_header'); $r->opts('nocase'); } if ($urlfile) { if (length($urlfile) > 2047) { $rules .= "# Skipping rule for $urlfile because the length exceeds snort's content limit of 2047\n\n"; $skip_this_rule = 1; } $r->opts('content', escape_content($urlfile)); $r->opts('http_uri'); $r->opts('nocase'); } } elsif ($dnsdomain) { $rules .= "# $dnsdomain [domain-only (dns) rule]\n"; # alert udp !$DNS_SERVERS any -> any 53 ( msg:"RESTRICTED - botnet domain unknown"; sid:1; content:"|03|foo|03|com"; ) $r->opts('content', content_as_dns_query($dnsdomain)); # dont have to escape bc we passed isdomain() test above $r->opts('nocase'); } else { $rules .= "# $dstnet [ip address only / not url / not domain rule]\n" } $rules .= $r->string()."\n\n" unless($skip_this_rule); } return $rules; } sub content_as_dns_query { my $d = shift; return '' unless $d; return join('', map { sprintf("|%2.2x|%s", length($_), $_) } split('\.', $d)); } sub isipv4 { my ($i, $m) = (shift, 32); ($i, $m) = split('/', $i) if ($i =~ /\//); return 1 if ( ($i =~ /^0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])(\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])){3}$/) && ($m > 0 && $m < 33) ); return 0; } sub translate_proto { my $protonum = shift; my $protos = { 6 => 'tcp', 17 => 'udp', 1 => 'icmp' }; # snort only supports these, default is 'ip' return $protos->{$protonum} if (defined($protonum) && exists($protos->{$protonum})); return 'ip'; } sub confor { my $conf = shift; my $name = shift; my $def = shift; # handle # snort_foo = 1,2,3 # snort_foo = "1,2,3" if (exists($conf->{$name}) && defined($conf->{$name})) { return ref($conf->{$name} eq "ARRAY") ? join(', ', @{$conf->{$name}}) : $conf->{$name}; } return $def; } sub isdomain { my $x = shift; if ($x =~ /^[0-9a-z\.\-]+\.[a-z]{2,6}$/i) { return 1; } return 0; } sub ishttpurl { my $x = shift; return (undef, undef, undef) unless $x; # it only makes sense to try to look for http: urls # https will be encrypted, ftp doesnt contain header fields to trigger on, etc if ($x =~ /http:\/\/([^\/]+)[\/]{0,1}(.*)/) { my ($h, $p) = split(':', $1); my $d = ($2 ? '/'.$2 : ''); return ($h, $p, $d); } return (undef, undef, undef); } sub make_snort_ref { my $r = shift; return undef unless defined($r); if ($r =~ /(https?):\/\/(.*)/) { return "url," . $2 if ($1 eq "http"); return "urlssl," . $2; } return 'url,' . $r if($r =~ /[a-z0-9-.]+\.[a-z]{2,6}(\/)?/); return undef; } # http://manual.snort.org/node32.html#SECTION00451000000000000000 #Note: #Also note that the following characters must be escaped inside a content rule: # # ; \ " sub escape_content { my $x = shift; $x =~ s/\\/\\\\/gi; $x =~ s/;/\\;/gi; $x =~ s/\"/\\"/gi; return $x; } 1;