Local File Inclusions in Perl/CGI

Last time we wrote about Local File Inclusion we covered the PHP vectors, this time we will discuss the Perl/CGI vectors instead. Perl/CGI consists of Perl scripts with the file endings .cgi and .pl, as opposed to PHP’s .php. While the concept remains the same, the Perl/CGI way of this attack differs greatly from PHP. 

Let’s say a web site is running Perl/CGI, a common way of showing different content while browsing is to load other resources located on the server. The setup we’ll explore consists of a single page used as a foothold for reading other files located on the server.

Say we visit the index page:

…from the index page, we navigate to the contact page, like so;

Notice how the file variable is getting changed whenever we visit another link on the website.

As I explained, the index.cgi is a web page (Perl/CGI script) that acts as a foothold for displaying various contents on the website. The source code looks something like this:

#!/usr/bin/perl 

$query = $ENV{'QUERY_STRING'};

#Do we have any parameters?
if (length ($query) > 0) {

    #Separate the parameters as a collection of key-value paris.
    @pairs = split(/&/, $query); 
    foreach $p (@pairs) {
        ($k, $v) = split(/=/, $p); 
        $kvp{$k} = $v; 
    } 
    $file = $kvp{'file'};
    $content = '';
    
    #Try to read the file.
    if(open(FILE, "$file")){
        while(<FILE>){
            $content.=$_;
        }
        close(FILE);
    }else{
        $content = "Unable to load resource $file.\r\n";
    }
    $length = length($content);
    
    #Generate the HTTP response.
    print "Content-Type: text/html\n\n"; 
    print "Content-Length: $length\n\n";
    print "\r\n";
    print $content; 
} else {

    #No parameters? Weird. Redirect to the index page.
    print "Location: ./cgi-bin/index.cgi?file=index.html\r\n";
}

#Finalize execution.
exit 0;

This script is bad in several ways, for example it has a Cross Site Scripting (XSS) flaw, but mainly, it has a local file inclusion vulnerability. This is because the $file variable is derived from the query string (everything after the question mark in the URL is interpreted as the query string).
E.g; http://vuln/cgi-bin/index.cgi?file=index.html

So by changing the file variable to /etc/passwd in the URL (or any other locally stored file) an attacker would be able to read the contents of the file specified.

Example: http://vuln/cgi-bin/index.cgi?file=/etc/passwd

How to avoid this?

We have to validate the input data. We could use a conditional statement to check if the $file variable appears to be valid, or we could make a whitelist containing all allowed accessible files. However, it will actually be less of a hassle using Perl’s stupendous regular expression functionality to make a dynamic whitelist, only allowing what appears to be valid HTML files.

For example: $file =~ m/^[\w\s]+.html?$/ig;

If you are not familiar with regular expressions, or regex, you probably don’t have a clue what that line of code really did. Simply, it will check if a regex pattern matches the $file variable.

$variable =~ <operator> / <pattern> / <flags>;

Effectively, we check if our $file variable is compatible with the pattern by specifying the m operator (Here you can find more information about the m-operator). The i-flag at the end stands for case insensitive matching, which means that Perl will interpret the variable disregarding if the characters are upper- or lowercase. In conclusion, this code will return a boolean indicating whether or not the filename appears to be a valid whitelisted file or not.

Another thing which may be good to keep in mind; the Perl open()-function could be used to execute arbitrary operating system commands as well.

If the filename begins with “|”, the filename is interpreted as a command to which output is to be piped, and if the filename ends with a “|”, the filename is interpreted as a command which pipes output to us." - http://www.ccsf.edu/Pub/Perl/perlfunc/open.html

You can however specify how the open() function shall interpret the filename. The following identifiers can be used as prefixes to force the correct mode of operation:

  • » Appends data to a file.
  • > Overwrites a file with new data.
  • < Reads data from a file.

A way to fix the problem in our example above is to change this line:

if(open(FILE, "$file")){

With this patched version:

if(open(FILE, "<$file")){

Notice how we prepended the < character to force read-only operations.

It’s starting to get secure now! However, we’re not done yet. Do you see the remaining flaws?

Environment Variables and Client Side vulnerabilities!

If someone were to have access on the server, he or she would be able to tamper with the underlying operating systems environment variables. That means, the person in question could change the file enumeration flow of relative paths by modifying the PATH variable. Sounds complicated? It’s not really.

In our original snippet of code, a malicious user could type in the full path to a file on the system, e.g: index.cgi?file=/etc/passwd

With our regex whitelist, the user would now only get the capability to view files starting out alphanumerically, and ending with .html. This is expected behaviour, although, this opens up for the PATH environmental bug. If the attacker were to provide an invalid path, like: index.cgi?file=this_doesnt_exist.html

The operating system wouldn’t find the file in the local scripts directory (/cgi-bin/), but instead, would start searching parts of the operating system (specified by the PATH variable) for the file in question.

A quick patch would be prepend the current working directory before the user supplied path variable. Example:

$file = cwd().'/'.$file;

To use the cwd() function in Perl, you must include it’s references by this snippet:

use Cwd;

And finally, we have a Cross Site Scripting (XSS) flaw in the $content variable.

The best way to skip this would to whitelist it in Perl with a similar pattern as used for validating files or by escaping it properly using HTML::Entities::encode(). However, we chose to remove the vulnerability all together.

Change this line:

$content = "Unable to load resource $file.\r\n";

To this:

$content = "Unable to load resource.\r\n";

Done. The user supplied data is gone. No more XSS.

So what have we done?

  • Path traversal bugs should be eliminated by our whitelisted files.
  • Files should no longer be able to be interpreted as operating system commands by the < prefix used in open().
  • Environmental bugs shouldn’t be considered a threat in our code. We prepend the current working directory before any relative paths, e.g; we transform relative paths to absolute paths.
  • The Cross Site Scripting (XSS) flaw is simply removed. You could however use the HTML::Entities library to escape user supplied data.

This knowledge is key to keep your Perl/CGI scripts safe and secure. There are of course many more factors to take in consideration. For further information check out the following resources:

This is how the patched example is supposed to look like:

#!/usr/bin/perl 
use Cwd;

$query = $ENV{'QUERY_STRING'};

#Do we have any parameters?
if (length ($query) > 0) {
    #Separate the parameters as a collection of key-value paris.
    @pairs = split(/&/, $query); 
    foreach $p (@pairs) {
        ($k, $v) = split(/=/, $p); 
        $kvp{$k} = $v; 
    } 
    $file = $kvp{'file'};
    $content = "Unable to load resource.\r\n";
    
    #Validate against a whitelist of allowed files.
    if($file =~ m/^[\w\s]+.html?$/i){

        #Prepend the current working directory.
        $cwd = cwd();
        $file = "$cwd/$file";       

        #Try to read the file.
        if(open(FILE, "<$file")){
            while(<FILE>){
                $content.=$_;
            }
            close(FILE);
        }
    }
    $length = length($content);
    
    #Generate the HTTP response.
    print "Content-Type: text/html\n\n"; 
    print "Content-Length: $length\n\n";
    print "\r\n";
    print $content; 
} else {
    #No parameters? Weird. Redirect to the index page.
    print "Location: ./cgi-bin/index.cgi?file=index.html\r\n";
}

#Finalize execution.
exit 0;

Further questions? Did we miss something?
Hit us up with an email at info@detectify.com, or tweet us at @detectify.

By: Håkon Vågsether & Fredrik Nordberg Almroth

comments powered by Disqus