Подтвердить что ты не робот

Perl CGI взломан? Но я делаю все правильно

Я только что заметил некоторые странные PHP файлы в одном из моих веб-каталогов. Они оказались файлами эксплойтов, помещенных в спамер.

Они были там с 2006 года, когда я проводил широкомасштабную кампанию пожертвований, используя CGI script. И файлы были помещены в каталог script, поэтому я подозреваю, что мой script мог быть каким-то образом использован.

Но я использую Perl-проверку taint, strict и т.д., и я никогда не передаю данные запроса в оболочку (она никогда не вызывает оболочку!) или использует данные запроса для создания пути к файлу для OPEN... Я только ОТКРЫТЬ файлы, которые я указываю непосредственно в script. Я передаю данные запроса в письменные файлы как содержимое файла, но, насколько мне известно, это не опасно.

Я уставился на эти сценарии и ничего не вижу, и я изучил все стандартные дыры Perl CGI. Конечно, они могли бы получить пароль для моей учетной записи хостинга, но тот факт, что эти скрипты были помещены в мой каталог данных CGI script, заставляет меня подозревать script. (Кроме того, они получают мой пароль "как-то" - это гораздо более страшное объяснение.) Кроме того, в то время в моих журналах было показано много сообщений "Warning, IPN, полученных из сообщений не для PayPal", с теми IP-адресами, которые поступают из России. Поэтому кажется, что кто-то был, по крайней мере, ПЫТАЕТСЯ взломать эти сценарии.

Два сценария задействованы, и я вставляю их ниже. Кто-нибудь видит что-нибудь, что может быть использовано для записи неожиданных файлов?

Здесь первый script (для получения IPN PayPal и отслеживания пожертвований, а также отслеживания, на котором сайт генерирует большинство пожертвований):

#!/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 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 );
}

И там какая-то избыточность (извините за это... полноту?), но здесь второй script (для создания 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 );
}
4b9b3361

Ответ 1

Я видел нечто подобное раньше. В нашем случае я уверен, что хакеры использовали переполнение буфера в библиотеке, которая не обновлялась. Затем они могли использовать оболочку PHP для записи файлов на сервер.

Скорее всего, проблема не в вашем коде. Обновление вашего программного обеспечения чаще всего делает атаки менее вероятными, но, к сожалению, невозможно быть полностью взломанным. Скорее всего, они сканировали общую уязвимость в старой версии программного обеспечения.

Ответ 2

Прошло некоторое время с тех пор, как я играл с модулем CGI perl, но уверены ли вы, что CGI:: param избегает значений? Из того места, где я сижу, значения могут содержать обратные ссылки и, следовательно, будут расширены и выполнены.

Ответ 3

Вы можете реорганизовать свой код, чтобы сделать все ссылки на файловую карту во временные константы компиляции с помощью 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 изменить любые аргументы, которые передаются как пути к файлам.

Ответ 4

Ваш код кажется мне абсолютно безопасным. Я бы лишь немного возражал против использования относительных путей для файлов, что делает меня немного неудобным, но трудно представить себе угрозу безопасности в этом. Я бы поставил, что уязвимость была где-то ниже (perl, apache...)

Ответ 5

Если это не огромная проблема, вы могли бы просто оставить ее в покое. Если вы не можете этого сделать, восстановите резервную копию. Видя, как он был взломан так долго, вы, вероятно, тоже не можете этого сделать. Третий и, скорее всего, ответ будет заключаться в том, чтобы подкрепить то, что вы можете, и просто уничтожить его и начать все заново. Повторяя все с нуля.