我刚刚注意到我的一个网页目录中有一些奇怪的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 );
- }