The security of a php/javascript authentification scheme

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
cybaf
Forum Commoner
Posts: 89
Joined: Tue Oct 01, 2002 5:28 am
Location: Gothenburg Sweden

The security of a php/javascript authentification scheme

Post by cybaf »

Hi!

About 1,5 years ago I started to look into secure web authentification without the use of SSL and "expensive" certificates. So I made a little something which I'm about to go through now.

So, I asked myself what was the current alternatives there were to SSL. Using .htaccess would be a joke as a security-measure since all data is sent in clear text over the network. So this was naturally the first problem to arise; how to send data over the network without anyone being able to read it.

Another direct problem that arose was the issue that the webserver was the one being able to run the php. So using php encryption methods would not work when the clients send the data from a form.

Further, the problem with keeping state came to my attention. The use of sessions is not ideal, but was the only thing I found usable.

I then looked into the way that M$ Windows use for authentification. (at least for NT workstations) They use a scheme called Challenge-Response. This works in a way that the server sends out a challenge, probably a MD5-hash or alike, and then the client make a new MD5-hash of the users username and password entered combined with the challenge, and sends it to the server. The server then does the same with the username and password from the database combined with the challenge and checks for a match.

The above was what I ended up implementing for php using a MD5-hashing javascript to do the clients part of the job.

Now, the purpose of this thread, besides enlightning those interested in authentification, is to use our combined knowledge to test how really safe this is. The code for my script is below, and also my analysis of the security of the script.

Code: Select all

<?php
class ezAuth {
  var $userData = array();
  var $_options = array(); //for future
  var $msg;
  var $ez;
  var $requiredUserLevel;
  var $action;

  function ezAuth($userLevel='') { //constructor
    $sessionName='ezAuthMDO';
    $this->ez = new ezMysql('localhost','mysqlUser','mysqlPass','db'); //ezMysql is just another class handling mysql-connections and queries
    session_name($sessionName);
    session_start();
    $this->requiredUserLevel = $userLevel;
    if(isset($_SESSION["valid"]) && !isset($_POST['fromlogin'])) { // user is logged in
      $this->getUserData($_SESSION['user']);
      $this->login($this->userData['user'],$this->userData['challenge'],$this->userData['response']);
    } else if(isset($_POST['fromlogin'])) {
      $this->login($_POST['user'],$_POST['challenge'],$_POST['response']);
    } else { // first time user loads the page
      $this->msg = "<b>The requested page is protected</b>";
      $this->printLoginPage($this->generateChallenge());
      exit;
    }
  }
	
  function login($user='',$challenge='',$response='') {
    if($user=='') {
      $this->msg = "<b>Access Denied: Invalid login!</b>";
      $this->printLoginPage($this->generateChallenge());
      exit;
    }else if($challenge=='') {
      $this->msg = "<b>Access Denied: Invalid login!</b>";
      $this->printLoginPage($this->generateChallenge());
      exit;
    }else if($response=='') {
      $this->msg = "<b>Access Denied: Invalid login!</b>";
      $this->printLoginPage($this->generateChallenge());
      exit;
    } else { // supplied variables are ok
      // main login check
      $mytime = time();
      $now = date("Y-m-d H:i:s",$mytime);
      $timeTenMinsAgo = strtotime($now) - (10*60);
      $row = mysql_fetch_row($this->ez->ezSelect("*","users","username='" . $user . "'")); // user not found	
      if($row[0]=='') {
        $this->msg = "<b>Access Denied: Invalid login!</b>";
        $this->printLoginPage($this->generateChallenge());
        exit;
      }
      
      $this->logOutExpired($timeTenMinsAgo); // logs out users who are not have not logged out and have expired.
      if((!isset($this->requiredUserLevel)) || (isset($this->requiredUserLevel) && $this->requiredUserLevel <= $row[3])) {
        if(isset($this->userData['lastChange'])) { // user is already validated but might have expired
          if(strtotime($this->userData['lastChange']) < $timeTenMinsAgo) {
            $this->msg = "<b>Your session has expired. Please login again!</b>";
            $this->printLoginPage($this->generateChallenge());
            exit;
          }
        }

        $challengeResponse = md5($row[0] . ":" . $row[1] . ":" .  $challenge);
        if($challengeResponse === $response) { //login = valid
          $_SESSION['user'] = $user;
          $_SESSION['response'] = $response;
          $_SESSION['challenge'] = $challenge;
          $_SESSION['valid'] = 1;
          $myArgs = array();
          $myArgs['isAuth'] = 1;
          $myArgs['loginTime'] = $now;
          $this->ez->ezUpdate("users",$myArgs,"username='" . $user . "'");
          $this->getUserData($_SESSION['user']);
          //finally logged in
        } else {
          $this->msg = "<b>Access Denied: Invalid login!</b>";
          $this->printLoginPage($this->generateChallenge());
          exit;
        }
      } else {
        $this->msg = "<b>You do not have access to this page!</b>";
        $this->printLoginPage($this->generateChallenge());
        exit;
      }
				
    }
  }

  function logOutExpired($time='') {
    if($time=='') $time=time();
    $found = $this->ez->ezSelect("username,loginTime","users","isAuth='1'");
    if($found!='') {
      while($data=mysql_fetch_object($found)) {
        if(strtotime($data->loginTime) < $time) {
          $args['isAuth'] = 0;
          $this->ez->ezUpdate("users",$args,"username='".$data->username."'");
        }
      }
    }
  }
	
  function getUserData($user='') {
    $row = mysql_fetch_row($this->ez->ezSelect("*","users","username='" . $user . "'"));
    if($row[0]=='') {
      $this->msg = "<b>Access Denied: Invalid login!</b>";
      $this->printLoginPage($this->generateChallenge());
      exit;
    }
    $this->userData['user'] = $row[0];
    $this->userData['isAdmin'] = $row[3];
    $this->userData['lastChange'] = $row[4];
    $this->userData['response'] = $_SESSION['response'];
    $this->userData['challenge'] = $_SESSION['challenge'];
  }
	
  function generateChallenge() {
    $ip = $_SERVER['REMOTE_ADDR'];
    $now = time();
    $str = $ip . $now . rand();
    return md5($str);
  }

  function printLoginPage($challenge) {
    print "<html><head><title>Login</title><script language="javascript" src="md5.js"></script>\n";
    print "<script language="javascript">\n";
    print "function authenticate() {\n";
    print "str = document.forms['login'].user.value + ":";\n";
    print "str += calcMD5(document.forms['login'].password.value) + ":";\n";
    print "str += document.forms['login'].challenge.value;\n";
    print "document.forms['login'].response.value = calcMD5(str);\n";
    print "document.forms['login'].password.value = "";\n";
    print "document.forms['login'].action = "$_SERVER[PHP_SELF]";\n";
    print "document.forms['login'].submit();\n";
    print "}\n </script></head>\n<body onLoad="document.forms['login'].user.focus()">\n";
    print "<center>\n";
    print "<b>Authentication Required</b><br>\n";
    print "</font>\n";
    print "<font size='0' face='Verdana,Helvetica,Sans-serif'>\n";
    print "$this->msg\n<br><br>";
    print "<form name="login" method="POST">\n";
    print "<b>Username:</b><br>";
    print "<input type="text" name="user">\n";
    print "<b>Password:</b><br>";
    print "<input type="password" name="password">\n";
    print "<input type="submit" value="login" onclick="authenticate();">\n<br>";
    print "<a href='register.php'>register</a>\n";
    print "<input type="hidden" name="challenge" value="$challenge"><br>\n";
    print "<input type="hidden" name="response" value="">\n";
    print "<input type="hidden" name="fromlogin" value="1"></form>\n";
    print "</font>\n";
    print "</center>\n";
    print "</body></html>\n";
  }
}
?>
The above uses a MySql-database with a users-table with 5 fields: (username, password, isAuth, isAdmin, loginTime)

My analysis:

The only things transmitted in the authentification process is 1) the md5 challenge from server => client, 2) the username client => server, 3) the md5-hash of the password and the challenge client => server, 4) the challenge sent back client => server.

The challenge is built up from the users current IP, current time, and a random number. This way the challenge will be "quite" difficult to forge by a potential attacker.

If a user "forgets" to click the logout button to unset the session variables, the login is only valid 10 minutes after last action.

A replay-attack would not work either since the challenge is based on time.

As I see it, a potential attacker would first have to spoof a user's ip, and then hijack that user's session to gain access using the above code.

ok... I hope I still have some patient readers who have followed me down this looong post. :)

Please have a look at this and analyze it thurroully (hmm spelling), as I think we will all gain from this.

//cybaf

(and no... it didn't take me 1,5 years to finish the script...:)...just havn't thought about posting it here before) ;)
User avatar
cybaf
Forum Commoner
Posts: 89
Joined: Tue Oct 01, 2002 5:28 am
Location: Gothenburg Sweden

Post by cybaf »

hmm... perhaps too long for anyone to even look at... *doh*
User avatar
infolock
DevNet Resident
Posts: 1708
Joined: Wed Sep 25, 2002 7:47 pm

Post by infolock »

no, wasn't that long, it probably is just taking a few of us a while to get to it. gonna take even longer to be able to go through it all since i'm at school, but an interesting read at the very very least. the theory behind the security proceedures seems to be correct, but the only thing that makes me wonder about it is, if M$ made it, how secure can it really be? just kidding. using challenges is new to me. i am eager to view this is more detail later. good job
hedge
Forum Contributor
Posts: 234
Joined: Fri Aug 30, 2002 10:19 am
Location: Calgary, AB, Canada

Post by hedge »

I didn't have time to go through it any detail. I guess I don't see how it's any more secure... If I can see the js then I can figure out the encryption can I not?
User avatar
cybaf
Forum Commoner
Posts: 89
Joined: Tue Oct 01, 2002 5:28 am
Location: Gothenburg Sweden

Post by cybaf »

well... the javascript is a md5-hash function that works the exact same way as any other md5 hash function, which is quite complicated and irreverable. So I don't think that will get you anywhere. want me to post the js aswell??
User avatar
BDKR
DevNet Resident
Posts: 1207
Joined: Sat Jun 08, 2002 1:24 pm
Location: Florida
Contact:

Post by BDKR »

Hi Cybaff,

This was the same conclusion I came to as well. As a matter of fact, in my last job in Venezuela I used SHA1 160 bit hash done client side. It's potentially a little cumbersome to deal with as the sha1() function in PHP is a PHP5 thing.

For the most part, I think this is a scheme that goes a long ways and is extremely difficult to deal with.

The potential weak area is the fact that the hashed password can still be captured. Once that's done, and if the attacker knows the persons user name, which he or she most likely does as they've allready captured the hashed password, then all they have to do is a build a form that for all intents and purposes looks like what the server wants, but eliminates the javascript hash functionality.

But this is something that most people won't do anyways so your scheme will easily deal with 70% (an irresponsible guess) of idiot wanna be's. That upper 30% can bust it without a sweat.

But it's still much MUCH better than nothing! And without the ability to use a certificate, it's one of the few viable options.

However, if you use a linux based server, you should be able to generate your own certificate no problem and with no money. Allmost every Linux distro I've worked with in the last 4 years is capable of running a secure (as in https://) web server for no money.

Cheers,
BDKR
User avatar
cybaf
Forum Commoner
Posts: 89
Joined: Tue Oct 01, 2002 5:28 am
Location: Gothenburg Sweden

Post by cybaf »

Thanks for the response!

When I was writing the above article I sortof came to the same conclusion that a potential attacker could capture the hashed password-with-extras. However, it will not work to break the script the way that you suggested, that is with knowing the username combined with knowing the hash.

The reason for why it doesn't work is that the server sends out a different challenge every time, and that challenge is combined in the password hash. And that same hash is sent back to the server from the javascript.

So a natural followup on that would be that if an attacker captures the password-combined hash as well as the perticular challenge for that session, he could as you suggested create a form which sends the password hash with the correct challenge he/she could gain access.

An idea for a solution to this might be to set a field in a temporary database, perhaps using the users ip as a key, with the sent challenge. And then never send the challenge back to the server again. Then the server gets the password-combined hash and gets the challenge from the temp-database using the senders ip to get the correct challenge.

This will make it even safer I think, but it can still be broken if an attacker spoofs the users IP, or I guess if the user about to log in is behind some proxy or firewall, making it look like different persons come from the same IP.

I know that I can make my own certificates, but I just don't like the prompt that comes up for users to verify that they trust my certificate every time. (at least in IE) but maybe that's just me...:)
User avatar
BDKR
DevNet Resident
Posts: 1207
Joined: Sat Jun 08, 2002 1:24 pm
Location: Florida
Contact:

Post by BDKR »

cybaf wrote:Thanks for the response!
An idea for a solution to this might be to set a field in a temporary database, perhaps using the users ip as a key, with the sent challenge. And then never send the challenge back to the server again. Then the server gets the password-combined hash and gets the challenge from the temp-database using the senders ip to get the correct challenge.

This will make it even safer I think, but it can still be broken if an attacker spoofs the users IP, or I guess if the user about to log in is behind some proxy or firewall, making it look like different persons come from the same IP.
This sounds like IP Locking (is that the correct term?). I did something like that once at the session level where once a person started a session from a particular IP, they had to continue the session from the same IP. While that makes sense per session, it's a little limiting to the user in general if he or she could only log in from one IP address.

I also had the additional problem in the example above where if the person was coming through our backup firewall into the cluster, the ip address was allways different as I was doing some weird NAT stuff to ensure that the packets were being routed correctly back out of the cluster (you can only have defualt gateway).

All in all, my conclusion is that IP locking can easily create more problems if you try to use it in all situations.

Cheers,
BDKR
User avatar
cybaf
Forum Commoner
Posts: 89
Joined: Tue Oct 01, 2002 5:28 am
Location: Gothenburg Sweden

Post by cybaf »

ok, well IP-locking in the sense you mean is not exactly the way I meant. the ip would only matter during the login phase, and then the user would be kept online with sessions in combination with the isAuth field in the database set to true (or 1).

having the same ip is only important when the server calculates what challenge to send to the login-form, and when the client sends the auth info back.

I'll implement the solution I'm thinking of and then post it back here, maybe in a simplified version, so that more of us can understand the code. not that it is very advanced, just that I at least think it is quite difficult to "read" others code and understand it in the way it is meant to be understood. :)
User avatar
BDKR
DevNet Resident
Posts: 1207
Joined: Sat Jun 08, 2002 1:24 pm
Location: Florida
Contact:

Post by BDKR »

cybaf wrote: I'll implement the solution I'm thinking of and then post it back here, maybe in a simplified version, so that more of us can understand the code. not that it is very advanced, just that I at least think it is quite difficult to "read" others code and understand it in the way it is meant to be understood. :)
Cool! I wasn't too sure I understood anyways.

Cheers,
BDKR
Post Reply