Skip to content

Commit 18ce340

Browse files
author
Fieux
committed
Allow user to change expired password
1 parent 7f4d18b commit 18ce340

File tree

4 files changed

+238
-7
lines changed

4 files changed

+238
-7
lines changed

conf/config.inc.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
$ad_options['force_unlock'] = false;
4141
# Force user change password at next login
4242
$ad_options['force_pwd_change'] = false;
43-
# Allow user with expired password to change password
44-
$ad_options['change_expired_password'] = false;
4543

4644
# Samba mode
4745
# true: update sambaNTpassword and sambaPwdLastSet attributes too
@@ -146,6 +144,9 @@
146144
$answer_objectClass = "extensibleObject";
147145
$answer_attribute = "info";
148146

147+
# Allow change of expired password
148+
$change_expired_password = "false";
149+
149150
# Extra questions (built-in questions are in lang/$lang.inc.php)
150151
#$messages['questions']['ice'] = "What is your favorite ice cream flavor?";
151152

lib/functions.inc.php

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
# Create SSHA password
2323
function make_ssha_password($password) {
2424
$salt = random_bytes(4);
25+
$hash = make_ssha_password_with_salt($password, $salt);
26+
return $hash;
27+
}
28+
29+
# Creates SSHA password using custom salt
30+
# Notes: use ONLY for verification purpose, security issue when using this function to encode a password into a LDAP with a fixed salt.
31+
function make_ssha_password_with_salt($password, $salt) {
2532
$hash = "{SSHA}" . base64_encode(pack("H*", sha1($password . $salt)) . $salt);
2633
return $hash;
2734
}
@@ -41,6 +48,13 @@ function make_sha512_password($password) {
4148
# Create SMD5 password
4249
function make_smd5_password($password) {
4350
$salt = random_bytes(4);
51+
$hash = make_smd5_password_with_salt($password, $hash);
52+
return $hash;
53+
}
54+
55+
# Creates SMD5 password using custom salt
56+
# Notes: use ONLY for verification purpose, security issue when using this function to encode a password into a LDAP with a fixed salt.
57+
function make_smd5_password_with_salt($password, $salt) {
4458
$hash = "{SMD5}" . base64_encode(pack("H*", md5($password . $salt)) . $salt);
4559
return $hash;
4660
}
@@ -61,9 +75,9 @@ function make_crypt_password($password, $hash_options) {
6175

6276
// Generate salt
6377
$possible = '0123456789'.
64-
'abcdefghijklmnopqrstuvwxyz'.
65-
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
66-
'./';
78+
'abcdefghijklmnopqrstuvwxyz'.
79+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
80+
'./';
6781
$salt = "";
6882

6983
while( strlen( $salt ) < $salt_length ) {
@@ -74,6 +88,13 @@ function make_crypt_password($password, $hash_options) {
7488
$salt = $hash_options['crypt_salt_prefix'] . $salt;
7589
}
7690

91+
$hash = make_crypt_password_with_salt( $password, $salt);
92+
return $hash;
93+
}
94+
95+
# Creates CRYPT password using custom salt
96+
# Notes: use ONLY for verification purpose, security issue when using this function to encode a password into a LDAP with a fixed salt.
97+
function make_crypt_password_with_salt($password, $salt) {
7798
$hash = '{CRYPT}' . crypt( $password, $salt);
7899
return $hash;
79100
}
@@ -238,6 +259,84 @@ function check_password_strength( $password, $oldpassword, $pwd_policy_config, $
238259

239260
return $result;
240261
}
262+
# Hash old password using same method and salt as ldap password
263+
# @return the old password, hashed
264+
function hash_old_password($ldap_password, $old_password) {
265+
if ( preg_match( '/^\{(\w+)\}/', $ldap_password, $matches ) ) {
266+
$current_hash_method = strtoupper($matches[1]);
267+
268+
# Check old password using current hash
269+
if ( $current_hash_method == "SSHA" ) {
270+
#Get salt of ldap_password
271+
$sha1_len = 20;
272+
$ldap_bytes = base64_decode(str_replace("{SSHA}", "", str_replace("{ssha}", "", $ldap_password)));
273+
$ldap_salt = substr($ldap_bytes, $sha1_len);
274+
$old_password_hash = make_ssha_password_with_salt($old_password, $ldap_salt);
275+
}
276+
if ( $current_hash_method == "SHA" ) {
277+
$old_password_hash = make_sha_password($old_password);
278+
}
279+
if ( $current_hash_method == "SHA512" ) {
280+
$old_password_hash = make_sha512_password($old_password);
281+
}
282+
if ( $current_hash_method == "SMD5" ) {
283+
$md5_len = 16;
284+
$ldap_bytes = base64_decode(str_replace("{SMD5}", "", str_replace("{smd5}", "", $ldap_password)));
285+
$ldap_salt = substr($ldap_bytes, $md5_len);
286+
$old_password_hash = make_smd5_password_with_salt($old_password, $ldap_salt);
287+
}
288+
if ( $current_hash_method == "MD5" ) {
289+
$old_password_hash = make_md5_password($old_password);
290+
}
291+
if ( $current_hash_method == "CRYPT" ) {
292+
# salt value and length may vary. Checking values for a recognizable form. They are all described in php crypt method online description
293+
$ldap_bytes = str_replace("{CRYPT}", "", str_replace("{crypt}", "", $ldap_password));
294+
if ( substr($ldap_bytes, 0, 1) == '_' ) {
295+
# [NOT TESTED]if $ldap_password starts with '_': Extended DES hash, salt starts with '_' and is 9 bytes long
296+
$ldap_salt = substr($ldap_bytes, 0, 9);
297+
}
298+
elseif ( substr($ldap_bytes, 0, 3) == '$1$' ) {
299+
# if $ldap_password starts with '$1$': MD5 hashing, salt starts with $1$ and ends with '$'
300+
$exploded = explode('$', $ldap_bytes, 4);
301+
$ldap_salt = '$1$' . $exploded[2] . '$';
302+
}
303+
elseif ( substr($ldap_bytes, 0, 2) == '$2' ) {
304+
# [NOT TESTABLE]if $ldap_password starts with '$2': Blowfish hash
305+
$exploded = explode('$', $ldap_bytes, 4);
306+
$ldap_salt = '$' . $exploded[1] . '$' . $exploded[2] . '$' . substr($exploded[3], 0, 22);
307+
}
308+
elseif ( substr($ldap_bytes, 0, 3) == '$5$' ) {
309+
# if $ldap_password starts with '$5$': sha256 sum
310+
$exploded = explode('$', $ldap_bytes, 5);
311+
if ( substr($exploded[2], 0, 7) == 'rounds=' ) {
312+
$ldap_salt = '$5$' . $exploded[2] . '$' . $exploded[3] . '$';
313+
} else {
314+
$ldap_salt = '$5$' . $exploded[2] . '$';
315+
}
316+
}
317+
# if $ldap_password starts with '$6$': sha512 sum, salt is 16 char long
318+
elseif ( substr($ldap_bytes, 0, 3) == '$6$' ) {
319+
# if $ldap_password starts with '$6$': sha512 sum
320+
$exploded = explode('$', $ldap_bytes, 5);
321+
if ( substr($exploded[2], 0, 7) == 'rounds=' ) {
322+
$ldap_salt = '$6$' . $exploded[2] . '$' . $exploded[3] . '$';
323+
} else {
324+
$ldap_salt = '$6$' . $exploded[2] . '$';
325+
}
326+
}
327+
else {
328+
# if $ldap password doesn't start with '$': DES hash, salt = two first letters
329+
$ldap_salt = substr($ldap_bytes, 0, 2);
330+
}
331+
$old_password_hash = make_crypt_password_with_salt($old_password, $ldap_salt);
332+
}
333+
}
334+
else {
335+
# Could not get the hash type, empty value for old_password_hash so that it fails
336+
$old_password_hash = "";
337+
}
338+
return $old_password_hash;
339+
}
241340

242341
# Change password
243342
# @return result code
@@ -267,7 +366,7 @@ function change_password( $ldap, $dn, $password, $ad_mode, $ad_options, $samba_m
267366
if ( isset($userpassword) ) {
268367
if ( preg_match( '/^\{(\w+)\}/', $userpassword[0], $matches ) ) {
269368
$hash = strtoupper($matches[1]);
270-
}
369+
}
271370
}
272371
}
273372
}

pages/change.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,49 @@
152152
error_log("LDAP - Bind user password needs to be changed");
153153
$errno = 0;
154154
}
155-
if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $ad_options['change_expired_password'] ) {
155+
if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $change_expired_password ) {
156156
error_log("LDAP - Bind user password is expired");
157157
$errno = 0;
158158
}
159159
unset($extended_error);
160160
}
161+
} elseif ( ! $ad_mode && ($errno == 49) && $change_expired_password && ( $who_change_password == "manager" ) ) {
162+
# Try to check password value without binding
163+
# First rebind as Manager, it will be needed (since our user can't log in)
164+
$bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw);
165+
166+
# Check that the password is not locked (due to several attempts for example)
167+
$search_password_info = ldap_read( $ldap, $userdn, "(objectClass=*)", array("pwdAccountLockedTime", "userPassword") );
168+
169+
# Read LDAP password value
170+
#$search_password_info = ldap_read( $ldap, $userdn, "(objectClass=*)", array("userPassword") );
171+
if ( $search_password_info ) {
172+
$first_entry = ldap_first_entry($ldap, $search_password_info);
173+
$pwd_locked = ldap_get_values($ldap, $first_entry, "pwdAccountLockedTime");
174+
if ( isset($pwd_locked) ) {
175+
#LDAP attribute pwdAccountLockedTime is still set after our earlier ldap_bind attempt: user is locked out
176+
error_log("LDAP - denying password change - user ". $login ." is locked out");
177+
}
178+
else {
179+
$password_val = ldap_get_values($ldap, $first_entry, "userPassword");
180+
if ( isset($password_val) ) {
181+
$old_pwd_hashed = hash_old_password($password_val[0], $oldpassword);
182+
183+
if ( hash_equals($password_val[0], $old_pwd_hashed)) {
184+
# Hashes match: user can't log in for some reason (expired, most likely) but we should allow a password change
185+
error_log("LDAP - Allowing password change for user " . $login . "despite bind unsuccessful");
186+
$errno = 0;
187+
}
188+
else {
189+
error_log("LDAP - denying password change - passwords don't match");
190+
}
191+
}
192+
}
193+
} else {
194+
error_log("Denying password change - user does not exist");
195+
}
161196
}
197+
162198
if ( $errno ) {
163199
$result = "badcredentials";
164200
error_log("LDAP - Bind user error $errno (".ldap_error($ldap).")");

tests/HashOldPasswordTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../lib/vendor/defuse-crypto.phar';
4+
5+
class HashOldPasswordTest extends \PHPUnit_Framework_TestCase
6+
{
7+
/**
8+
* Test hash_old_password function
9+
*/
10+
public function testHashOldPassword()
11+
{
12+
13+
# Load functions
14+
require_once("lib/functions.inc.php");
15+
16+
$candidate_password = "hello_S3lfServ1ce";
17+
18+
# Test SSHA hashed password
19+
$ldap_password = '{SSHA}/oFcrDRwH5K6Gv3ng1D9I+m32ftIpGivlUB6jw=='; # Obtained via Apache Directory Studio
20+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
21+
$this->assertEquals($ldap_password, $candidate_hashed);
22+
23+
# Test SHA1 hashed password
24+
$ldap_password = '{SHA}cxMgpYJFOW1BPAkO4tbAHXwz9Z0='; # Obtained via Apache Directory Studio
25+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
26+
$this->assertEquals($ldap_password, $candidate_hashed);
27+
28+
# Test SHA512 hashed password
29+
$ldap_password = '{SHA512}Eg8xKRGYinaxsrM4edoROEUcQoqFRDI3Slcg5wWig80g1VpYFx+DQVqA++TN2B44XDoSMjRkwCOcgrRS+wsS1g=='; # Obtained via Apache Directory Studio
30+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
31+
$this->assertEquals($ldap_password, $candidate_hashed);
32+
33+
# Test SMD5 hashed password
34+
$ldap_password = '{SMD5}xT4bI4kPLtAmUYjmRqT2t0H+PTHjFY4C'; # Obtained via Apache Directory Studio
35+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
36+
$this->assertEquals($ldap_password, $candidate_hashed);
37+
38+
# Test MD5 hashed password
39+
$ldap_password = '{MD5}iNCvX+AiYnAeZoORPjwOuw=='; # Obtained via Apache Directory Studio
40+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
41+
$this->assertEquals($ldap_password, $candidate_hashed);
42+
43+
44+
# Test CRYPT hashed password - standard DES
45+
$ldap_password = '{CRYPT}WCFar/a24WRlk'; # Obtained via Apache Directory Studio
46+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
47+
$this->assertEquals($ldap_password, $candidate_hashed);
48+
49+
# Test CRYPT hashed password - extended DES
50+
$ldap_password = '{CRYPT}_6uVCtbvym/90wKTIrKY';
51+
# Generated using random 8-chars string from https://www.random.org/strings/ and https://quickhash.com/ Extended DES algorithm
52+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
53+
$this->assertEquals($ldap_password, $candidate_hashed);
54+
55+
# Test CRYPT hashed password - md5 hash
56+
$ldap_password = '{CRYPT}$1$akUYgSg4$WQceCqgPDPgefRr/Zutj70'; # Obtained via Apache Directory Studio
57+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
58+
$this->assertEquals($ldap_password, $candidate_hashed);
59+
60+
# Test CRYPT hashed password - Blowfish hash with 2a
61+
$ldap_password = '{CRYPT}$2a$06$1ZJSTvTH9xju5zGUoXuMIuDSFw57SLYopcqamCKQYeUgfeUbndMNW';
62+
# Above password generated using random string from https://www.random.org/strings/ and http://php.fnlist.com/crypt_hash/crypt online calculation
63+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
64+
$this->assertEquals($ldap_password, $candidate_hashed);
65+
66+
# Test CRYPT hashed password - Blowfish hash with 2y
67+
$ldap_password = '{CRYPT}$2y$06$1ZJSTvTH9xju5zGUoXuMIuDSFw57SLYopcqamCKQYeUgfeUbndMNW';
68+
# Above password generated using random string from https://www.random.org/strings/ and http://php.fnlist.com/crypt_hash/crypt online calculation
69+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
70+
$this->assertEquals($ldap_password, $candidate_hashed);
71+
72+
# Test CRYPT hashed password - sha256 sum
73+
$ldap_password = '{CRYPT}$5$4mxifKQN$PRzssKp/vzWdcN3QNXSOuutw7vbS6pR4hgtp9AboH83'; # Obtained via Apache Directory Studio
74+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
75+
$this->assertEquals($ldap_password, $candidate_hashed);
76+
77+
# Test CRYPT hashed password - sha256 sum with custom rounds value
78+
$ldap_password = '{CRYPT}$5$rounds=12345$eKNy1/RDIlJ$gehYUSLkKuhof/Sbp.N7XHdAe/U6hi1G9ZrdEgDbPx8';
79+
#Above pass obtained via linux command : echo "username:hello_S3lfServ1ce" | chpasswd -c SHA256 -s 12345 and copying /etc/shadow value
80+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
81+
$this->assertEquals($ldap_password, $candidate_hashed);
82+
83+
# Test CRYPT hashed password - sha512 sum
84+
$ldap_password = '{CRYPT}$6$T87Jnfqj$6gdQfurLrxU0E6TxzdiklrT1QTDPFTO06vIkDBN2Frx4WMJNr.uMWUm4basMbu8D7mEFVFxXkEED72DNPzoYH.'; # Obtained via Apache Directory Studio
85+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
86+
$this->assertEquals($ldap_password, $candidate_hashed);
87+
88+
# Test CRYPT hashed password - sha512 sum with custom rounds value
89+
$ldap_password = '{CRYPT}$6$rounds=12345$mgoMm/FzDwtJjkr$j0zvqlK9Tn/iTpEgnKFMCW8us1x.ex54qpzljcCXZfVJL2FHvNg7t2fjdCfqKb7HNMvRC838XdJdiyUmaIkzs/';
90+
#Above pass obtained via linux command : echo "username:hello_S3lfServ1ce" | chpasswd -c SHA512 -s 12345 and copying /etc/shadow value
91+
$candidate_hashed = hash_old_password($ldap_password, $candidate_password);
92+
$this->assertEquals($ldap_password, $candidate_hashed);
93+
}
94+
}
95+

0 commit comments

Comments
 (0)