PHP Login System
Starting Out
This article is not meant to assist in the set up the most secure, unbreakable login system that has ever existed. Instead, it’s just meant to give an idea of the types of things that go into a login system and some features you can add to yours. As always, if you are building your own and you need it to be secure, I’d definitely recommend that you do some additional research into security methods, though this article will give you a starting point. Also, to keep the comments section of this organized, if you have a question about a specific section, please post it in the forum. This will keep everything organized and help others get a response to you. Ok, let’s get started!
The first thing we need to do is to set up a database to hold our users' information. Our database will have four simple fields for now. This is just the database to keep track of the username, password, and email. Here is a schematic of the database (though if you make your own, add some creativity to the names):
| Database Information | ||
| Database Name: | my_data | |
| Table Name: | userpass | |
| User ID Field: | user_ID | INTEGER - Auto Increment, Unsigned, Not Null, Key |
| Username Field: | username | VARCHAR(20) - Not Null |
| Password Field: | password | VARCHAR(35) - Not Null |
| Email Field: | VARCHAR(50) - Not Null | |
Safety Tip: When you actually create your own, be creative with these names. Change them up. No one will ever see them except you. Also, it will make it much harder for hackers to map out your database if you make these names individual.
Now we need to create a new database user to have access to the database. For mine, I'll have the following:
| Database User Information | |
| DB Username: | siteAdmin |
| DB Password: | password |
Now we have our database set up and ready to use. Now it is always a good idea to make an include file that will hold data for your entire. This makes movement and change easy. We're going to call it include.php. include.php will contain all the variables and functions that are used by more than one or two files. So here is what we should have in there now. I added a few functions that are always nice to have:
| include.php | Select All |
|
<?php /* VARIABLES */ //main site variables $domainName="http://localhost/login_system/"; //database variables $dblocation="localhost"; $dbuser="siteAdmin"; $dbpass="password"; $dbname="my_data"; $dbUserTable="userpass"; $dbUserTable_userid="userID"; $dbUserTable_username="username"; $dbUserTable_password="password"; $dbUserTable_email="email"; /* FUNCTIONS */ function strToInt($s) { if(!$s) { return 0; } else { $n = ord(substr($s,0,1)) - 48; return strToInt(substr($s,1)) + ($n*pow(10,strlen($s)-1)); } } function encrypt($str) { return md5($str); } ?> |
|
Now to explain what everything is in this file. The domain name is the path to where you want the "root" directory to be. This is where your site "starts". It will be used to make direct links to anything that needs it. The database variables are from the actual database, so if you used different ones from me, be sure to change them. Also, the freebie function changes a string to an integer. It is a recursive function (it calls itself), so if you want to take a look at it, feel free. There’re easier ways to do this, but this shows an example of a recursive function.
The other function is the encryption method for the users' passwords. Since this will be used in two places (registration and login), adding a function in here will make changing it easier. Feel free to use any encryption method you want, but know that some are better than others. The md5 encryption is a well known scheme with works very well, so I'd suggest sticking with it.
Everything is set up and ready for the actual coding to begin. Time to start coding for the actual site!
The Registration Page
Before we can actually have a login page, we need a way for users to sign up or join, i.e. a registration page. The first thing we are going to need for this page is a simple form to test functionality. We can use the following simple form:
| register.php | Select All |
|
<html> <body> <form action="register.php" method="post"> <table align='center'> <tr> <th colspan='2'>REGESTRATION</th> </tr> <tr> <td align='right'>Username: </td> <td align='left' > <input type='text' name='user'> </td> </tr> <tr> <td align='right'>Password: </td> <td align='left' > <input type='text' name='pass'> </td> </tr> <tr> <td align='right'>Confirm: </td> <td align='left' > <input type='text' name='conf'> </td> </tr> <tr> <td align='right'>Email: </td> <td align='left' > <input type='text' name='mail'> </td> </tr> <tr> <td align='center' colspan='2'> <input type='submit' value='Submit Registration'> </td> </tr> </table> </form> </body> </html> |
|
This code creates a simple HTML form that calls itself when the user clicks the submit button. If you do not know about HTML forms or tables, I would strongly suggest learning about those before continuing. So now the user can type in what they want for their username and password, but how do they actually put it in the data base? Well, since the page calls itself, we'll work through this page. The first thing we need is a check to see if the user has actually submitted the form or not. Add this check to the top of the page before the html tag:
<?php
include("include.php");
if(isset($_POST['user']))
{
}
?>
This checks to see if the $_POST array is set. When the form is submitted, the $_POST array will contain all the information the user input. To get the information out of the array, we can put the following code inside the if statement:
$user=$_POST['user'];
$pass=$_POST['pass'];
$conf=$_POST['conf'];
$mail=$_POST['mail'];
The indices of the array are the names of the corresponding HTML form. Now we need to add checks to make sure that everything is in order with the registration (i.e. entered a valid email address). Below are a few checks that I would add to the page. You can add more if you would like:
| register.php | Select All |
|
$connection = @mysql_connect($dblocation, $dbuser, $dbpass); @mysql_select_db($dbname); $query = "SELECT * FROM " . $dbUserTable; if($result = mysql_query($query)) { $num = mysql_numrows($result); } else { $num = 0; } //username already taken check $i=0; while ($i < $num) { if(mysql_result($result,$i,$dbUserTable_username) == $user) { $userError='Username already taken.'; } $i++; } //invalid username check $regex = '/^[_a-z0-9-]+$/i'; if(!preg_match($regex, $user)) { $userError='Invalid username.'; } //invalid password check $regex = '/^[_a-z0-9-]{5,}$/i'; if($user == $pass || !preg_match($regex, $pass)) { $passError='Invalid password.'; } //confirmation password check if($pass != $conf) { $confError='Passwords do not match.'; } //invalid email check (correct format) $regex = '/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i'; if(!preg_match($regex, $mail)) { $mailError='Invalid email.'; } mysql_close($connection); |
|
Here is what the error checks do (each if statement is a different error check):
- This checks to see that the username hasn't already been taken. The MySQL code before the if statement is used by this check to retrieve all the current user information from the database. It then searches through the data list for matching usernames. If it does find a match, a username error is set.
- This checks that the username is in the proper format. This check is not too restrictive, so feel free to add your own additional checks. As it is now, the check will just check that the username contains at least one character and that all the characters are letters, numbers, hyphens, or underscores. If the username fails this test, a username error is set.
- This check is like the username check, in that it ensures that the password contains only letters, numbers, hyphens, and underscores. However, the password is also checked to make sure that it is at least 5 characters long and that it is not the same as the username. Again, feel free to add your own additional checks (like ensuring that the password is not “password”). If the password fails this check, a password error is set.
- This simply checks that the confirmation password field matches the password field. This just lessens the chance of password typos that lock the user out of their own account on registration. If the two passwords do not match, a confirmation password is set.
- This check ensures that the email is in the proper format. Although there are valid emails that will trigger this error, they involve the use of punctuation characters in the username of the email address, so they are unlikely. The check ensures that the email looks like [username]@[domain] where domain looks like [domain_name].[domain_suffix] where [domain_suffix] is between 2 and 4 characters long. Additionally, the email address is checked to make sure that it doesn't contain any illegal characters.
The preg_match() function uses something called "regular expressions" or "regex" to perform matches. These expressions are EXTREMELY confusing, so don't feel bad if you don't understand these right off. If you want to know more about them, you can read up on them more at this website. If you are planning on reading up on them, I'd recommend reading through this tutorial from the beginning, instead of just bits and pieces.
Now that the errors are set, we need a way to display them. By adding lines like the following to the HTML table just underneath the row with the respective field, the errors will be displayed if they are set:
<?php if(isset($userError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$userError."</font></td></tr>"; ?>
The new code for the registration HTML table will look similar to the following:
| register.php | Select All |
|
<table align='center'> <tr> <th colspan='2'>REGESTRATION</th> </tr> <tr> <td align='right'>Username: </td> <td align='left' > <input type='text' name='user'> </td> </tr> <?php if(isset($userError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$userError."</font></td></tr>"; ?> <tr> <td align='right'>Password: </td> <td align='left' > <input type='text' name='pass'> </td> </tr> <?php if(isset($passError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$passError."</font></td></tr>"; ?> <tr> <td align='right'>Confirm: </td> <td align='left' > <input type='text' name='conf'> </td> </tr> <?php if(isset($confError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$confError."</font></td></tr>"; ?> <tr> <td align='right'>Email: </td> <td align='left' > <input type='text' name='mail'> </td> </tr> <?php if(isset($mailError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$mailError."</font></td></tr>"; ?> <tr> <td align='center' colspan='2'> <input type='submit' value='Submit Registration'> </td> </tr> </table> |
|
Now all we need to do is add functionality ot this page to actually store the users' information in the database. However, if there were errors in the user's information, we don't want to store the information. Therefore, we first need to check that no errors occurred. An if statement like the following after the error checks should fit the bill:
if(!isset($userError) && !isset($passError) && !isset($confError) && !isset($mailError))
{
}
Next, we need to encrypt the password, and actually store the user's information into the database. For this we can use the following PHP code:
| register.php | Select All |
|
$pass=encrypt($pass); $query = "INSERT INTO " . $dbUserTable . " (" . $dbUserTable_username . "," . $dbUserTable_password . "," . $dbUserTable_email . ") " . " VALUES " . "('" . $user . "','" . $pass . "','" . $mail . "')"; mysql_query($query); mysql_close($connection); |
|
For those of you that have read the SQL injection article in the SQL section, this process is SQL injection safe. The validity checks do not allow quotations, semicolons, or backslashes through to the SQL, so there is no way for the user to inject any statements.
Now all you have to do is add a little "Thank you for registering!" message, probably with a link to the login page (this has yet to be built, but it will), and then terminate the script so that it doesn't re-output the sign-up table. When finished, the page will look similar to the following:
| register.php | Select All |
|
<?php include("include.php"); if(isset($_POST['user'])) { $user=$_POST['user']; $pass=$_POST['pass']; $conf=$_POST['conf']; $mail=$_POST['mail']; $connection = @mysql_connect($dblocation, $dbuser, $dbpass); @mysql_select_db($dbname); $query = "SELECT * FROM " . $dbUserTable; if($result = mysql_query($query)) { $num = mysql_numrows($result); } else { $num = 0; } //username already taken check $i=0; while ($i < $num) { if(mysql_result($result,$i,$dbUserTable_username) == $user) { $userError='Username already taken.'; } $i++; } //invalid username check $regex = '/^[_a-z0-9-]+$/i'; if(!preg_match($regex, $user)) { $userError='Invalid username.'; } //invalid password check $regex = '/^[_a-z0-9-]{5,}$/i'; if($user == $pass || !preg_match($regex, $pass)) { $passError='Invalid password.'; } //confirmation password check if($pass != $conf) { $confError='Passwords do not match.'; } //invalid email check (correct format) $regex = '/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i'; if(!preg_match($regex, $mail)) { $mailError='Invalid email.'; } if(!isset($userError) && !isset($passError) && !isset($confError) && !isset($mailError)) { $pass=encrypt($pass); $query = "INSERT INTO " . $dbUserTable . " (" . $dbUserTable_username . "," . $dbUserTable_password . "," . $dbUserTable_email . ") " . " VALUES " . "('" . $user . "','" . $pass . "','" . $mail . "')"; mysql_query($query); mysql_close($connection); ?> <html> <body> <center> <h3>Thank you for registering</h3> </center> <p>Your login information has been stored in the database. Your password has been encrypted so that it cannot be decrypted. Thank you again for registering! You may now <a href='login.php'>login</a>.</p> </body> </html> <?php exit(); } mysql_close($connection); } ?> <html> <body> <form action="register.php" method="post"> <table align='center'> <tr> <th colspan='2'>REGESTRATION</th> </tr> <tr> <td align='right'>Username: </td> <td align='left' > <input type='text' name='user'> </td> </tr> <?php if(isset($userError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$userError."</font></td></tr>"; ?> <tr> <td align='right'>Password: </td> <td align='left' > <input type='text' name='pass'> </td> </tr> <?php if(isset($passError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$passError."</font></td></tr>"; ?> <tr> <td align='right'>Confirm: </td> <td align='left' > <input type='text' name='conf'> </td> </tr> <?php if(isset($confError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$confError."</font></td></tr>"; ?> <tr> <td align='right'>Email: </td> <td align='left' > <input type='text' name='mail'> </td> </tr> <?php if(isset($mailError)) echo "<tr><td colspan='2' align='center'><font color='red'>".$mailError."</font></td></tr>"; ?> <tr> <td align='center' colspan='2'> <input type='submit' value='Submit Registration'> </td> </tr> </table> </form> </body> </html> |
|
Safety tip: When you publish your website (i.e. you are done debugging/testing on this page), be sure to change the password and confirmation HTML fields from 'text' to 'password'. This will keep the actual password hidden, so that screen-shot viruses won't be able to steal the user's password.
The User List
This is an optional step that involves making an administrative page that displays all the users registered to your system as well as an option to delete them. Since this is a very basic login system, more than likely, it would be built upon, not to mention have other things added for the user to do after logging in. Therefore, it would be very convenient to have a list of all the existing users and their information along with an option to delete them (in case you need to add more fields to the database or something like that) for your debug site. If you are just doing these tutorials to learn some PHP, you can skip this step, but I would recommend going through it anyway.
To start out, create a new PHP file called existingUsers.php. The first thing we need to do is to connect our new file to the database to retrieve the information out. Since this information is in a different file, we need to include this file just like we did in the registration page:
| existingUsers.php | Select All |
|
<?php include("include.php"); mysql_connect($dblocation,$dbuser,$dbpass);//connect to the database @mysql_select_db($dbname) or die( "Unable to select database");//select the database mysql_close();//Close the connection ?> |
|
Starting with PHP 5.3, mysql_close() began requiring a specific connection to close. For this reason, these functions may appear slightly different than in other tutorials. In PHP 5.3+, mysql_close() with no parameters will cause an error that will hang the page for a little while. This file shows the mysql connection process better than registration.php, so this comment is here.
Now that we have connected to the database, we need to retrieve all the information out of it. Since we want practically all the information and this is a testing file, we don't need to worry too much about wasted space so we can use the following statement. Stick this just below where you select the database.
$query="SELECT * FROM $dbUserTable";
$result=mysql_query($query);
Now we have all the information we need stored in the $result array. Now we will set up a little HTML table to view the information of one of the users. To do this, we first need to extract the information we need out of the array if user information:
| existingUsers.php | Select All |
|
$id=mysql_result($result,0,$dbUserTable_userid); $user=mysql_result($result,0,$dbUserTable_username); $pass=mysql_result($result,0,$dbUserTable_password); $email=mysql_result($result,0,$dbUserTable_email); |
|
Now we have all the information we need for the first user (indexed at 0). Now we just have to create a simple table to display it. We'll take a pre-emtive action, knowing that we are going to be displaying more than one user and put the <table> tag at the beginning of the file. Our new file should look similar to this:
| existingUsers.php | Select All |
|
<?php include("include.php"); echo "<HTML><BODY><TABLE BORDER='1'>"; mysql_connect($dblocation,$dbuser,$dbpass);//connect to the database @mysql_select_db($dbname) or die( "Unable to select database");//select the database $query="SELECT * FROM $dbUserTable"; $result=mysql_query($query); mysql_close();//Close the connection $id=mysql_result($result,0,$dbUserTable_userid); $user=mysql_result($result,0,$dbUserTable_username); $pass=mysql_result($result,0,$dbUserTable_password); $email=mysql_result($result,0,$dbUserTable_email); echo " <tr> <td ROWSPAN='2'> <b>".(0+1).": </b> </td> <td align='center'> Username: $user </td> <td align='center'> Email: $email <td> </tr> <tr> <td align='center'> Password: [encrypted] </td> <form action='delete.php' method='post'> <td align='center'> <input type='submit' value='Delete User'> </td> </form> </tr>"; ?> </TABLE> </BODY> </HTML> |
|
You may notice a couple of things about this that seem rather weird, so I'll try to explain them. First of all, the reference for the number of user is (0+1). This has a simple explanation. When we incorporate more than one user into this, we will want this number to increment. So we will simply replace the 0 with a variable that kaaps track of the number. The second is that we have no use of the id field. We will use that later when we add the delete function. The form that was added has no way, right now, of distinguishing between users, but once we add the id in there, we can know which user to delete. The last thing is that instead of displaying the password, I opted to display "[encrypted]". Remember that the password that is stored in the database is encypted. This encrypted may not always be pretty and can stretch the form and make it look non-pretty. I simply chose to remove that part and just put "[encrypted]", which has just as much information about the password as the encrypted password does, for asthetic value.
Now we are going to use this template to display all the users' information. We could use a for() loop, but a while() loop works just as well, and we can add a condition in case there are no users in the database. Then, we can simply change the 0s to the variable we used in the loop to get the page to display all of the users' information. The code after "mysql_close();//Close the connection" should look similar to:
| existingUsers.php | Select All |
|
$num=mysql_numrows($result); if($num==0) { echo " <tr> <td></td> <td>There are no existing users</td> <td></td> </tr>"; } else { $i=0; while($i < $num) { $id=mysql_result($result,$i,$dbUserTable_userid); $user=mysql_result($result,$i,$dbUserTable_username); $pass=mysql_result($result,$i,$dbUserTable_password); $email=mysql_result($result,$i,$dbUserTable_email); echo " <tr> <td ROWSPAN=2> <b>".($i+1).": </b> </td> <td align='center'> Username: $user </td> <td align='center'> Email: $email <td> </tr> <tr> <td align='center'> Password: [encrypted] </td> <form action='delete.php' method='post'> <td align='center'> <input type='submit' value='Delete User'> </td> </form> </tr>"; $i++; } } ?> </TABLE> </BODY> </HTML> |
|
Now that the page will display all the information for all of the users, it is time to get the delete function working. The first thing we need to do, as mentioned earlier, is to be able to differentiate between the users so that delete.php knows which user to remove from the database. To do this we will utilize the id of each user. Remember that form that we created? If we add a hidden field to the form, that would get passed to delete.php and we could differentiate between users. So add this between the <form> and <td> tags: </p>
<input name='userID' type='hidden' value='$id'>
Now this file is complete. It does everything it is intended to do. It displays the information inputted and gives the delete option. Now we need to add functionality behind the delete function. Make a new php file called delete.php (this is the file called when the forms are submitted). Since this file will also need to connect to the database, we can start it off just like the other file:
| delete.php | Select All |
|
<?php include("include.php"); mysql_connect($dblocation,$dbuser,$dbpass);//connect to the database @mysql_select_db($dbname) or die( "Unable to select database");//select the database mysql_close();//Close the connection ?> |
|
Next we need to retrieve the information about the user that needs to be deleted. This is stored in the POST array. Also, we don't want people to be accessing this file without entering a user to be deleted so we will put in a little check to make sure that they have (if they haven't, we will direct them back to existingUsers.php). We can add this above the mysql connection to save a little time :P
| delete.php | Select All |
|
if(isset($_POST['userID'])) $id=$_POST['userID']; else { header("Location: existingUsers.php"); exit(); } |
|
Next, we want to actually add the code to delete the user from the database. This is a simple sql statement that should look similar to the following one:
$query = "DELETE FROM $dbUserTable WHERE $dbUserTable_userid='$id'";
mysql_query($query);
And finally, we want to be nicely navigated back to existingUsers.php so we can add this statement to the end:
header("Location: existingUsers.php");
exit();
The two completed files should similar to the following:
| existingUsers.php | Select All |
|
<?php include("include.php"); echo "<HTML><BODY><TABLE BORDER='1'>"; mysql_connect($dblocation,$dbuser,$dbpass);//connect to the database @mysql_select_db($dbname) or die( "Unable to select database");//select the database $query="SELECT * FROM $dbUserTable"; $result=mysql_query($query); mysql_close();//Close the connection $num=mysql_numrows($result); if($num==0) { echo " <tr> <td></td> <td> There are no existing users </td> <td></td> </tr>"; } else { $i=0; while(i < $num) { $id=mysql_result($result,$i,$dbUserTable_userid); $user=mysql_result($result,$i,$dbUserTable_username); $pass=mysql_result($result,$i,$dbUserTable_password); $email=mysql_result($result,$i,$dbUserTable_email); echo " <tr> <td ROWSPAN=2> <b>".($i+1).": </b> </td> <td align='center'> Username: $user </td> <td align='center'> Email: $email </td> </tr> <tr> <td align='center'> Password: [encrypted] </td> <form action='delete.php' method='post'> <input name='userID' type='hidden' value='$id'> <td align='center'> <input type='submit' value='Delete User'> </td> </form> </tr>"; $i++; } } ?> </TABLE> </BODY> </HTML> |
|
| delete.php | Select All |
|
<?php include("include.php"); if(isset($_POST['userID'])) $id=$_POST['userID']; else { header("Location: existingUsers.php"); exit(); } mysql_connect($dblocation,$dbuser,$dbpass);//connect to the database @mysql_select_db($dbname) or die( "Unable to select database");//select the database $query = "DELETE FROM $dbUserTable WHERE $dbUserTable_userid='$id'"; mysql_query($query); mysql_close();//Close the connection header("Location: existingUsers.php"); exit(); ?> |
|
Congratulations, you have finished Chapter 2 1/2 of the login system tutorial. This will be a great tool when you start developing your own stuff.
The Login Page
Let's say we've found the same forgotten password page above. We know someone on this website who stole your pencil in 3rd grade and you are going to get back at them by stealing their password and posting a terrible post on this forum. They will be blamed and their life will be ruined. So here's how we begin.
First we need to know whether this solution is even possible. Will SQL injection work on this system? This is actually pretty simple to do. There are two inputs to try: a single quote and a double quote (both should be tried in case the web designed switched their order in statement. The resulting statement would appear:
sql = "SELECT password FROM user_table WHERE email = ''"'";
This statement itself will not give you any passwords what-so-ever. However, websites very often have different error messages for errors and no-matches. As a result, this input would generate an sql error page instead of a no-users-found or even a success page. If this happens, you know that this page is succeptable to SQL injection.
If you receive a non-standard error message when you input the input above, you know that this website has not placed in protection for SQL injection. This means that we have a chance at stealing our "friend's" password. So now we have to take inventory of what we've got: a website that we can use SQL injection to infiltrate. More importantly, we should make a list of what we don't know:
- The database name
- The database table
- The database field names
In order to find this information out, we'll need to do a process called mapping the database. Essentially, we'll be trying to find information about the structure of the database through trial and error.
Mapping the Database
Taking stock of what we know now, we can begin to map out the database to figure out a way to exploit SQL injection to steal our "friend's" password. It is worth noting that many times when an SQL error occurs, some error pages will actually display the SQL statement. If this happens, most of the need for this mapping is removed. However, if we have to start from scratch, here's an algorithm.
First we'll make a few assumptions about the database. Since we're infiltrating a website that has a user login, we can assume that there is a table that stores all the information about the user: the user's id, the user's username, the user's password, and the user's email address among other things. Since we're going in through an email recover field, we can assume that the SQL statement would look something like:
SELECT password FROM user_table WHERE email = '$user_input'
We'll work from the bottom of out list up. So the first thing we need to find out is the field names for the users' information. Fortunately, this is relatively simple, though it may require some tenacity. We'll design an input that will test these. We already know that the website will generate a different error page for an SQL error and a user-not-found error. Therefore, our input must do three things: finish the first statement in a real fassion, insert our test statement, and comment out the rest of the unnecessary statement. For this example, we'll try and find out the email field name (the bolded part is our input to the text field):
SELECT password FROM user_table WHERE email = 'bad_email' AND emailfield = ''; /*'
There are a few things to note about the statement above. First of all, you want to make sure that the SQL command does not return anything valid. If it does return a valid entry, then someone could get their password emailed to them (so much for being sneaky...). A second thing is the comment characters at the end. We want to make sure that we're executing what we want, so it is a good idea to comment out the rest of the statement instead of trying to fit your input into the statement (just end the statement with your input and comment the rest out). Finally, the input after the 'AND' is what we're using to test the field. After submitting our input, if we get an SQL error page, we know that the field we've tested is wrong. If we get a user-not-found page, we know that we've stumbled across a real field.
This is where we get to be creative. We have to map out all the fields that we wish to use from our little input box. Each time you get an SQL error page, change it slightly to keep testing. Some common email field names are 'email', 'emailfield', 'user_email', 'mail', etc. I think that you can see where the tenacity and creativity come into play. The best advise I would offer at this point is to just keep at it. If they are simple, you'll find them eventually. However, don't waste your entire life trying possible field names; the web developer may have used extremely esoteric names. If you do manage to get all the field names that you need, feel free to continue.
Now we have all the field names from the database that we need. However, these will not do us much good unless we know the database table name. Therefore, finding the database table name is our next task. A similar SQL statement can be constructed using the input to the text field as when we were trying to find the field names:
SELECT password FROM user_table WHERE email = 'bad_email' AND 1=(SELECT COUNT(*) FROM tablename); /*'
The second statement will almost never return a true value, however, it will differentiate between an SQL error (wrong table name) and a user-not-found error (real table name). Again, just as with the field names, you just have to keep trying table names. Some common examples are 'user_table', 'users', 'user_list', and 'members'. Once you find a database table that you believe to be the correct one, all you need to do is test it. Again, this is relatively simple. Also, you'll be happy that there is no guesswork here!
SELECT password FROM user_table WHERE email = 'bad_email' AND tablename.email IS NULL; /*'
When you input the command above, the same set of results as before. If you get an SQL error page, you need to find a new table name. However, if you get a user-not-found error, you know that the field belongs to that table. Simply check that the other fields you've found belong to the table as well, and if they do, you're finished with mapping the database!
Performing Your Mischief
At this point, we've mapped out the database to meet our ends. It's time to really get our hands dirty. For reference, let's say we've mapped the database below:
| Table Name | user_table |
| Username Field | user |
| Password Field | pass |
| Email Field |
This is probably the most fun part of the whole process: everything comes together. The guesswork is much more fun, and consists of more than simply brute-forcing. It is now time to take a better look at how this password recovery system works. This will take a little specialization in each case, but for this example, let's assume the system works as follows: if a legitimate email address is entered, an email is sent to the email address offerring a link to reset the user's password. Let us also assume that the passowrd is stored encrypted inside the database (there's no way to know this, but it is usually safe to assume). This is probably the hardest setup you'd have to face if you've gotten to this point. There is no way to directly get the user's password. Also there is no way to get the encrypted password. This means that there is no way to infiltrate without anyone knowing you've done it. So in this case, we should minimize the number of people who know (only the person who's account you're breaking into will know). The list below details the steps we'll take to do our damage:
- Change the email address to one that we control
- Send the reset password email to ourselves
- Change the password
- Perform our nefarious tasks
- Change the password again
- Change the email address again
Once we've gotten to this point, we only need to engineer two more SQL injection statements, and they will appear very similar. It may be possible to do it in just one, if you can change your email address while logged in. The statement to change the email address is as follows:
SELECT password FROM user_table WHERE email = 'bad_email'; UPDATE user_table SET email='myemail@domain.com' WHERE user='my_friend'; /*'
SELECT password FROM user_table WHERE email = 'bad_email';
UPDATE user_table SET email='myemail@domain.com' WHERE user='my_friend';
Once you have done this to change your email address (remember a user-not-found error page means that you've successfully changed the email address), enter the email address you used in the previous statement into the input field to send yourself the reset password email. Then, reset your "friend's" password to whatever you want. Now you are free to do whatever you want with his/her account. Do your damage!
Now it's time to clean up after yourself. This is important to prevent retaliation or legal action (if you did anything worthy of that). The first thing to do is reset the password to something else that no one would guess. Do this the same way as above, entering your own email into the forgot password field. Here you have some options. You can set the email back to the previous email (assuming you know it). This will allow the previous user to recover his account and discover the damage, etc. etc. However, I would prefer to lock this person out completely. Change the email to an email address that no one has access to. If you do this, your "friend" no longer has his/her password and the email address is not his, so he/she has no way to reset it. Short of emailing the site administrator, convincing them that they are not trying to deceive, and getting them to manually reset the account, this person can no longer access their account. The perfect crime!
Protecting Against SQL Injection
I do not wish to promote people to use SQL injection to try and break into their enemies' websites. Instead the previous example was created to explain the methods of protecting against SQL injection. If we were to re-read the example above, we can identify the steps used access the database. Using these steps, we can engineer a great defense against this technique. Here's the list of what a hacker would have to do:
- Check if it is possible: enter a single and double quote
- Map out the database: guess the table and field names
- Make their own changes: inject their own statement
We'll break these down one by one. The first deals with the heart of the problem, the quotation characters. THe easiest way to stop this is to escape these characters. Add an extra command when parsing that adds a backslash in front of the quotation marks. However, you should be careful as this can still be broken. In addition, you would need to escape the backslash character itself as well. Once you've done this, you are essenstially safe from SQL injection. However, you can never be too careful. There are extra steps that will make future hacking techniques more difficult.
If you remember, the only way that we knew whether we were succeeding or failing when we were injecting was by the differing error messages. Since the average user won't know the difference between the errors, there's not much of a reason to program in different error messages outside of debug. Therefore, Make SQL errors and user-not-found errors look the same, at least in the page they return.
The next task on the hacker's list is to map out the database. This too can be easily stopped. The first way is to implement a maximum number of failed tries before the system locks down for that IP. This would delay and frustrate the attacker, but not stop them (still a good technique to implement to stop brute-forcers). The ideal way to stop this step is to add a fair amount of creativity to your field and table names. Remember, you are the only person who will see them, so just go nuts. Instead of 'user_table', make it 'user_haha_no_sql_injection_table' or 'user_access_denied_table'. You get the idea. Since the would-be hacker would have to guess at the table, just make them unguessable.
The last task on the hacker's list is to inject their own statements into your statement. The easiest way to protect against this is to limit the size of the user's input. This can be done in the HTML for the webpage, however, this is easily bypassed. Therefore, you'll have to include a check for it in the backend for the form. Again, not so difficult to do, but severely limits a would-be hackers options.
There are also some other techniques that are good to implement. They are not geared towards stopping SQL injection, but more towards letting you, the site administrator, know that someone has attempted it. Since you read a few paragraphs previous, you have already implemented the design to give the same error page on both types of errors. However, you can still implement different backends for the two errors. You can have the website either update an administration log or send an email to an administration email on database errors. In addition, the email should include the parsed SQL statement, so you can see if someone is up to mischief. If you also include the user's IP, you can IP ban them, or take other actions as you see fit.
Concluding Remarks
Well, that's really all I have to say about SQL injection. It gets a bit more complicated, but the basic priniples are still the same. If you are more interested, feel free to read up on it in other places. Wikipedia has a nice article on different techniques used by hackers besides the differing error pages. Remember, this is applicable to more than just SQL databases on webpages. Anytimes commands are parsed, you can run into this problem. Also, it may happen unintentionally in non-user input situations. If this happens, it creates a bug that is EXTREMELY difficult to track down. So just keep this in mind and good luck!
Comments (0) Sun, Oct 24,2010 1:20AM