Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user to change expired password #114

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions conf/config.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@
$ad_options['force_unlock'] = false;
# Force user change password at next login
$ad_options['force_pwd_change'] = false;
# Allow user with expired password to change password
$ad_options['change_expired_password'] = false;

# Samba mode
# true: update sambaNTpassword and sambaPwdLastSet attributes too
Expand Down Expand Up @@ -146,6 +144,9 @@
$answer_objectClass = "extensibleObject";
$answer_attribute = "info";

# Allow change of expired password
$change_expired_password = "false";

# Extra questions (built-in questions are in lang/$lang.inc.php)
#$messages['questions']['ice'] = "What is your favorite ice cream flavor?";

Expand Down
84 changes: 80 additions & 4 deletions lib/functions.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,30 @@
#
#==============================================================================

#hash_equals implementation for older versions of php
if(!is_callable('hash_equals')) {
function hash_equals($str1, $str2) {
if(strlen($str1) != strlen($str2)) {
return false;
} else {
$res = $str1 ^ $str2;
$ret = 0;
for($i = strlen($res) - 1; $i >= 0; $i--) $ret |= ord($res[$i]);
return !$ret;
}
}
}

# Create SSHA password
function make_ssha_password($password) {
$salt = random_bytes(4);
$hash = make_ssha_password_with_salt($password, $salt);
return $hash;
}

# Creates SSHA password using custom salt
# Notes: use ONLY for verification purpose, security issue when using this function to encode a password into a LDAP with a fixed salt.
function make_ssha_password_with_salt($password, $salt) {
$hash = "{SSHA}" . base64_encode(pack("H*", sha1($password . $salt)) . $salt);
return $hash;
}
Expand All @@ -41,6 +62,13 @@ function make_sha512_password($password) {
# Create SMD5 password
function make_smd5_password($password) {
$salt = random_bytes(4);
$hash = make_smd5_password_with_salt($password, $hash);
return $hash;
}

# Creates SMD5 password using custom salt
# Notes: use ONLY for verification purpose, security issue when using this function to encode a password into a LDAP with a fixed salt.
function make_smd5_password_with_salt($password, $salt) {
$hash = "{SMD5}" . base64_encode(pack("H*", md5($password . $salt)) . $salt);
return $hash;
}
Expand All @@ -61,9 +89,9 @@ function make_crypt_password($password, $hash_options) {

// Generate salt
$possible = '0123456789'.
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'./';
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'./';
$salt = "";

while( strlen( $salt ) < $salt_length ) {
Expand All @@ -74,6 +102,13 @@ function make_crypt_password($password, $hash_options) {
$salt = $hash_options['crypt_salt_prefix'] . $salt;
}

$hash = make_crypt_password_with_salt( $password, $salt);
return $hash;
}

# Creates CRYPT password using custom salt
# Notes: use ONLY for verification purpose, security issue when using this function to encode a password into a LDAP with a fixed salt.
function make_crypt_password_with_salt($password, $salt) {
$hash = '{CRYPT}' . crypt( $password, $salt);
return $hash;
}
Expand Down Expand Up @@ -238,6 +273,47 @@ function check_password_strength( $password, $oldpassword, $pwd_policy_config, $

return $result;
}
# Hash old password using same method and salt as ldap password
# @return the old password, hashed
function hash_old_password($ldap_password, $old_password) {
if ( preg_match( '/^\{(\w+)\}/', $ldap_password, $matches ) ) {
$current_hash_method = strtoupper($matches[1]);

# Check old password using current hash
if ( $current_hash_method == "SSHA" ) {
#Get salt of ldap_password
$sha1_len = 20;
$ldap_bytes = base64_decode(substr($ldap_password, 6));
$ldap_salt = substr($ldap_bytes, $sha1_len);
$old_password_hash = make_ssha_password_with_salt($old_password, $ldap_salt);
}
if ( $current_hash_method == "SHA" ) {
$old_password_hash = make_sha_password($old_password);
}
if ( $current_hash_method == "SHA512" ) {
$old_password_hash = make_sha512_password($old_password);
}
if ( $current_hash_method == "SMD5" ) {
$md5_len = 16;
$ldap_bytes = base64_decode(substr($ldap_password, 6));
$ldap_salt = substr($ldap_bytes, $md5_len);
$old_password_hash = make_smd5_password_with_salt($old_password, $ldap_salt);
}
if ( $current_hash_method == "MD5" ) {
$old_password_hash = make_md5_password($old_password);
}
if ( $current_hash_method == "CRYPT" ) {
# salt value and length may vary. Checking values for a recognizable form. They are all described in php crypt method online description
$ldap_bytes = substr($ldap_password, 7);
$old_password_hash = make_crypt_password_with_salt($old_password, $ldap_bytes);
}
}
else {
# Could not get the hash type, empty value for old_password_hash so that it fails
$old_password_hash = "";
}
return $old_password_hash;
}

# Change password
# @return result code
Expand Down Expand Up @@ -267,7 +343,7 @@ function change_password( $ldap, $dn, $password, $ad_mode, $ad_options, $samba_m
if ( isset($userpassword) ) {
if ( preg_match( '/^\{(\w+)\}/', $userpassword[0], $matches ) ) {
$hash = strtoupper($matches[1]);
}
}
}
}
}
Expand Down
37 changes: 36 additions & 1 deletion pages/change.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,48 @@
error_log("LDAP - Bind user password needs to be changed");
$errno = 0;
}
if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $ad_options['change_expired_password'] ) {
if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $change_expired_password ) {
error_log("LDAP - Bind user password is expired");
$errno = 0;
}
unset($extended_error);
}
} elseif ( ! $ad_mode && ($errno == 49) && $change_expired_password && ( $who_change_password == "manager" ) ) {
# Try to check password value without binding
# First rebind as Manager, it will be needed (since our user can't log in)
$bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw);

# Check that the password is not locked (due to several attempts for example)
$search_password_info = ldap_read( $ldap, $userdn, "(objectClass=*)", array("pwdAccountLockedTime", "userPassword") );

# Read LDAP password value
if ( $search_password_info ) {
$first_entry = ldap_first_entry($ldap, $search_password_info);
$pwd_locked = ldap_get_values($ldap, $first_entry, "pwdAccountLockedTime");
if ( $pwd_locked && $pwd_locked["count"] > 0 ) {
#LDAP attribute pwdAccountLockedTime is still set after our earlier ldap_bind attempt: user is locked out
error_log("LDAP - denying password change - user ". $login ." is locked out");
}
else {
$password_val = ldap_get_values($ldap, $first_entry, "userPassword");
if ( $password_val && $password_val["count"] > 0 ) {
$old_pwd_hashed = hash_old_password($password_val[0], $oldpassword);

if ( hash_equals($password_val[0], $old_pwd_hashed)) {
# Hashes match: user can't log in for some reason (expired, most likely) but we should allow a password change
error_log("LDAP - Allowing password change for user " . $login . " despite bind unsuccessful");
$errno = 0;
}
else {
error_log("LDAP - denying password change - passwords don't match");
}
}
}
} else {
error_log("Denying password change - user does not exist");
}
}

if ( $errno ) {
$result = "badcredentials";
error_log("LDAP - Bind user error $errno (".ldap_error($ldap).")");
Expand Down
95 changes: 95 additions & 0 deletions tests/HashOldPasswordTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

require_once __DIR__ . '/../lib/vendor/defuse-crypto.phar';

class HashOldPasswordTest extends \PHPUnit_Framework_TestCase
{
/**
* Test hash_old_password function
*/
public function testHashOldPassword()
{

# Load functions
require_once("lib/functions.inc.php");

$candidate_password = "hello_S3lfServ1ce";

# Test SSHA hashed password
$ldap_password = '{SSHA}/oFcrDRwH5K6Gv3ng1D9I+m32ftIpGivlUB6jw=='; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test SHA1 hashed password
$ldap_password = '{SHA}cxMgpYJFOW1BPAkO4tbAHXwz9Z0='; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test SHA512 hashed password
$ldap_password = '{SHA512}Eg8xKRGYinaxsrM4edoROEUcQoqFRDI3Slcg5wWig80g1VpYFx+DQVqA++TN2B44XDoSMjRkwCOcgrRS+wsS1g=='; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test SMD5 hashed password
$ldap_password = '{SMD5}xT4bI4kPLtAmUYjmRqT2t0H+PTHjFY4C'; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test MD5 hashed password
$ldap_password = '{MD5}iNCvX+AiYnAeZoORPjwOuw=='; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);


# Test CRYPT hashed password - standard DES
$ldap_password = '{CRYPT}WCFar/a24WRlk'; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - extended DES
$ldap_password = '{CRYPT}_6uVCtbvym/90wKTIrKY';
# Generated using random 8-chars string from https://www.random.org/strings/ and https://quickhash.com/ Extended DES algorithm
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - md5 hash
$ldap_password = '{CRYPT}$1$akUYgSg4$WQceCqgPDPgefRr/Zutj70'; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - Blowfish hash with 2a
$ldap_password = '{CRYPT}$2a$06$1ZJSTvTH9xju5zGUoXuMIuDSFw57SLYopcqamCKQYeUgfeUbndMNW';
# Above password generated using random string from https://www.random.org/strings/ and http://php.fnlist.com/crypt_hash/crypt online calculation
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - Blowfish hash with 2y
$ldap_password = '{CRYPT}$2y$06$1ZJSTvTH9xju5zGUoXuMIuDSFw57SLYopcqamCKQYeUgfeUbndMNW';
# Above password generated using random string from https://www.random.org/strings/ and http://php.fnlist.com/crypt_hash/crypt online calculation
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - sha256 sum
$ldap_password = '{CRYPT}$5$4mxifKQN$PRzssKp/vzWdcN3QNXSOuutw7vbS6pR4hgtp9AboH83'; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - sha256 sum with custom rounds value
$ldap_password = '{CRYPT}$5$rounds=12345$eKNy1/RDIlJ$gehYUSLkKuhof/Sbp.N7XHdAe/U6hi1G9ZrdEgDbPx8';
#Above pass obtained via linux command : echo "username:hello_S3lfServ1ce" | chpasswd -c SHA256 -s 12345 and copying /etc/shadow value
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - sha512 sum
$ldap_password = '{CRYPT}$6$T87Jnfqj$6gdQfurLrxU0E6TxzdiklrT1QTDPFTO06vIkDBN2Frx4WMJNr.uMWUm4basMbu8D7mEFVFxXkEED72DNPzoYH.'; # Obtained via Apache Directory Studio
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);

# Test CRYPT hashed password - sha512 sum with custom rounds value
$ldap_password = '{CRYPT}$6$rounds=12345$mgoMm/FzDwtJjkr$j0zvqlK9Tn/iTpEgnKFMCW8us1x.ex54qpzljcCXZfVJL2FHvNg7t2fjdCfqKb7HNMvRC838XdJdiyUmaIkzs/';
#Above pass obtained via linux command : echo "username:hello_S3lfServ1ce" | chpasswd -c SHA512 -s 12345 and copying /etc/shadow value
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
$this->assertEquals($ldap_password, $candidate_hashed);
}
}