21

Web ディレクトリの 1 つに奇妙な PHP ファイルがあることに気付きました。これらは、スパマーが配置したエクスプロイト ファイルであることが判明しました。

彼らは 2006 年以来、私が私の CGI スクリプトを使用して有名な寄付キャンペーンを行っていた頃からそこにいます。そして、ファイルはスクリプトの書き込み可能なディレクトリに置かれたので、私のスクリプトがなんらかの形で悪用されたのではないかと疑っています。

しかし、私はPerlの「汚染チェック」、厳密などを使用しており、クエリデータをシェルに渡すことは決してありません(シェルを呼び出すことはありません!)またはクエリデータを使用してOPENのファイルパスを生成します...スクリプトで直接指定した OPEN ファイル。クエリ データを書き込みファイルにファイル コンテンツとして渡しますが、私が知る限り、それは危険ではありません。

私はこれらのスクリプトをじっと見つめましたが、何も見えず、すべての標準的な Perl CGI の穴を調べました。もちろん、彼らはどうにかして私のホスティング アカウントのパスワードを入手できた可能性がありますが、これらのスクリプトが私の CGI スクリプトのデータ ディレクトリに配置されているという事実から、私はそのスクリプトを疑います。(また、彼らが私のパスワードを「何らかの方法で」取得するというのは、もっと恐ろしい説明です。) また、その頃、私のログには、「警告、PayPal 以外のアドレスから IPN を受信しました」というメッセージが多数表示され、それらの IP はロシアからのものです。したがって、少なくとも誰かがこれらのスクリプトをハッキングしようとしていたようです。

2 つのスクリプトが関係しているので、下に貼り付けます。予期しないファイルを書き込むために悪用される可能性のあるものを見た人はいますか?

最初のスクリプトは次のとおりです (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 );
}

そして、ここにはいくらかの冗長性があります (申し訳ありませんが...完全ですか?) が、ここに 2 番目のスクリプトがあります (人々が自分のサイトに追加できる Web サイトの 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 );
}
4

4 に答える 4

2

以前似たようなものを見たことがあります。私たちの場合、更新されていないライブラリでハッカーがバッファ オーバーフローを使用したことは間違いありません。その後、PHP シェルを使用してサーバーにファイルを書き込むことができました。

問題がコードにない可能性が非常に高いです。ソフトウェアをより頻繁に更新すると、攻撃の可能性は低くなりますが、残念ながら、ハッキングを完全に防止することは不可能です. 古いバージョンのソフトウェアに共通する脆弱性をスキャンしていた可能性があります。

于 2011-06-08T18:40:40.993 に答える
0

コードをリファクタリングして、すべてのファイルパス参照をconstantプラグマを使用してコンパイル時定数にすることができます。

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がファイルパスとして渡される引数を変更するのは非常に困難になるはずです。

于 2011-05-31T15:02:19.780 に答える
0

perl の CGI モジュールで遊んでからしばらく経ちましたが、CGI::param が値をエスケープしていると確信していますか? 私が座っている場所から、値にはバックティックが含まれている可能性があるため、展開されて実行されます。

于 2011-05-08T16:42:30.767 に答える
0

あなたのコードは私にとって非常に安全に思えます。ファイルに相対パスを使用することに少し反対するだけで、少し不快になりますが、その中にセキュリティ上のリスクがあるとは想像しがたいです。脆弱性は以下のどこかにあるに違いない(perl、apache ...)

于 2011-06-08T19:50:16.377 に答える