2011-05-08 74 views
21

我刚刚注意到我的一个Web目录中有一些奇怪的PHP文件。他们原来是垃圾邮件发送者放置的漏洞文件。Perl CGI被黑了吗?但我正在做的一切正确

自2006年以来,他们一直在那里,大约在我使用CGI脚本进行高调捐赠活动的时候。这些文件被放置在脚本的可写目录中,所以我怀疑我的脚本可能已被以某种方式利用。但我使用Perl“异常检查”,严格等等,而且我从来没有将查询数据传递给shell(它从不调用shell!)或使用查询数据为OPEN生成文件路径。 ..我只打开文件,我直接在脚本中指定。我将查询数据作为文件内容传递给书面文件,但据我所知,这并不危险。

我盯着这些脚本,看不到任何东西,我研究了所有标准的Perl CGI漏洞。当然,他们可能已经以某种方式获得了我的托管帐户的密码,但是这些脚本放在我的CGI脚本的数据目录中的事实让我怀疑这个脚本。 (另外,他们以某种方式获得我的密码是一个非常可怕的解释)。另外,在那段时间,我的日志显示了很多“警告,IPN从非PayPal地址收到”消息,这些IP来自俄罗斯。所以看起来好像有人至少在试图破解这些脚本。

涉及到两个脚本,我在下面粘贴它们。任何人都可以看到任何可被利用来写入意外文件的东西?

这里的第一个脚本(用于接收贝宝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/&/&amp;/g; 
$buttonTemplate =~ s/</&lt;/g; 
$buttonTemplate =~ s/>/&gt;/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); 
} 
+1

Jason,这是该机器上唯一的动态内容? – 2011-05-08 16:25:53

+0

如果您的上传目录中的文件可以被所有人执行,并且外部世界可以访问它们,您就会遇到问题。但是你没有提到,它们是可执行的吗?还是他们只是文件,这可能表明尝试失败?当然,网络服务器不应该在可执行内容的上传目录中查找。另外,还有谁能够访问该机器?它可能是里面的人吗? – DavidO 2011-05-08 16:28:19

+8

如果机器没有保持最新状态,那么5年是一个很大的时间,因为远程漏洞在Web服务器,邮件服务器,内核或其他运行在该机器上网络访问。 – Quentin 2011-05-08 16:28:24

回答

0

已经有一段时间,因为我用Perl的CGI模块发挥,但你肯定CGI :: param转义值?从我所在的位置,这些值可能包含反引号,因此会被扩展和执行。

+2

我正在看那无限制的'开放FIL E,$ filename' ..如果你可以以某种方式操纵'$ filename',你可以做你想做的事情。 'open FILE“| echo#!/ usr/bin/perl> hack.cgi”' – TLP 2011-05-08 17:41:21

+0

嗯......我不认为在用户提交的变量内反引号会做任何事情......当使用变量时(这已经是字符串了),什么都没有扩展。是吗?我知道的每个漏洞都涉及将用户提交的变量传递给OPEN调用或在OWN反引号内使用用户提交的变量。 TLP,你认为OPEN调用可能存在问题,但如果你看看这些子程序被调用的位置,你会发现$ filename从来不涉及用户提交的变量。我只打开由我硬编码的4或5个静态文件名。 – 2011-05-08 19:35:53

+3

与'open'相关的安全预防措施是使用三个参数版本,假设您的Perl版本支持它。我认为它出现在5.6.1中,已经有十多年的历史了。例如:打开FILE,'>',$ filename或者死掉$ !;这会将打开模式(>)置于与文件名不同的参数中,从而防止shell注入。有关详细信息,请参阅perldoc -f open。 – DavidO 2011-05-09 08:58:31

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'erdowells更难以改变任何正在作为文件路径传递的参数。

+1

由于这些缺点[应避免使用'constant'模块](http://p3rl.org/Perl::Critic::Policy::ValuesAndExpressions::ProhibitConstantPragma)。建议[Const :: Fast](http://p3rl.org/Const::Fast)代替。 – daxim 2011-06-01 12:28:01

2

我以前见过类似的东西。在我们的例子中,我很确定黑客在未更新的库中使用了缓冲区溢出。然后他们可以使用PHP shell将文件写入服务器。

这很可能是您的代码中没有的问题。更频繁地更新软件会使得攻击的可能性降低,但不幸的是,它不可能完全防黑。很可能他们正在扫描旧版软件中的常见漏洞。

0

你的代码对我来说似乎很安全。我只是稍微反对使用文件的相对路径,这让我有点不舒服,但很难想象其中存在一些安全风险。我敢打赌,该漏洞是下面的某个地方(Perl中,阿帕奇...)

0

如果它不是一个巨大的问题,你可以PROBABLY只是息事宁人。如果您无法做到这一点,请从备份中恢复。看到它被如此长时间的黑客攻击,你可能无法做到这一点。第三个也是最有可能的答案是备份你所能做的,然后把它烧掉并重新开始。从头开始重做所有事情。

相关问题