我刚刚在我的一个网站目录中发现了一些奇怪的PHP文件。他们原来是垃圾邮件发送者利用文件。
他们自2006年以来一直在那里,大概是我使用我的CGI剧本进行高调的捐赠活动。并且文件放在脚本的可写目录中,因此我怀疑我的脚本可能已被某种方式利用。
但我正在使用Perl“污点检查”,严格等等,而且我永远不会将查询数据传递给shell(它永远不会调用shell!)或使用查询数据为OPEN生成文件路径。我只打开我在脚本中直接指定的文件。我确实将查询数据INTO写入文件作为文件内容传递,但据我所知,这并不危险。
我盯着这些脚本看不到任何东西,我研究了所有标准的Perl CGI漏洞。当然,他们可能以某种方式获得了我的托管帐户的密码,但事实上这些脚本放在我的CGI脚本的数据目录中让我怀疑脚本。 (此外,他们“以某种方式”获取我的密码是一个更可怕的解释。)此外,在那段时间,我的日志显示许多“警告,从非PayPal地址收到的IPN”消息,这些IP来自俄罗斯。所以看起来有人至少试图破解这些脚本。
涉及两个脚本,我在下面粘贴它们。有人看到任何可能被利用来编写意外文件的东西吗?
这是第一个脚本(用于接收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", $newSum );
writeFile( $overallSumFile, $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, my $siteSum ) = split( /\s+/, $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', -expires=>'now',
-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, "$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 );
}
答案 0 :(得分:2)
问题很可能不在您的代码中。更频繁地更新软件会降低攻击的可能性,但不幸的是,不可能完全防止攻击。他们可能正在扫描旧版软件中的常见漏洞。
答案 1 :(得分:0)
我玩过perl的CGI模块已经有一段时间了,但你确定CGI :: param逃脱了这些值吗?从我所在的位置开始,这些值可能包含反引号,因此会被扩展并执行。
答案 2 :(得分:0)
您可以重构代码,使用constant
pragma将所有文件路径引用转换为编译时常量:
use constant {
DIR_PRIVATE_DATA => "/paths/of/glory",
FILE_DONATION_LOG => "donationLog.txt"
};
open( FILE, ">>".DIR_PRIVATE_DATA."/".FILE_DONATION_LOG );
处理常量是一种痛苦,因为它们不会被qq
内插,并且你必须不断地使用(s)printf
或大量连接运营商。 但是它应该让ne'erdowell更难以改变作为文件路径传递的任何参数。
答案 3 :(得分:0)
您的代码对我来说似乎很安全。我只会稍微反对使用文件的相对路径,这让我有点不舒服,但很难想象有一些安全风险。我敢打赌,这个漏洞位于下方(perl,apache ......)
答案 4 :(得分:0)
如果这不是一个大问题,你可以 PROBABLY 不管它。如果您不能这样做,请从备份恢复。看到它被如此长时间的黑客攻击,你可能也做不到。第三个也是最有可能的答案就是备份你能做到的,然后重新开始并重新开始。从头开始重做一切。