Today, I was working on a script to automate detection of poor WordPress admin passwords by using the top 500 commonly used bad passwords. Digging into WordPress core functions is a simple task and I was quickly able to narrow down how they generate their password hashes to the following functions in wp-includes/pluggable.php:
function wp_hash_password($password) {
global $wp_hasher;
if ( empty($wp_hasher) ) {
require_once( ABSPATH . WPINC . '/class-phpass.php');
// By default, use the portable hash from phpass
$wp_hasher = new PasswordHash(8, true);
}
return $wp_hasher->HashPassword( trim( $password ) );
}
This snippet of code uses phpass to securely store the password in the database. For more detail see: http://www.openwall.com/phpass/
Reviewing their documentation I saw they had a python port here: https://github.com/exavolt/python-phpass
This python egg is what does all the real work and allows us to replicate the check function WordPress uses internally in Python.
PHP
function wp_check_password($password, $hash, $user_id = '') {
global $wp_hasher;
// If the hash is still md5 (older WP installs)...
if ( strlen($hash) <= 32 ) {
$check = ( $hash == md5($password) );
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}
if ( empty($wp_hasher) ) {
require_once( ABSPATH . WPINC . '/class-phpass.php');
$wp_hasher = new PasswordHash(8, true);
}
$check = $wp_hasher->CheckPassword($password, $hash);
/** This filter is documented in wp-includes/pluggable.php */
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}
To python
'''
Test the site admin against common easily crackable passwords
'''
def test_passwords(data, password_list):
url = data['url'][0]
users = data['users']
user_count = len(users)
total_md5 = 0
errors = []
insecure = []
for u in users:
username = u[0]
password_hash = u[1]
if len(password_hash) <= 32:
'''
Haven't written support yet for older versions and md5 conversion
'''
total_md5 += 1
else:
wp_hasher = phpass.PasswordHash(8, True)
for p in password_list:
p = p.strip()
check = wp_hasher.check_password(p, password_hash)
if check:
insecure.append("[!] Insecure password found for admin user %s:%s on %s" % (username, p, url))
if total_md5 == user_count:
errors.append("[!] All admin users require conversion from MD5 on %s" % url)
return errors, insecure
For more details see: https://github.com/morissette/insecure-wp-admin-password-check
