The security of a php/javascript authentification scheme
Posted: Sun Nov 16, 2003 6:01 pm
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.
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) 
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";
}
}
?>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...