Changing vBulletin 4 its password hashing to use BCrypt

By default, vBulletin uses a very basic and easy to crack password hashing:

md5(md5($password) . $salt)

With the GPU’s of these days, it may be a matter of minutes to crack a password using a dictionary attack/rainbow tables.

Fortunately, it’s very easy to change the password hashing. I will change the password hashing of vBulletin to use BCrypt, a much better algorithm. The password hashing will look like this at the end of this guide:

password_hash(md5(md5($password) . $salt), PASSWORD_BCRYPT, array('salt' => $salt))

 
Requirements for this guide:
– Access to the FTP or permission to edit .php files on the server.
– PHP version of 5.5+ (we make use of the password_hash function)
– You must update all of the passwords in the current database.
– vBulletin 4.2.2 (this modification has been made on 4.2.2)
– Database access in the form of SSH/PHPMyAdmin or any other database manager tool

Create a backup of the files we are going to edit first so you can always restore it in case it goes wrong!

 
Step 1: modify password column type

First things first, we have to modify the type of the password column in the user table.
You can either do this in PHPMyAdmin by changing it to char(60) or by executing the following query:

ALTER TABLE user MODIFY password char(60);

 
Step 2: edit /includes/functions_login.php

Now let’s modify the verify_authentication function in the /includes/functions_login.php file.
Look for:

		if (
			$vbulletin->userinfo['password'] != iif($password AND !$md5password, md5(md5($password) . $vbulletin->userinfo['salt']), '') AND
			$vbulletin->userinfo['password'] != iif($md5password, md5($md5password . $vbulletin->userinfo['salt']), '') AND
			$vbulletin->userinfo['password'] != iif($md5password_utf, md5($md5password_utf . $vbulletin->userinfo['salt']), '')
		)

And replace it with:

		$hash1 = password_hash(md5(md5($password) . $vbulletin->userinfo['salt']), PASSWORD_BCRYPT, array('salt' => $vbulletin->userinfo['salt']));
		$hash2 = password_hash(md5($md5password . $vbulletin->userinfo['salt']), PASSWORD_BCRYPT, array('salt' => $vbulletin->userinfo['salt']));
		$hash3 = password_hash(md5($md5password_utf . $vbulletin->userinfo['salt']), PASSWORD_BCRYPT, array('salt' => $vbulletin->userinfo['salt']));

		if (
			$vbulletin->userinfo['password'] != iif($password AND !$md5password, $hash1, '') AND
			$vbulletin->userinfo['password'] != iif($md5password, $hash2, '') AND
			$vbulletin->userinfo['password'] != iif($md5password_utf, $hash3, '')
		)

This function will, as the function name describes, verify the authentication.

 
Step 3: edit /includes/class_dm_user.php

This file contains the verify_password and hash_password function which must be changed as well.
Look for the hash_password function:

	function hash_password($password, $salt)
	{
		// if the password is not already an md5, md5 it now
		if ($password == '')
		{
		}
		else if (!$this->verify_md5($password))
		{
			$password = md5($password);
		}

		// hash the md5'd password with the salt
		return md5($password . $salt);
	}

And replace it with:

	function hash_password($password, $salt)
	{
		// if the password is not already an md5, md5 it now
		if ($password == '')
		{
		}
		else if (!$this->verify_md5($password))
		{
			$password = md5($password);
		}

		// hash the md5'd password with the salt
		return password_hash(md5($password . $salt), PASSWORD_BCRYPT, array('salt' => $salt));
	}

Now look for the verify_password function:

	function verify_password(&$password)
	{
		//regenerate the salt when the password is changed.  No reason not to and its
		//an easy way to increase the size when the user changes their password (doing
		//it this way avoids having to reset all of the passwords)
		$this->user['salt'] = $salt = $this->fetch_user_salt();

		// generate the password
		$password = $this->hash_password($password, $salt);

		if (!defined('ALLOW_SAME_USERNAME_PASSWORD'))
		{
			// check if password is same as username; if so, set an error and return false
			if ($password == md5(md5($this->fetch_field('username')) . $salt))
			{
				$this->error('sameusernamepass');
				return false;
			}
		}

		$this->set('passworddate', 'FROM_UNIXTIME(' . TIMENOW . ')', false);

		return true;
	}

And replace it with:

	function verify_password(&$password)
	{
		//regenerate the salt when the password is changed.  No reason not to and its
		//an easy way to increase the size when the user changes their password (doing
		//it this way avoids having to reset all of the passwords)
		$this->user['salt'] = $salt = $this->fetch_user_salt();

		// generate the password
		$password = $this->hash_password($password, $salt);

		if (!defined('ALLOW_SAME_USERNAME_PASSWORD'))
		{
			// check if password is same as username; if so, set an error and return false
			if ($password == password_hash(md5(md5($this->fetch_field('username')) . $salt), PASSWORD_BCRYPT, array('salt' => $salt)))
			{
				$this->error('sameusernamepass');
				return false;
			}
		}

		$this->set('passworddate', 'FROM_UNIXTIME(' . TIMENOW . ')', false);

		return true;
	}

We have to modify this function since there’s a check to see if the password matches the current username, which is not allowed by default.

 
Step 4: update all passwords in the user table

The only downside of this is that we have to update all passwords in the database.
But no worries, no one will have to change or update their password to make this work since we use the old password hashing in BCrypt.

Please make a backup of your database or user table before doing this so you can restore it in case it does not work as intended!

Create a file in the root of your forum and name it something like update_passwords.php. Contents of the file:

require("./global.php");
$query = $db->query_read("SELECT userid, password, salt FROM user WHERE password NOT LIKE '$%' ORDER BY userid ASC");

echo 'We have ' . $db->num_rows($query) . ' password(s) to update..

'; while($row = $db->fetch_array($query)){ $db->query_write("UPDATE user SET password = '" . password_hash($row['password'], PASSWORD_BCRYPT, array('salt' => $row['salt'])) . "' WHERE userid = '" . $row['userid'] . "'"); } echo 'Updated all passwords, re-run this script to be sure!';

This will update all of the current password hashes to use BCrypt.
In case the script timed out, just run it again or change the script execution timeout.

 
Step 5: verifying functionality

Now try to login onto your forum and see if that works, in case you get an incorrect username/password error, it means you did something wrong in this guide.
In case it works, try to update your password and relog to see if that part works as well.

I tested the login, registration, remember me checkbox and changing password function on a local test forum which seemed to work fine.

Let me know if you have any questions and I’ll gladly answer them.

Securing your vBulletin 4 installation

Securing your vBulletin forum should be one of the first few things you should do after you installed your forum.
I will provide a list of things you should do:

 
Change the admin control panel directory
Change the directory of your admin control panel on the server and then modify the /includes/config.php file and look for $config[‘Misc’][‘admincpdir’].
Modify this variable to the updated location of your admin control panel.

 
Protecting your admin control panel
There are many different ways to restrict access to your admin control panel.
The thing I recommend is an IP restriction, simply by uploading a .htaccess file to the admincp with something like:

order deny,allow
deny from all
allow from 111.222.333.444

This will block all requests to the admincp, but only allow the IP address 111.222.333.444.
Alternatively, you could use a plugin like http://www.vbulletin.org/forum/showthread.php?t=296383.

 
Setting the right permissions
You should also CHMOD all of your files to 644, except your signature/avatar folder in case you have configured vBulletin to upload everything to the server.
This will prevent anyone from modifying your files

 
Delete your /install/ folder
It’s very important to delete the install folder after installation.
Someone could potentially exploit this and mess up your forum.

 
Check your plugins and keep them up-to-date
You should always be sure that your plugins are up-to-date. You never know if an author of a plugin released a critical security patch.
Also don’t just install plugins without looking at the comments first, it may happen that users made comments on the plugin that the plugin is vulnerable.

 
Check for suspicious files
The vBulletin admin control panel has a nice function under the maintenance > diagnostics tab which allows you to check all vBulletin directories for suspicious files.

In case vBulletin found suspicious .php files, open the files with a FTP client or through SSH and check the source code for things like system, eval, shell_exec, exec, base64_decode and popen. If a file contains something like this, it’s highly likely that it’s a shell and that you are or will be a victim of a hack.

 
HTML in posts/signatures
Be sure that HTML is turned off at all locations. You don’t want users to have the possibility to inject HTML into their signatures or posts.
If you don’t, users may be able to include a Java drive-by, clickjacking and session hijacking scripts on the page.

 
Hosting
If possible, do not use shared hosting but get your own VPS.
A VPS can be very cheap these days and has a lot more capacity and less limitations than a shared website host. Your site will probably even load faster because of this.
The downside is that this is usually un-managed, you will need someone to install a web-server, the PHP-CGI, secure it, etc.

How to make secure vBulletin 4 queries

Something I see a lot is that many vulnerable vBulletin plugins do not sanitize/check variables the right way.
The right way to use user input data in queries is like this:

$vbulletin->input->clean_array_gpc('p', array(
	'username' => TYPE_NOHTML,
	'some_field' => TYPE_INT
));

$db->query->write("
	UPDATE " . TABLE_PREFIX . "table SET
		username = '" . $db->escape_string($vbulletin->GPC['username']) . "'
	WHERE some_Field = '" . $vbulletin->GPC['some_field'] . "'
")

The first argument defined the type of request. p is a POST request in this case.
The second argument is an array with field values and the type of the variable.

Whenever you use a string and it should not contain any HTML, ALWAYS use TYPE_NOHTML. If you use TYPE_STR, it might open up a cross site scripting vulnerability as well as SQL injection.

In case you use a variable which is not an integer, always wrap it around the $db->escape_string function.

Here a small part of the code which is used by the clean_array_gpc function:

			case TYPE_INT:    $data = intval($data);                                   break;
			case TYPE_UINT:   $data = ($data = intval($data)) < 0 ? 0 : $data;         break;
			case TYPE_NUM:    $data = strval($data) + 0;                               break;
			case TYPE_UNUM:   $data = strval($data) + 0;
							  $data = ($data < 0) ? 0 : $data;                         break;
			case TYPE_BINARY: $data = strval($data);                                   break;
			case TYPE_STR:    $data = trim(strval($data));                             break;
			case TYPE_NOTRIM: $data = strval($data);                                   break;
			case TYPE_NOHTML: $data = htmlspecialchars_uni(trim(strval($data)));       break;
			case TYPE_BOOL:   $data = in_array(strtolower($data), $booltypes) ? 1 : 0; break;

As you can see, variables which should be integers get wrapped around the intval function.
NOHTML variables will be wrapped around the htmlspecialchars function, which converts special characters to HTML entities

Never think that the clean_array_gpc or clean_gpc functions actually clean strings of bad stuff, they do not!