我刚刚注意到我的一个网页目录中有一些奇怪的PHP文件。他们原来是垃圾邮件发送者的漏洞利用文件。
自2006年以来,他们一直在那里,在我使用我的CGI脚本进行高调的捐赠活动的时候。而这些文件被放置在脚本的可写目录中,所以我怀疑我的脚本可能被以某种方式被利用了。
但是我使用Perl“污点检查”,严格等等,我从来没有将查询数据传递给shell(它从不调用shell!)或使用查询数据生成OPEN的文件路径…我只OPEN文件,我直接在脚本中指定。我将查询数据INTO写入文件作为文件内容,但据我所知,这并不危险。
我盯着这些脚本,看不到任何东西,我研究了所有的Perl CGI标准孔。当然,他们可能会以某种方式获得我的主机帐户的密码,但是这些脚本被放置在我的CGI脚本的数据目录中的事实使我怀疑脚本。 (另外,他们得到我的密码“不知何故”是一个非常可怕的解释。)另外,在那段时间,我的日志显示了很多“警告,从非PayPal地址收到的IPN”消息,这些IP来自俄罗斯。所以似乎有人至少TRYING来破解这些脚本。
涉及两个脚本,我将它们粘贴到下面。任何人都可以看到任何可能被利用来写入意外的文件?
这是第一个脚本(用于接收PayPal IPN并跟踪捐款,并跟踪哪个站点产生最多捐款):
#!/usr/bin/perl -wT # Created by Jason Rohrer,December 2005 # Copied basic structure and PayPal protocol code from DonationTracker v0.1 # Script settings # Basic settings # email address this script is tracking payments for my $receiverEmail = "receiver\@yahoo.com"; # This script must have write permissions to BOTH of its DataDirectories. # It must be able to create files in these directories. # On most web servers,this means the directory must be world-writable. # ( chmod a+w donationData ) # These paths are relative to the location of the script. my $pubDataDirectory = "../goliath"; my $privDataDirectory = "../../cgi-data/donationNet"; # If this $privDataDirectory setting is changed,you must also change it below # where the error LOG is opened # end of Basic settings # Advanced settings # Ignore these unless you know what you are doing. # where the log of incoming donations is stored my $donationLogFile = "$privDataDirectory/donationLog.txt"; # location of public data generated by this script my $overallSumFile = "$pubDataDirectory/overallSum.html"; my $overallCountFile = "$pubDataDirectory/donationCount.html"; my $topSiteListFile = "$pubDataDirectory/topSiteList.html"; # private data tracking which donation total coming from each site my $siteTrackingFile = "$privDataDirectory/siteTracking.txt"; # Where non-fatal errors and other information is logged my $logFile = "$privDataDirectory/log.txt"; # IP of notify.paypal.com # used as cheap security to make sure IPN is only coming from PayPal my $paypalNotifyIP = "216.113.188.202"; # setup a local error log use CGI::Carp qw( carpout ); BEGIN { # location of the error log my $errorLogLocation = "../../cgi-data/donationNet/errors.log"; use CGI::Carp qw( carpout ); open( LOG,">>$errorLogLocation" ) or die( "Unable to open $errorLogLocation: $!\n" ); carpout( LOG ); } # end of Advanced settings # end of script settings use strict; use CGI; # Object-Oriented CGI library # setup stuff,make sure our needed files are initialized if( not doesFileExist( $overallSumFile ) ) { writeFile( $overallSumFile,"0" ); } if( not doesFileExist( $overallCountFile ) ) { writeFile( $overallCountFile,"0" ); } if( not doesFileExist( $topSiteListFile ) ) { writeFile( $topSiteListFile,"" ); } if( not doesFileExist( $siteTrackingFile ) ) { writeFile( $siteTrackingFile,"" ); } # allow group to write to our data files umask( oct( "02" ) ); # create object to extract the CGI query elements my $cgiQuery = CGI->new(); # always at least send an HTTP OK header print $cgiQuery->header( -type=>'text/html',-expires=>'now',-Cache_control=>'no-cache' ); my $remoteAddress = $cgiQuery->remote_host(); my $action = $cgiQuery->param( "action" ) || ''; # first,check if our count/sum is being queried by another script if( $action eq "checkResults" ) { my $sum = readTrimmedFileValue( $overallSumFile ); my $count = readTrimmedFileValue( $overallCountFile ); print "$count \$$sum"; } elsif( $remoteAddress eq $paypalNotifyIP ) { my $donorName; # $customField contains URL of site that received donation my $customField = $cgiQuery->param( "custom" ) || ''; # untaint and find whitespace-free string (assume it's a URL) ( my $siteURL ) = ( $customField =~ /(\S+)/ ); my $amount = $cgiQuery->param( "mc_gross" ) || ''; my $currency = $cgiQuery->param( "mc_currency" ) || ''; my $fee = $cgiQuery->param( "mc_fee" ) || '0'; my $date = $cgiQuery->param( "payment_date" ) || ''; my $transactionID = $cgiQuery->param( "txn_id" ) || ''; # these are for our private log only,for tech support,etc. # this information should not be stored in a web-accessible # directory my $payerFirstName = $cgiQuery->param( "first_name" ) || ''; my $payerLastName = $cgiQuery->param( "last_name" ) || ''; my $payerEmail = $cgiQuery->param( "payer_email" ) || ''; # only track US Dollars # (can't add apples to oranges to get a final sum) if( $currency eq "USD" ) { my $status = $cgiQuery->param( "payment_status" ) || ''; my $completed = $status eq "Completed"; my $pending = $status eq "Pending"; my $refunded = $status eq "Refunded"; if( $completed or $pending or $refunded ) { # write all relevant payment info into our private log addToFile( $donationLogFile,"$transactionID $date\n" . "From: $payerFirstName $payerLastName " . "($payerEmail)\n" . "Amount: \$$amount\n" . "Fee: \$$fee\n" . "Status: $status\n\n" ); my $netDonation; if( $refunded ) { # subtract from total sum my $oldSum = readTrimmedFileValue( $overallSumFile ); # both the refund amount and the # fee on the refund are now reported as negative # this changed as of February 13,2004 $netDonation = $amount - $fee; my $newSum = $oldSum + $netDonation; # format to show 2 decimal places my $newSumString = sprintf( "%.2f",$newSum ); writeFile( $overallSumFile,$newSumString ); my $oldCount = readTrimmedFileValue( $overallCountFile ); my $newCount = $oldCount - 1; writeFile( $overallCountFile,$newCount ); } # This check no longer needed as of February 13,2004 # since now only one IPN is sent for a refund. # # ignore negative completed transactions,since # they are reported for each refund (in addition to # the payment with Status: Refunded) if( $completed and $amount > 0 ) { # fee has not been subtracted yet # (fee is not reported for Pending transactions) my $oldSum = readTrimmedFileValue( $overallSumFile ); $netDonation = $amount - $fee; my $newSum = $oldSum + $netDonation; # format to show 2 decimal places my $newSumString = sprintf( "%.2f",$newSumString ); my $oldCount = readTrimmedFileValue( $overallCountFile ); my $newCount = $oldCount + 1; writeFile( $overallCountFile,$newCount ); } if( $siteURL =~ /http:\/\/\S+/ ) { # a valid URL # track the total donations of this site my $siteTrackingText = readFileValue( $siteTrackingFile ); my @siteDataList = split( /\n/,$siteTrackingText ); my $newSiteData = ""; my $exists = 0; foreach my $siteData ( @siteDataList ) { ( my $url,my $siteSum ) = split( /\s+/,$siteData ); if( $url eq $siteURL ) { $exists = 1; $siteSum += $netDonation; } $newSiteData = $newSiteData . "$url $siteSum\n"; } if( not $exists ) { $newSiteData = $newSiteData . "$siteURL $netDonation"; } trimWhitespace( $newSiteData ); writeFile( $siteTrackingFile,$newSiteData ); # now generate the top site list # our comparison routine,descending order sub highestTotal { ( my $url_a,my $total_a ) = split( /\s+/,$a ); ( my $url_b,my $total_b ) = split( /\s+/,$b ); return $total_b <=> $total_a; } my @newSiteDataList = split( /\n/,$newSiteData ); my @sortedList = sort highestTotal @newSiteDataList; my $listHTML = "<TABLE BORDER=0>\n"; foreach my $siteData ( @sortedList ) { ( my $url,$siteData ); # format to show 2 decimal places my $siteSumString = sprintf( "%.2f",$siteSum ); $listHTML = $listHTML . "<TR><TD><A HREF=\"$url\">$url</A></TD>". "<TD ALIGN=RIGHT>\$$siteSumString</TD></TR>\n"; } $listHTML = $listHTML . "</TABLE>"; writeFile( $topSiteListFile,$listHTML ); } } else { addToFile( $logFile,"Payment status unexpected\n" ); addToFile( $logFile,"status = $status\n" ); } } else { addToFile( $logFile,"Currency not USD\n" ); addToFile( $logFile,"currency = $currency\n" ); } } else { # else not from paypal,so it might be a user accessing the script # URL directly for some reason my $customField = $cgiQuery->param( "custom" ) || ''; my $date = $cgiQuery->param( "payment_date" ) || ''; my $transactionID = $cgiQuery->param( "txn_id" ) || ''; my $amount = $cgiQuery->param( "mc_gross" ) || ''; my $payerFirstName = $cgiQuery->param( "first_name" ) || ''; my $payerLastName = $cgiQuery->param( "last_name" ) || ''; my $payerEmail = $cgiQuery->param( "payer_email" ) || ''; my $fee = $cgiQuery->param( "mc_fee" ) || '0'; my $status = $cgiQuery->param( "payment_status" ) || ''; # log it addToFile( $donationLogFile,"WARNING: got IPN from unexpected IP address\n" . "IP address: $remoteAddress\n" . "$transactionID $date\n" . "From: $payerFirstName $payerLastName " . "($payerEmail)\n" . "Amount: \$$amount\n" . "Fee: \$$fee\n" . "Status: $status\n\n" ); # print an error page print "Request blocked."; } ## # Reads file as a string. # # @param0 the name of the file. # # @return the file contents as a string. # # Example: # my $value = readFileValue( "myFile.txt" ); ## sub readFileValue { my $fileName = $_[0]; open( FILE,"$fileName" ) or die( "Failed to open file $fileName: $!\n" ); flock( FILE,1 ) or die( "Failed to lock file $fileName: $!\n" ); my @lineList = <FILE>; my $value = join( "",@lineList ); close FILE; return $value; } ## # Reads file as a string,trimming leading and trailing whitespace off. # # @param0 the name of the file. # # @return the trimmed file contents as a string. # # Example: # my $value = readFileValue( "myFile.txt" ); ## sub readTrimmedFileValue { my $returnString = readFileValue( $_[0] ); trimWhitespace( $returnString ); return $returnString; } ## # Writes a string to a file. # # @param0 the name of the file. # @param1 the string to print. # # Example: # writeFile( "myFile.txt","the new contents of this file" ); ## sub writeFile { my $fileName = $_[0]; my $stringToPrint = $_[1]; open( FILE,">$fileName" ) or die( "Failed to open file $fileName: $!\n" ); flock( FILE,2 ) or die( "Failed to lock file $fileName: $!\n" ); print FILE $stringToPrint; close FILE; } ## # Checks if a file exists in the filesystem. # # @param0 the name of the file. # # @return 1 if it exists,and 0 otherwise. # # Example: # $exists = doesFileExist( "myFile.txt" ); ## sub doesFileExist { my $fileName = $_[0]; if( -e $fileName ) { return 1; } else { return 0; } } ## # Trims any whitespace from the beginning and end of a string. # # @param0 the string to trim. ## sub trimWhitespace { # trim from front of string $_[0] =~ s/^\s+//; # trim from end of string $_[0] =~ s/\s+$//; } ## # Appends a string to a file. # # @param0 the name of the file. # @param1 the string to append. # # Example: # addToFile( "myFile.txt","the new contents of this file" ); ## sub addToFile { my $fileName = $_[0]; my $stringToPrint = $_[1]; open( FILE,">>$fileName" ) or die( "Failed to open file $fileName: $!\n" ); flock( FILE,2 ) or die( "Failed to lock file $fileName: $!\n" ); print FILE $stringToPrint; close FILE; } ## # Makes a directory file. # # @param0 the name of the directory. # @param1 the octal permission mask. # # Example: # makeDirectory( "myDir",oct( "0777" ) ); ## sub makeDirectory { my $fileName = $_[0]; my $permissionMask = $_[1]; mkdir( $fileName,$permissionMask ); }
而且,这里有一些冗余(对于…完整性呢?),但这是第二个脚本(用于生成网站HTML按钮,人们可以添加到自己的网站):
#!/usr/bin/perl -wT # Created by Jason Rohrer,December 2005 # Script settings # Basic settings my $templateFile = "buttonTemplate.html"; # end of Basic settings # Advanced settings # Ignore these unless you know what you are doing. # setup a local error log use CGI::Carp qw( carpout ); BEGIN { # location of the error log my $errorLogLocation = "../../cgi-data/donationNet/errors.log"; use CGI::Carp qw( carpout ); open( LOG,">>$errorLogLocation" ) or die( "Unable to open $errorLogLocation: $!\n" ); carpout( LOG ); } # end of Advanced settings # end of script settings use strict; use CGI; # Object-Oriented CGI library # create object to extract the CGI query elements my $cgiQuery = CGI->new(); # always at least send an HTTP OK header print $cgiQuery->header( -type=>'text/html',-Cache_control=>'no-cache' ); my $siteURL = $cgiQuery->param( "site_url" ) || ''; print "Paste this HTML into your website:<BR>\n"; print "<FORM><TEXTAREA COLS=40 ROWS=10>\n"; my $buttonTemplate = readFileValue( $templateFile ); $buttonTemplate =~ s/SITE_URL/$siteURL/g; # escape all tags $buttonTemplate =~ s/&/&/g; $buttonTemplate =~ s/</</g; $buttonTemplate =~ s/>/>/g; print $buttonTemplate; print "\n</TEXTAREA></FORM>"; ## # Reads file as a string. # # @param0 the name of the file. # # @return the file contents as a string. # # Example: # my $value = readFileValue( "myFile.txt" ); ## sub readFileValue { my $fileName = $_[0]; open( FILE,$permissionMask ); }