perl cgi exploit

Perl CGI hackeado? Pero estoy haciendo todo bien



exploit (5)

Acabo de notar algunos archivos PHP extraños en uno de mis directorios web. Resultaron ser archivos de exploits colocados por spammers.

Han estado allí desde 2006, aproximadamente cuando estaba ejecutando una campaña de donaciones de alto perfil utilizando un script CGI mío. Y los archivos se colocaron en el directorio de escritura del script, por lo que sospecho que mi script podría haber sido explotado de alguna manera.

Pero estoy usando la "comprobación de corrupción" de Perl, estricta, etc., y nunca paso datos de consulta al shell (¡nunca invoca al shell!) O uso datos de consulta para generar una ruta de archivo para ABRIR ... solo ABRIR archivos que yo especifique directamente en el script. Paso los datos de consulta INTO a los archivos escritos como contenido de archivo, pero hasta donde sé, eso no es peligroso.

He observado estos scripts y no puedo ver nada, y he estudiado todos los agujeros estándar de Perl CGI. Por supuesto, podrían haber obtenido la contraseña de mi cuenta de alojamiento de alguna manera, pero el hecho de que estos scripts se hayan colocado en el directorio de datos de mi script CGI me hace sospechar el script. (Además, el hecho de que obtengan mi contraseña "de alguna manera" es una explicación mucho más aterradora). Además, en esa época, mis registros muestran muchos mensajes de "Advertencia, IPN recibidos de una dirección que no son de PayPal", y esas IP vienen de Rusia. Así que parece que alguien al menos estaba TRATANDO de hackear estos scripts.

Dos guiones están involucrados, y los estoy pegando a continuación. ¿Alguien vio algo que pudiera ser explotado para escribir archivos inesperados?

Aquí está el primer script (para recibir PayPal IPN y rastrear las donaciones, y también para rastrear qué sitio genera la mayor cantidad de donaciones):

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

Y, hay algo de redundancia aquí (lo siento, ¿está completo?), Pero aquí está el segundo script (para generar los botones HTML del sitio web que la gente puede agregar a su propio sitio):

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


Ha pasado un tiempo desde que jugué con el módulo CGI de Perl, pero ¿estás seguro de que CGI :: param escapa de los valores? Desde donde estoy sentado, los valores pueden contener comillas inversas y, por lo tanto, se expandirán y ejecutarán.


He visto algo similar antes. En nuestro caso, estoy bastante seguro de que los hackers usaron un desbordamiento de búfer en una biblioteca que no se había actualizado. Entonces pudieron usar un shell de PHP para escribir archivos en el servidor.

Es muy probable que el problema no estuviera en tu código. Actualizar su software con más frecuencia haría que los ataques sean menos probables, pero desafortunadamente es imposible estar completamente a prueba de hackers. Lo más probable es que estuvieran buscando una vulnerabilidad común en una versión anterior del software.


Podría refactorizar su código para convertir todas las referencias de ruta de acceso en constantes de tiempo de compilación con el pragma constant :

use constant { DIR_PRIVATE_DATA => "/paths/of/glory", FILE_DONATION_LOG => "donationLog.txt" }; open( FILE, ">>".DIR_PRIVATE_DATA."/".FILE_DONATION_LOG );

Tratar con las constantes es un dolor porque no se interpolan por qq , y tienes que estar usando constantemente constantemente (s)printf o muchos operadores de concatenación. Pero debería hacer mucho más difícil para los ne''erdowells alterar los argumentos que se están pasando como filepaths.


Si no es un gran problema, PROBABLEMENTE podría simplemente dejarlo en paz. Si no puedes hacer eso, restaura desde una copia de seguridad. Al ver cómo ha sido pirateado durante tanto tiempo, probablemente tampoco puedas hacer eso. Una tercera y más probable respuesta sería hacer una copia de seguridad de lo que pueda, y simplemente oprimirla y comenzar de nuevo. Rehacer todo desde cero.


Tu código me parece bastante seguro. Solo objetaría ligeramente el uso de rutas relativas para los archivos, eso me hace sentir un poco incómodo, pero es difícil imaginar algún riesgo de seguridad en eso. Apostaría que la vulnerabilidad estaba en algún lugar abajo (perl, apache ...)