414
my $whitelistreport = '';
415
if ($iswhitelisted) {
416
$whitelistreport = ' (' .
417
MailScanner::Config::LanguageValue($this, 'whitelisted') .
384
421
if (!$iswhitelisted) {
385
422
# Not whitelisted, so do the RBL checks
386
#$rblspamheader = MailScanner::RBLs::Checks($this);
387
423
($rblcounter, $rblspamheader) = MailScanner::RBLs::Checks($this);
388
$RBLsaysspam = 1 if $rblcounter;
389
#$RBLsaysspam = 1 if $rblspamheader;
424
my $rblthreshold = MailScanner::Config::Value('normalrbls', $this);
425
my $highrblthreshold = MailScanner::Config::Value('highrbls', $this);
426
$rblthreshold = 1 if $rblthreshold <= 1;
427
$highrblthreshold = 1 if $highrblthreshold <= 1;
428
$RBLsaysspam = 1 if $rblcounter >= $rblthreshold;
390
429
# Add leading "spam, " if RBL says it is spam. This will be at the
391
430
# front of the spam report.
392
$rblspamheader = $LocalSpamText . ', ' . $rblspamheader if $rblcounter;
393
$this->{isspam} = 1 if $rblcounter;
394
$this->{isrblspam} = 1 if $rblcounter;
395
$this->{ishigh} = 1 if $rblcounter >= MailScanner::Config::Value(
397
#print STDERR "RBL report is \"$rblspamheader\"\n";
398
#print STDERR "RBLCounter = $rblcounter\n";
399
#print STDERR "HighRBLs = " .
400
# MailScanner::Config::Value('highrbls', $this) . "\n";
431
$this->{isspam} = 1 if $RBLsaysspam;
432
$this->{isrblspam} = 1 if $RBLsaysspam;
433
$this->{ishigh} = 1 if $rblcounter >= $highrblthreshold;
435
# rblspamheader is useful start to spamreport if RBLsaysspam.
437
# Do the Custom Spam Checker
438
my($gsscore, $gsreport);
439
#print STDERR "In Message.pm about to look at gsscanner\n";
441
#print STDERR "In Message.pm about to run gsscanner\n";
442
($gsscore, $gsreport) = MailScanner::GenericSpam::Checks($this);
443
#print STDERR "In Message.pm we got $gsscore, $gsreport\n";
444
$this->{gshits} = $gsscore;
445
$this->{gsreport} = $gsreport;
446
$this->{sascore} = $gsscore; # Add the score
447
MailScanner::Log::InfoLog("Custom Spam Scanner for message %s from %s " .
448
"(%s) to %s report is %s %s",
449
$this->{id}, $this->{clientip},
450
$this->{from}, $todomain, $gsscore, $gsreport)
451
if $LogSpam && ($gsscore!=0 || $gsreport ne "");
403
454
# Don't do the SA checks if they have said no.
404
455
unless (MailScanner::Config::Value('usespamassassin', $this)) {
405
456
$this->{spamwhitelisted} = $iswhitelisted;
406
$this->{spamreport} = $rblspamheader;
459
MailScanner::Config::Value('reqspamassassinscore',$this)+0.0;
462
MailScanner::Config::Value('highspamassassinscore',$this)+0.0;
407
463
MailScanner::Log::InfoLog("Message %s from %s (%s) to %s is %s",
408
464
$this->{id}, $this->{clientip},
409
465
$this->{from}, $todomain, $rblspamheader)
410
466
if $RBLsaysspam && $LogSpam;
467
# Replace start of report if it wasn't spam from rbl but now is.
468
$this->{spamreport} = ($this->{isspam})?$LocalSpamText:$LocalNotSpamText;
469
$this->{spamreport} .= $whitelistreport;
470
$this->{spamreport} .= ', ' if $this->{spamreport};
471
$this->{spamreport} .= $rblspamheader if $rblspamheader;
472
$this->{spamreport} .= ', ' if $this->{spamreport} && $rblspamheader;
473
$this->{spamreport} .= $gsscore+0.0 if $gsscore!=0;
474
$this->{spamreport} .= ', ' if $this->{spamreport} && $gsscore!=0;
475
$this->{spamreport} .= $gsreport if $gsreport ne "";
476
$this->{spamreport} = $this->ReflowHeader(
477
MailScanner::Config::Value('spamheader',$this),
478
$this->{spamreport});
479
return $this->{isspam};
414
482
# If it's spam and they dont want to check SA as well
415
483
if ($this->{isspam} &&
416
484
!MailScanner::Config::Value('checksaifonspamlist', $this)) {
417
485
$this->{spamwhitelisted} = $iswhitelisted;
418
$this->{spamreport} = $rblspamheader;
419
486
MailScanner::Log::InfoLog("Message %s from %s (%s) to %s is %s",
420
487
$this->{id}, $this->{clientip},
421
488
$this->{from}, $todomain, $rblspamheader)
422
489
if $RBLsaysspam && $LogSpam;
490
# Replace start of report if it wasn't spam from rbl but now is.
491
$this->{spamreport} = ($this->{isspam})?$LocalSpamText:$LocalNotSpamText;
492
$this->{spamreport} .= $whitelistreport;
493
$this->{spamreport} .= ', ' if $this->{spamreport};
494
$this->{spamreport} .= $rblspamheader if $rblspamheader;
495
$this->{spamreport} .= ', ' if $this->{spamreport} && $rblspamheader;
496
$this->{spamreport} .= $gsscore+0.0 if $gsscore!=0;
497
$this->{spamreport} .= ', ' if $this->{spamreport} && $gsscore!=0;
498
$this->{spamreport} .= $gsreport if $gsreport ne "";
499
$this->{spamreport} = $this->ReflowHeader(
500
MailScanner::Config::Value('spamheader',$this),
501
$this->{spamreport});
423
502
return $RBLsaysspam;
459
538
if ($this->{isspam}) {
460
539
#print STDERR "It is spam\nInclude SA = $includesaheader\n";
461
540
#print STDERR "SAHeader = $saheader\n";
462
$spamheader = $rblspamheader;
463
541
# If it's SA spam as well, or they always want the SA header
464
542
if ($SAsaysspam || $includesaheader) {
465
543
#print STDERR "Spam or Add SA Header\n";
466
$spamheader = $LocalSpamText unless $spamheader;
467
$spamheader .= ', ' if $spamheader && $saheader;
468
$spamheader .= $saheader;
469
544
$this->{ishigh} = 1 if $SAHighScoring;
545
$this->{spamreport} = ($this->{isspam})?$LocalSpamText:$LocalNotSpamText;
546
$this->{spamreport} .= $whitelistreport;
547
$this->{spamreport} .= ', ' if $this->{spamreport};
548
$this->{spamreport} .= $rblspamheader if $rblspamheader;
549
$this->{spamreport} .= ', ' if $this->{spamreport} && $rblspamheader;
550
$this->{spamreport} .= $gsscore+0.0 if $gsscore!=0;
551
$this->{spamreport} .= ', ' if $this->{spamreport} && $gsscore!=0;
552
$this->{spamreport} .= $gsreport if $gsreport ne "";
553
$this->{spamreport} .= ', ' if $this->{spamreport} && $gsreport;
554
$this->{spamreport} .= $saheader if $saheader ne "";
472
557
# It's not spam...
473
558
#print STDERR "It's not spam\n";
474
559
#print STDERR "SAHeader = $saheader\n";
475
$spamheader = MailScanner::Config::LanguageValue($this, 'notspam');
476
if ($iswhitelisted) {
477
$spamheader .= ' (' .
478
MailScanner::Config::LanguageValue($this, 'whitelisted') .
481
# so RBL report must be blank as you can't force inclusion of that.
482
# So just include SA report.
483
$spamheader .= ", $saheader";
486
# Now just reflow and log the results
487
if ($spamheader ne "") {
488
$spamheader = $this->ReflowHeader(
489
MailScanner::Config::Value('spamheader',$this), $spamheader);
490
$this->{spamreport} = $spamheader;
560
$this->{spamreport} = ($this->{isspam})?$LocalSpamText:$LocalNotSpamText;
561
$this->{spamreport} .= $whitelistreport;
562
$this->{spamreport} .= ', ' if $this->{spamreport};
563
$this->{spamreport} .= $rblspamheader if $rblspamheader;
564
$this->{spamreport} .= ', ' if $this->{spamreport} && $rblspamheader;
565
$this->{spamreport} .= $gsscore+0.0 if $gsscore!=0;
566
$this->{spamreport} .= ', ' if $this->{spamreport} && $gsscore!=0;
567
$this->{spamreport} .= $gsreport if $gsreport ne "";
568
$this->{spamreport} .= ', ' if $this->{spamreport} && $gsreport;
569
$this->{spamreport} .= $saheader if $saheader ne "";
493
572
# Do the spam logging here so we can log high-scoring spam too
494
573
if (($LogSpam && $this->{isspam}) || ($LogNonSpam && !$this->{isspam})) {
495
my $ReportText = $spamheader;
574
my $ReportText = $this->{spamreport};
496
575
$ReportText =~ s/\s+/ /sg;
497
576
MailScanner::Log::InfoLog("Message %s from %s (%s) to %s is %s",
498
577
$this->{id}, $this->{clientip},
499
578
$this->{from}, $todomain, $ReportText);
581
# Now just reflow and log the results
582
if ($this->{spamreport} ne "") {
583
$this->{spamreport} = $this->ReflowHeader(
584
MailScanner::Config::Value('spamheader',$this),
585
$this->{spamreport});
502
588
return $this->{isspam};
1694
# Unpack a rar file into the named directory.
1695
# Return 1 if an error occurred, else 0.
1696
# Return 0 on success.
1697
# Return "password" if a member was password-protected.
1698
# Very much like UnpackZip except it uses the external "unrar" command.
1700
my($this, $zipname, $explodeinto, $allowpasswords, $onlycheckencryption) = @_;
1702
my($zip, @members, $member, $name, $fh, $safename, $memb, $check, $junk,
1703
$unrar,$IsEncrypted, $PipeTimeOut, $PipeReturn,$NameTwo, $HasErrors,
1704
$member2, $Stuff, $BeginInfo, $EndInfo, $ParseLine, $what);
1706
# Timeout value for unrar is currently the same as that of the file
1707
# command + 20. Julian, when you add the filetimeout to the config file
1708
# perhaps you should think about adding a maxcommandexecutetime setting
1710
$PipeTimeOut = MailScanner::Config::Value('unrartimeout');
1711
$unrar = MailScanner::Config::Value('unrarcommand');
1712
return 1 unless $unrar && -x $unrar;
1714
#MailScanner::Log::WarnLog("UnPackRar Testing : %s", $zipname);
1716
# This part lists the archive contents and makes the list of
1717
# file names within. "This is a list verbose option"
1718
$memb = SafePipe("$unrar v '$explodeinto/$zipname' 2>/dev/null",
1727
my @test = split /\n/, $memb;
1730
# Have to parse the output from the 'v' command and parse the information
1731
# between the ----------------------------- lines
1732
foreach $what (@test) {
1733
# Have we already hit the beginng and now find another ------ string?
1734
# If so then we are at the end
1735
$EndInfo = 1 if $what =~ /-{40,}$/ && $BeginInfo;
1737
# if we are after the begning but haven't reached the end,
1738
# then process this line
1739
if ($BeginInfo && !$EndInfo) {
1740
# If we are on line one then it's the file name with full path
1741
# otherwise we are on the info line containing the attributes
1742
if ($ParseLine eq 1) {
1744
$junk =~ s/^\s+|\s+$//g;
1749
$Stuff =~ s/^\s+|\s+$//g;
1750
# Need to remove redundant spaces from our info line and
1751
# split it into it's components
1753
$Stuff =~ s/\s{2,}/ /g;
1754
my ($RSize,$RPacked,$RRatio,$RDate,$RTime,$RAttrib,$RCrc,$RMeth,$RVer)
1755
= split /\s/, $Stuff;
1756
# If RAttrib doesn't begin with d then it's a file and we
1757
# add it to our $memb string, otherwise we ignore the directory
1759
#MailScanner::Log::WarnLog("UnPackRar InfoLine :%s:", $Stuff);
1760
#MailScanner::Log::WarnLog("UnPackRar Looking at ATTRIB :->%s<-:",
1762
$memb .= "$junk\n" if $RAttrib !~ /^d|^.D/;
1768
# If we have a line full of ---- and $BeginInfo is not set then
1769
# we are at the first and we need to set $BeginInfo so next pass
1770
# begins processing file information
1771
if ($what =~ /-{40,}$/ && ! $BeginInfo) {
1776
# Remove returns from the output string, exit if the archive is empty
1777
# or the output is empty
1780
return 1 if $memb ne '' &&
1781
$memb =~ /(No files to extract|^COMMAND_TIMED_OUT$)/si;
1783
return 0 if $memb eq ''; # JKF If no members it probably wasn't a Rar self-ext
1784
#MailScanner::Log::DebugLog("Unrar : Archive Testing Completed On : %s",
1787
@members = split /\n/, $memb;
1788
$fh = new FileHandle;
1790
foreach $member2 (@members) {
1793
#MailScanner::Log::InfoLog("Checking member %s",$member2);
1794
# Test the current file name to see if it's password protected
1795
# and capture the output. If the command times out, then return
1797
next if $member2 eq "";
1798
$member = quotemeta $member2;
1799
#print STDERR "Member is ***$member***\n";
1801
"$unrar t -p- -idp '$explodeinto/$zipname' $member 2>&1",
1803
#print STDERR "Point 1\n";
1804
return 1 if $check =~ /^COMMAND_TIMED_OUT$/;
1806
# Check for any error with this file. Format is FileName - Error string
1807
if ($check =~ /$member\s+-\s/i){
1808
MailScanner::Log::WarnLog("Unrar: Error in file: %s -> %s",
1813
$check =~ s/\n/:/gsi;
1814
#MailScanner::Log::WarnLog("Got : %s", $check);
1816
# If we get the string Encrypted then we have found a password
1817
# protected archive and we handle it the same as zips are handled
1819
if ($check =~ /\bEncrypted file:\s.+\(password incorrect/si) {
1821
MailScanner::Log::WarnLog("Password Protected RAR Found");
1822
#print STDERR "Checking member " . $member . "\n";
1823
#print STDERR "******** Encryption = " . $IsEncrypted . "\n";
1824
return "password" if !$allowpasswords && $IsEncrypted;
1828
# If they don't want to extract, but only check for encryption,
1829
# then skip the rest of this as we don't actually want the files
1830
# checked against the file name/type rules
1831
next if $onlycheckencryption;
1834
#print STDERR "UnPackRar : Making Safe Name from $name\n";
1836
# There is no facility to change the output name for a rar file
1837
# but we can rename rename the files inside the archive
1838
# prefer to use $NameTwo because there is no path attached
1839
# $safename is guaranteed not to exist, but NameTwo gives us the
1840
# filename without any directory information, which we use later.
1841
$safename = $this->MakeNameSafe($name,$explodeinto);
1842
$NameTwo = $safename;
1843
$NameTwo = $1 if $NameTwo =~ /([^\/]+)$/;
1844
#MailScanner::Log::InfoLog("UnPackRar: Member : %s", $member);
1845
#print STDERR "UnPackRar : Safe Name is $safename\n";
1847
#MailScanner::Log::InfoLog("UnPackRar: SafeName : %s", $safename);
1848
$this->{file2parent}{$name} = $zipname;
1849
$this->{file2parent}{$safename} = $zipname;
1850
$this->{file2safefile}{$name} = $safename;
1851
$this->{safefile2file}{$safename} = $name;
1852
#print STDERR "Archive member \"$name\" is now \"$safename\"\n";
1854
#$this->{file2entity}{$name} = $this->{entity};
1855
$this->{file2safefile}{$name} = $zipname;
1856
#$this->{safefile2file}{$safename} = $zipname;
1858
$safename = "$explodeinto/$safename";
1862
if (!$IsEncrypted && !$HasErrors) {
1863
#print STDERR "Expanding ***$member***\ninto ***$NameTwo***\n";
1864
$PipeReturn = SafePipe(
1865
"$unrar p -y -inul -p- -idp '$explodeinto/$zipname' $member > \"$NameTwo\"",
1867
unless ("$?" == 0 && $PipeReturn ne 'COMMAND_TIMED_OUT'){
1868
# The rename operation failed!, so skip the extraction of a
1869
# potentially bad file name.
1870
# JKF Temporary testing code
1871
#MailScanner::Log::WarnLog("UnPackRar: RC: %s PipeReturn : ",$?,$PipeReturn);
1872
MailScanner::Log::WarnLog("UnPackRar: Could not rename or use " .
1873
"safe name in Extract, NOT Unpacking file %s", $safename);
1876
#MailScanner::Log::InfoLog("UnPackRar: Done...., got %d and %s", $?, $PipeReturn);
1878
#MailScanner::Log::WarnLog("RC = %s : Encrypt = %s : PipeReturn = %s",
1879
# $?,$IsEncrypted,$PipeReturn );
1880
unless ("$?" == 0 && !$HasErrors && !$IsEncrypted &&
1881
$PipeReturn ne 'COMMAND_TIMED_OUT') {
1883
# If we got an error, or this file is encrypted create a zero-length
1884
# file so the filename tests will still work.
1885
MailScanner::Log::WarnLog("Unrar : Encrypted Or Extract Error Creating" .
1886
" 0 length %s",$NameTwo);
1887
$fh->open(">$safename") && $fh->close();
1893
# Modified Julian's code from SweepOther.pm
1894
# Changed to allow execution of any given command line with a time
1895
# control. This could replace any call to system or use of backticks
1897
# $Cmd = command line to execute
1898
# $timeout = max time in seconds to allow execution
1901
my ($Cmd, $TimeOut) = @_;
1903
my($Kid, $pid, $TimedOut, $Str);
1904
$Kid = new FileHandle;
1907
#print STDERR "SafePipe : Command : $Cmd\n";
1908
#print STDERR "SafePipe : TimeOut : $TimeOut\n";
1910
$? = 0; # Make sure there's no junk left in here
1913
die "Can't fork: $!" unless defined($pid = open($Kid, '-|'));
1917
# Set up a signal handler and set the alarm time to the timeout
1918
# value passed to the function
1920
local $SIG{ALRM} = sub { $TimedOut = 1; die "Command Timed Out" };
1923
# while the command is running we will collect it's output
1924
# in the $Str variable. We don't process it in any way here so
1925
# whatever called us will get back exactly what they would have
1926
# gotten with a system() or backtick call
1928
#MailScanner::Log::DebugLog("SafePipe : Processing %s", $Cmd);
1932
#print STDERR "SafePipe : Processing line \"$_\"\n";
1935
#MailScanner::Log::DebugLog("SafePipe : Completed $Cmd");
1936
#print STDERR "SafePipe : Returned $PipeReturnCode\n";
1940
# Workaround for bug in perl shipped with Solaris 9,
1941
# it doesn't unblock the SIGALRM after handling it.
1943
my $unblockset = POSIX::SigSet->new(SIGALRM);
1944
sigprocmask(SIG_UNBLOCK, $unblockset)
1945
or die "Could not unblock alarm: $!\n";
1951
# Execute the command via an exec call, bear in mind this will only
1952
# capture STDIN so if you need STDERR, or both you have to handle, for
1953
# example, 2>&1 as part of the command line just as you would with
1954
# system() or backticks
1956
#the line following the
1957
# call should *never* be reached unless the call it's self fails
1958
#print STDERR "SafePipe in child exec $Cmd\n";
1960
my @args = ( "$Cmd" );
1961
#exec $Cmd or print STDERR "SafePipe : failed to execute $Cmd\n";
1963
open STDIN, "< /dev/null";
1966
or MailScanner::Log::WarnLog("SafePipe : failed to execute %s", $Cmd);
1967
#MailScanner::Log::DebugLog("SafePipe in Message.pm : exec failed " .
1974
#MailScanner::Log::DebugLog("SafePipe in Message.pm : Completed $Cmd");
1975
#MailScanner::Log::WarnLog("Returned Code : %d", $?);
1976
# Catch failures other than the alarm
1977
MailScanner::Log::WarnLog("SafePipe in Message.pm : $Cmd failed with real error: $@")
1978
if $@ and $@ !~ /Command Timed Out/;
1980
#print STDERR "SafePipe : pid = $pid and \@ = $@\n";
1982
# In which case any failures must be the alarm
1984
# Kill the running child process
1987
# Wait for up to 5 seconds for it to die
1988
for ($i=0; $i<5; $i++) {
1990
waitpid($pid, &POSIX::WNOHANG);
1991
($pid=0),last unless kill(0, $pid);
1994
# And if it didn't respond to 11 nice kills, we kill -9 it
1997
waitpid $pid, 0; # 2.53
2001
# If the command timed out return the string below, otherwise
2002
# return the command output in $Str
2003
return $Str unless $TimedOut;
2005
MailScanner::Log::WarnLog("Safepipe in Message.pm : %s timed out!", $Cmd);
2006
return "COMMAND_TIMED_OUT";
1498
2010
# Unpack a zip file into the named directory.
1499
2011
# Return 1 if an error occurred, else 0.
1500
2012
# Return 0 on success.
3025
3637
my $virustag = MailScanner::Config::Value('virussubjecttext', $this);
3026
3638
#print STDERR "I am infected\n" if $this->{infected};
3027
3639
if ($this->{infected} &&
3028
MailScanner::Config::Value('virusprependsubject',$this) &&
3029
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $virustag)) {
3640
MailScanner::Config::Value('virusprependsubject',$this) &&
3641
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $virustag)) {
3030
3642
$global::MS->{mta}->PrependHeader($this, 'Subject:', $virustag, ' ');
3031
3643
$subjectchanged = 1;
3647
# Modify the subject line for Disarming
3648
my $disarmtag = MailScanner::Config::Value('disarmsubjecttext',$this);
3649
my $phishingtag = MailScanner::Config::Value('phishingsubjecttag', $this);
3650
if ($this->{messagedisarmed}) {
3651
#print STDERR "Found messagedisarmed = " . join(',',@{$this->{disarmedtags}}) . "\n";
3652
if(MailScanner::Config::Value('disarmprependsubject',$this) =~ /1/ &&
3653
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $disarmtag)) {
3654
$global::MS->{mta}->PrependHeader($this, 'Subject:', $disarmtag, ' ');
3655
$subjectchanged = 1;
3657
if (grep /phishing/i, @{$this->{disarmedtags}}) {
3658
#print STDERR "Found phishing disarmedtags2\n";
3659
# We found it had a phishing link in it. Are we tagging phishing Subject?
3660
if (MailScanner::Config::Value('tagphishingsubject',$this) =~ /1/ &&
3661
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $phishingtag)) {
3662
$global::MS->{mta}->PrependHeader($this, 'Subject:', $phishingtag, ' ');
3663
$subjectchanged = 1;
3035
3669
# Modify the subject line for spam
3036
3670
# if it's spam AND they want to modify the subject line AND it's not
3037
3671
# already been modified by another of your MailScanners.
3038
3672
my $spamtag = MailScanner::Config::Value('spamsubjecttext', $this);
3039
3673
$spamtag =~ s/_SCORE_/$scoretext/;
3674
$spamtag =~ s/_STARS_/$stars/i;
3040
3675
if ($this->{isspam} && !$this->{ishigh} &&
3041
MailScanner::Config::Value('spamprependsubject',$this) &&
3042
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $spamtag)) {
3676
MailScanner::Config::Value('spamprependsubject',$this) &&
3677
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $spamtag)) {
3043
3678
$global::MS->{mta}->PrependHeader($this, 'Subject:', $spamtag, ' ');
3044
3679
$subjectchanged = 1;
3046
3681
# If it is high-scoring spam, then add a different bit of text
3047
3682
$spamtag = MailScanner::Config::Value('highspamsubjecttext', $this);
3048
3683
$spamtag =~ s/_SCORE_/$scoretext/;
3684
$spamtag =~ s/_STARS_/$stars/i;
3049
3685
if ($this->{isspam} && $this->{ishigh} &&
3050
3686
MailScanner::Config::Value('highspamprependsubject',$this) &&
3051
3687
!$global::MS->{mta}->TextStartsHeader($this, 'Subject:', $spamtag)) {
3672
4384
# Disarm some of the HTML tags in this message.
3673
4385
my($DisarmFormTag, $DisarmScriptTag, $DisarmCodebaseTag, $DisarmIframeTag,
3674
$DisarmWebBug, $DisarmPhishing, $DisarmPhishingFound);
4386
$DisarmWebBug, $DisarmPhishing, $DisarmNumbers, $DisarmHTMLChangedMessage,
4387
$DisarmWebBugFound, $DisarmPhishingFound, $PhishingSubjectTag,
4388
$PhishingHighlight);
3675
4389
sub DisarmHTML {
3676
4390
my $this = shift;
3678
4392
#print STDERR "Tags to convert are " . $this->{tagstoconvert} . "\n";
3680
4394
# Set the disarm booleans for this message
4396
$DisarmScriptTag = 0;
4397
$DisarmCodebaseTag = 0;
4398
$DisarmCodebaseTag = 0;
4399
$DisarmIframeTag = 0;
4401
$DisarmPhishing = 0;
4403
$DisarmWebBugFound = 0;
4404
$PhishingSubjectTag= 0;
4405
$PhishingHighlight = 0;
3681
4406
$DisarmFormTag = 1 if $this->{tagstoconvert} =~ /form/i;
3682
4407
$DisarmScriptTag = 1 if $this->{tagstoconvert} =~ /script/i;
3683
4408
$DisarmCodebaseTag = 1 if $this->{tagstoconvert} =~ /codebase/i;
3684
4409
$DisarmCodebaseTag = 1 if $this->{tagstoconvert} =~ /data/i;
3685
4410
$DisarmIframeTag = 1 if $this->{tagstoconvert} =~ /iframe/i;
3686
4411
$DisarmWebBug = 1 if $this->{tagstoconvert} =~ /webbug/i;
4412
$PhishingSubjectTag= 1
4413
if MailScanner::Config::Value('tagphishingsubject', $this) =~ /1/;
4414
#print STDERR "PhishingSubjectTag = $PhishingSubjectTag\n";
4415
$PhishingHighlight = 1
4416
if MailScanner::Config::Value('phishinghighlight', this) =~ /1/;
4417
#print STDERR "PhishingHighlight = $PhishingHighlight\n";
4418
$DisarmPhishingFound = 0;
4419
$DisarmHTMLChangedMessage = 0;
3687
4420
if (MailScanner::Config::Value('findphishing', $this) =~ /1/) {
3688
4421
$DisarmPhishing = 1;
3689
$DisarmPhishingFound = 0;
4423
if MailScanner::Config::Value('phishingnumbers', $this) =~ /1/;
3692
$this->DisarmHTMLTree($this->{entity});
4426
my($counter, @disarmedtags);
4427
($counter, @disarmedtags) = $this->DisarmHTMLTree($this->{entity});
4428
#print STDERR "disarmedtags = ". join(', ', @disarmedtags) . "\n";
4430
# If the HTML checks found a real problem or there really was a phishing
4431
# attack, only then should we log anything.
4432
@disarmedtags = ('phishing') if $DisarmPhishingFound && !@disarmedtags;
4433
#print STDERR "Found DisarmPhishingFound\n" if $DisarmPhishingFound;
4434
MailScanner::Log::InfoLog('Content Checks: Detected and have disarmed ' .
4435
join(', ', @disarmedtags) . ' tags in ' .
4436
'HTML message in %s from %s',
4437
$this->{id}, $this->{from})
4438
if $DisarmHTMLChangedMessage || $DisarmPhishingFound;
3694
4440
# And save the results from the phishing trip
3695
4441
if ($DisarmPhishingFound) {
3696
4442
# Do we want this or not? I say no. $this->{otherinfected} = 1;
3697
$this->{bodymodified} = 1;
4443
$this->{bodymodified} = 1;
4445
if ($DisarmHTMLChangedMessage) {
4446
#print STDERR "Disarm Changed the message\n";
4447
$this->{bodymodified} = 1;
4448
$this->{messagedisarmed} = 1;
4450
$this->{messagedisarmed} = 0;
4452
# Store all the tags we disarmed
4453
#print STDERR "Storing " . join(',', @disarmedtags) . "\n";
4454
@{$this->{disarmedtags}} = @disarmedtags;
4090
4868
#print "It's an iframe\n";
4091
4869
$text = substr $text, 1;
4092
4870
$output .= "<MailScannerIFrame$$ " . $text;
4871
$DisarmDoneSomething{'iframe'} = 1;
4093
4872
} elsif ($tagname eq 'script' && $DisarmScriptTag) {
4094
4873
#print "It's a script\n";
4095
4874
$text = substr $text, 1;
4096
4875
$output .= "<MailScannerScript$$ " . $text;
4876
$DisarmDoneSomething{'script'} = 1;
4097
4877
} elsif ($tagname eq 'a' && $DisarmPhishing) {
4098
4878
#print STDERR "It's a link\n";
4099
4879
$output .= $text;
4100
$DisarmLinkText = '';
4880
$DisarmLinkText = ''; # Reset state of automaton
4881
$DisarmLinkURL = '';
4101
4882
$DisarmLinkURL = $attr->{'href'} if exists $attr->{'href'};
4102
4883
$DisarmInsideLink = 1;
4884
$DisarmInsideLink = 0 if $text =~ /\/\>$/; # JKF Catch /> empty A tags
4885
#print STDERR "DisarmInsideLink = $DisarmInsideLink\n";
4103
4886
} elsif ($tagname eq 'img' && $DisarmWebBug) {
4104
#print "It's an image\n";
4887
#print STDERR "It's an image\n";
4888
#print STDERR "The src is \"" . $attr->{'src'} . "\"\n";
4105
4889
if (exists $attr->{'width'} && $attr->{'width'}<=2 &&
4106
exists $attr->{'height'} && $attr->{'height'}<=2) {
4890
exists $attr->{'height'} && $attr->{'height'}<=2 &&
4891
exists $attr->{'src'} && $attr->{'src'} !~ /^cid:|^MailScannerWebBug/i) {
4107
4892
$output .= '<img src="MailScannerWebBug" width="' . $attr->{'width'} .
4108
4893
'" height="' . $attr->{'height'} . '" alt="';
4109
4894
$output .= 'Web Bug from ' . $attr->{'src'} if $attr->{'src'};
4110
4895
$output .= '" />';
4896
$DisarmWebBugFound = 1;
4897
$DisarmDoneSomething{'web bug'} = 1;
4112
4899
$output .= $text;
4901
} elsif ($tagname eq 'base') {
4902
#print STDERR "It's a Base URL\n";
4904
#print STDERR "Base URL = " . $attr->{'href'} . "\n";
4905
$DisarmBaseURL = $attr->{'href'} if exists $attr->{'href'};
4906
} elsif ($tagname eq 'area' && $DisarmInsideLink && $DisarmPhishing) {
4907
#print STDERR "It's an imagemap area\n";
4909
#print STDERR "Area URL = " . $attr->{'href'} . "\n";
4910
$DisarmAreaURL = $attr->{'href'};
4115
4912
#print STDERR "The tag was a \"$tagname\"\n";
4116
4913
$output .= $text;
4914
#print STDERR "output text is now \"$output\"\n";
4916
# tagname DisarmPhishing
4918
# a 1 0 0 tagname=a && Disarm=1
4921
#if ($DisarmInsideLink && !($tagname eq 'a' && $DisarmPhishing)) {
4118
4922
if ($DisarmInsideLink && ($tagname ne 'a' || !$DisarmPhishing)) {
4119
4923
$DisarmLinkText .= $output;
4924
#print STDERR "StartCallback: DisarmLinkText now equals \"$DisarmLinkText\"\n";
4927
#print STDERR "StartCallback: Printed2 \"$output\"\n";
4129
4935
if ($tagname eq 'iframe' && $DisarmIframeTag) {
4130
4936
print "</MailScannerIFrame$$>";
4937
$DisarmDoneSomething{'iframe'} = 1;
4131
4938
} elsif ($tagname eq 'form' && $DisarmFormTag) {
4132
4939
print "</MailScannerForm$$>";
4940
$DisarmDoneSomething{'form'} = 1;
4133
4941
} elsif ($tagname eq 'script' && $DisarmScriptTag) {
4134
4942
print "</MailScannerScript$$>";
4135
} elsif ($tagname eq 'a' && $DisarmPhishing) {
4136
my($squashedtext,$linkurl);
4943
$DisarmDoneSomething{'script'} = 1;
4944
} elsif ($tagname eq 'map' && $DisarmAreaURL) {
4945
# We are inside an imagemap that is part of a phishing imagemap
4946
$DisarmLinkText .= '</map>';
4947
} elsif ($tagname eq 'a' && $DisarmPhishing) { # && defined $attr->{'href'}) {
4948
#print STDERR "Endtag Callback found link, disarmlinktext = \"$DisarmLinkText\"\n";
4949
my($squashedtext,$linkurl,$alarm,$numbertrap);
4137
4950
$DisarmInsideLink = 0;
4138
4951
$squashedtext = lc($DisarmLinkText);
4952
if ($DisarmAreaURL) {
4953
$squashedtext = $DisarmLinkURL;
4954
$DisarmLinkURL = lc($DisarmAreaURL);
4955
$DisarmAreaURL = ""; # End of a link, so reset this
4957
$squashedtext = lc($DisarmLinkText);
4960
# Try to filter out mentions of Microsoft's .NET system
4961
$squashedtext = "" if $squashedtext eq ".net";
4962
$squashedtext = "" if $squashedtext =~ /(^|\b)(ado|asp)\.net($|\b)/;
4139
4964
$squashedtext =~ s/\%a0//g;
4140
4965
$squashedtext =~ s#%([0-9a-f][0-9a-f])#chr(hex('0x' . $1))#gei; # Unescape
4141
4966
$squashedtext =~ s/\s+//g; # Remove any whitespace
4142
$squashedtext =~ s/\\/\//g;
4967
$squashedtext =~ s/\\/\//g; # Change \ to / as many browsers do this
4968
$squashedtext =~ s/^\[\d*\]//; # Removing leading [numbers]
4143
4969
$squashedtext =~ s/(\<\/?[^>]*\>)*//ig; # Remove tags
4144
#$squashedtext =~ s/^(\<\/?(br|p|ul)\>)*//ig; # Remove leading br, p, ul tags
4970
$squashedtext =~ s/^[^\/:]+\@//; # Remove username of email addresses
4971
#$squashedtext =~ s/\&\w*\;//g; # Remove things like < and >
4972
$squashedtext =~ s/\<\;/\</g; # Remove things like < and >
4973
$squashedtext =~ s/\>\;/\>/g; # rEmove things like < and >
4974
$squashedtext =~ s/./CharToIntnl("$&")/ge;
4145
4975
#print STDERR "Text = \"$text\"\n";
4146
4976
#print STDERR "1SquashedText = \"$squashedtext\"\n";
4147
4977
#print STDERR "1LinkURL = \"$DisarmLinkURL\"\n";
4148
4978
# If it looks like a link, remove any leading https:// or ftp://
4149
if ($squashedtext =~ /^(w+|ft+p|fpt+|ma[il]+to)([.,]|\%2e)/i ||
4979
($linkurl,$alarm) = CleanLinkURL($DisarmLinkURL);
4980
#print STDERR "linkurl = $linkurl\nBefore If statement\n";
4981
#print STDERR "squashedtext = $squashedtext\nBefore If statement\n";
4983
# Has it fallen foul of the numeric-ip phishing net? Must treat x
4984
# like a digit so it catches 0x41 (= 'A')
4985
$numbertrap = ($DisarmNumbers && $linkurl !~ /[<>g-wyz]+/)?1:0;
4988
$squashedtext =~ /^(w+|ft+p|fpt+|ma[il]+to)([.,]|\%2e)/i ||
4150
4989
$squashedtext =~ /[.,](com|org|net|info|biz|ws)/i ||
4151
4990
$squashedtext =~ /[.,]com?[.,][a-z][a-z]/i ||
4152
$squashedtext =~ /^(ht+ps?|ft+p|fpt+|mailto)[:;](\/\/)?(.*(\.|\%2e))/i) {
4153
#$squashedtext =~ /^(ht+ps?|ft+p|fpt+|mailto)[:;]\/\/?(.*(\.|\%2e))/i) {
4991
$squashedtext =~ /^(ht+ps?|ft+p|fpt+|mailto)[:;](\/\/)?(.*(\.|\%2e))/i ||
4154
4993
$squashedtext =~ s/^(ht+ps?|ft+p|fpt+|mailto)[:;](\/\/)?(.*(\.|\%2e))/$3/i;
4155
#$squashedtext =~ s/^(ht+ps?|ft+p|fpt+|mailto)[:;]\/\/?(.*(\.|\%2e))/$2/i;
4156
4994
$squashedtext =~ s/\/.*$//; # Only compare the hostnames
4157
4995
$squashedtext =~ s/[,.]+$//; # Allow trailing dots and commas
4158
4996
$squashedtext = 'www.' . $squashedtext
4159
unless $squashedtext =~ /^ww+|ft+p|fpt+|mailto/;
4160
$linkurl = lc($DisarmLinkURL);
4161
$linkurl =~ s/\%a0//ig;
4162
$linkurl =~ s#%([0-9a-f][0-9a-f])#chr(hex('0x' . $1))#gei; # Unescape
4163
$linkurl =~ s/\s+//; # Remove any whitespace
4164
$linkurl = "" if $linkurl =~ /\@/ && $linkurl !~ /\//; # Ignore emails
4165
#$linkurl =~ s/(\<\/?[^>]*\>)*//ig; # Remove tags
4166
#$linkurl =~ s/^(\<\/?(br|p|ul)\>)*//ig; # Remove leading br, p, ul tags
4167
$linkurl =~ s/^blocked[:\/]+//i; # Remove "blocked::" labels
4168
$linkurl =~ s/^(https?|ftp)[:;]\/\///i;
4169
$linkurl = "" if $linkurl =~ /^ma[il]+to[:;]/i;
4170
$linkurl =~ s/[?\/].*$//; # Only compare up to the first '/' or '?'
4171
$linkurl =~ s/(\<\/?(br|p|ul)\>)*$//ig; # Remove trailing br, p, ul tags
4172
$linkurl = "" if $linkurl =~ /^file:/i; # Ignore file: URLs completely
4173
$linkurl =~ s/\/$//; # LinkURL is trimmed -- note
4997
unless $squashedtext =~ /^ww+|ft+p|fpt+|mailto/ || $numbertrap;
4174
4998
#print STDERR "2SquashedText = \"$squashedtext\"\n";
4999
# If we have already tagged this link as a phishing attack, spot the
5000
# warning text we inserted last time and don't tag it again.
5001
my $possiblefraudstart = MailScanner::Config::LanguageValue(0, 'possiblefraudstart');
5002
my $squashedpossible = lc($possiblefraudstart);
5003
my $squashedsearch = lc($DisarmLinkText);
5004
$squashedpossible =~ s/\s//g;
5005
$squashedpossible =~ s/(\<\/?[^>]*\>)*//ig; # Remove tags
5006
$squashedsearch =~ s/\s//g;
5007
$squashedsearch =~ s/(\<\/?[^>]*\>)*//ig; # Remove tags
5008
#$squashedpossible = "www.$squashedpossible\"$linkurl\"";
5009
$squashedpossible = quotemeta($squashedpossible);
5010
#print STDERR "NEW CODE: SquashedText = $squashedtext\n";
5011
#print STDERR "NEW CODE: DisarmLinkText = $DisarmLinkText\n";
5012
#print STDERR "NEW CODE: Text = $text\n";
5013
#print STDERR "NEW CODE: SquashedPossible = $squashedpossible\n";
5014
#print STDERR "NEW CODE: LinkURL = $linkurl\n";
5015
if ($squashedtext =~ /$squashedpossible/) {
5016
#print STDERR "FOUND IT\n";
5017
#print STDERR "$DisarmLinkText$text\n";
5018
print "$DisarmLinkText$text";
5019
$DisarmLinkText = ""; # Reset state of automaton
4175
5022
#print STDERR "2LinkURL = \"$linkurl\"\n";
4176
if ($linkurl ne "" && $squashedtext !~ /^(w+\.)?\Q$linkurl\E\/?$/) {
4177
print MailScanner::Config::LanguageValue(0, 'possiblefraudstart') .
4178
' "' . $linkurl . '" ' .
4179
MailScanner::Config::LanguageValue(0, 'possiblefraudend') . ' ';
4180
$DisarmPhishingFound = 1;
4181
$linkurl = substr $linkurl, 0, 80;
4182
$squashedtext = substr $squashedtext, 0, 80;
4183
MailScanner::Log::InfoLog('Found phishing fraud from %s ' .
4184
'claiming to be %s in %s',
4185
$linkurl, $squashedtext, $id);
5023
# If it is a phishing catch, or else it's not (numeric or IPv6 numeric)
5025
#print STDERR "LinkURL is \"$linkurl\"\n";
5026
#print STDERR "Squashe is \"$squashedtext\"\n";
5027
#print STDERR "Phishing by numbers is $DisarmNumbers\n";
5029
#if ($linkurl ne "" && $squashedtext !~ /^(w+\.)?\Q$linkurl\E\/?$/) {
5030
# || ($linkurl ne "" && $DisarmNumbers && $linkurl =~ /^\d+\.[^g-z]+/)){
5032
($linkurl ne "" && $squashedtext !~ /^(w+\.)?\Q$linkurl\E\/?$/)
5033
|| ($linkurl ne "" && $numbertrap)) {
5035
unless (InPhishingWhitelist($linkurl)) {
5036
use bytes; # Don't send UTF16 to syslog, it breaks!
5037
if ($linkurl ne "" && numbertrap && $linkurl eq $squashedtext) {
5038
# It's not a real phishing trap, just a use of numberic IP links
5039
print MailScanner::Config::LanguageValue(0, 'numericlinkwarning') .
5040
' ' if $PhishingHighlight;
5042
# It's a phishing attack.
5043
print $possiblefraudstart . ' "' . $linkurl . '" ' .
5044
MailScanner::Config::LanguageValue(0, 'possiblefraudend') . ' ' if $PhishingHighlight;
5046
$DisarmPhishingFound = 1;
5047
$linkurl = substr $linkurl, 0, 80;
5048
$squashedtext = substr $squashedtext, 0, 80;
5049
$DisarmDoneSomething{'phishing'} = 1 if $PhishingSubjectTag;
5051
MailScanner::Log::InfoLog('Found ip-based phishing fraud from ' .
5052
'%s in %s', $linkurl, $id);
5054
MailScanner::Log::InfoLog('Found phishing fraud from %s ' .
5055
'claiming to be %s in %s',
5056
$linkurl, $squashedtext, $id);
5059
#print STDERR "Fake\n";
5065
#print STDERR "End tag printed \"$DisarmLinkText$text\"\n";
4189
5066
print "$DisarmLinkText$text";
5067
$DisarmLinkText = ""; # Reset state of automaton
5068
#print STDERR "Reset disarmlinktext\n";
5070
#print STDERR "End tag printed \"$text\"\n";
5075
my %CharToInternational = (
5174
# Turn any character into an international version of it if it is in the range
5178
# Passed in an 8-bit character.
5179
#print STDERR "Char in is $p\n";
5180
($a) = unpack 'C', $p;
5182
#print STDERR "Char is $a, $p\n";
5184
# Bash char 160 (space) to nothing
5185
return '' if $a == 160;
5186
my $char = $CharToInternational{$a};
5187
return '&' . $char . ';' if $char ne "";
5191
# Clean up a link URL so it is suitable for phishing detection
5192
# Return (clean url, alarm trigger value). An alarm trigger value non-zero
5193
# means this is definitely likely to be a phishing trap, no matter what
5194
# anything else says.
5196
my($DisarmLinkURL) = @_;
5200
my($linkurl,$alarm);
5202
$linkurl = $DisarmLinkURL;
5203
$linkurl = lc($linkurl);
5204
#print STDERR "Cleaning up $linkurl\n";
5205
#$linkurl =~ s/\%a0//ig;
5206
#$linkurl =~ s/\%e9/é/ig;
5208
$linkurl =~ s#%([0-9a-f][0-9a-f])#chr(hex('0x' . $1))#gei; # Unescape
5209
#print STDERR "2Cleaning up $linkurl\n";
5211
$linkurl =~ s/./CharToIntnl("$&")/ge;
5213
#print STDERR "Was $linkurl\n";
5214
$linkurl = "" unless $linkurl =~ /[.\/]/; # Ignore if it is not a website at all
5215
$linkurl =~ s/\s+//g; # Remove any whitespace
5216
$linkurl =~ s/\\/\//g; # Change \ to / as many browsers do this
5217
#print STDERR "Is $linkurl\n";
5218
$linkurl = "" if $linkurl =~ /\@/ && $linkurl !~ /\//; # Ignore emails
5219
$linkurl =~ s/^\[\d*\]//; # Remove leading [numbers]
5220
#$linkurl =~ s/(\<\/?[^>]*\>)*//ig; # Remove tags
5221
#$linkurl =~ s/^(\<\/?(br|p|ul)\>)*//ig; # Remove leading br, p, ul tags
5222
$linkurl =~ s/^blocked[:\/]+//i; # Remove "blocked::" labels
5223
$linkurl =~ s/^outbind:\/\/\d+\///i; # Remove "outbind://22/" type labels
5224
$linkurl = $DisarmBaseURL . '/' . $linkurl
5225
if $linkurl ne "" && $DisarmBaseURL ne "" &&
5226
$linkurl !~ /^(https?|ftp|mailto):/i;
5227
$linkurl =~ s/^(https?|ftp)[:;]\/\///i;
5228
$linkurl = "" if $linkurl =~ /^ma[il]+to[:;]/i;
5229
$linkurl =~ s/[?\/].*$//; # Only compare up to the first '/' or '?'
5230
$linkurl =~ s/(\<\/?(br|p|ul)\>)*$//ig; # Remove trailing br, p, ul tags
5231
$linkurl = "" if $linkurl =~ /^file:/i; # Ignore file: URLs completely
5232
$linkurl = "" if $linkurl =~ /^#/; # Ignore internal links completely
5233
$linkurl =~ s/\/$//; # LinkURL is trimmed -- note
5234
$alarm = 1 if $linkurl =~ s/[\x00-\x1f[:^ascii:]]/_BAD_/g; # /\&\#/;
5235
#$alarm = 1 if $linkurl =~ s/[\x00-\x1f\x80-\x{ffff}]/_BAD_/g; # /\&\#/;
5236
#print STDERR "Produced $linkurl and $alarm\n";
5237
$linkurl = 'JavaScript' if $linkurl =~ /^javascript:/i;
5241
# Return 1 if the hostname in $linkurl is in the safe sites file.
5242
# Return 0 otherwise.
5243
sub InPhishingWhitelist {
5247
return 1 if $MailScanner::Config::PhishingWhitelist{$linkurl};
5249
# Trim host. off the front of the hostname
5250
while ($linkurl ne "" && $linkurl =~ s/^[^.]+\.//) {
5251
# And replace it with *. then look it up
5252
#print STDERR "Looking up *.$linkurl\n";
5253
return 1 if $MailScanner::Config::PhishingWhitelist{'*.' . $linkurl};
4197
5260
# Convert 1 MIME entity from html to text using HTML::Parser.