ltb-project / self-service-password

Web interface to change and reset password in an LDAP directory
https://self-service-password.readthedocs.io/en/latest/
GNU General Public License v3.0
1.17k stars 325 forks source link

Changing the password when the flag "User must change password at next logon" is setted #414

Open EuGras-itools opened 4 years ago

EuGras-itools commented 4 years ago

Hello, I have a problem when trying to change the password of a AD user for which the flag "User must change password at next logon" is setted. SSP frontend returns error "Password was refused by the LDAP directory". According to the logs and to the comment from another issue (https://github.com/ltb-project/self-service-password/issues/216#issuecomment-399009554) rebind under the manager account probably does not happen. Option $who_change_password = "user"; is required for us because password group policies works only with it. My environment: Windows server 2019 with configured LDAPS.

Part of SSP config:

$ldap_url = "ldaps://HIDDEN:636";
$ldap_binddn = "cn=manager,cn=HIDDEN,dc=HIDDEN,dc=HIDDEN";
$ldap_bindpw = "HIDDEN";
$ldap_base = "dc=HIDDEN,dc=HIDDEN";
$ldap_login_attribute = "sAMAccountName";
$who_change_password = "user";
$ad_mode = true;
$ad_options['change_expired_password'] = true;

Part of /var/log/apache2/error.log:

error.log ``` ** ld 0x55ef5edb43c0 Connections: * host:HIDDEN port: 636 (default) refcnt: 2 status: Connected last used: Wed Aug 26 15:41:08 2020 ** ld 0x55ef5edb43c0 Outstanding Requests: * msgid 3, origid 3, status InProgress outstanding referrals 0, parent count 0 ld 0x55ef5edb43c0 request count 1 (abandoned 0) ** ld 0x55ef5edb43c0 Response Queue: Empty ld 0x55ef5edb43c0 response count 0 ldap_chkResponseList ld 0x55ef5edb43c0 msgid 3 all 1 ldap_chkResponseList returns ld 0x55ef5edb43c0 NULL ldap_int_select read1msg: ld 0x55ef5edb43c0 msgid 3 all 1 read1msg: ld 0x55ef5edb43c0 msgid 3 message type bind read1msg: ld 0x55ef5edb43c0 0 new referrals read1msg: mark request completed, ld 0x55ef5edb43c0 msgid 3 request done: ld 0x55ef5edb43c0 msgid 3 res_errno: 49, res_error: <80090308: LdapErr: DSID-0C09041C, comment: AcceptSecurityContext error, data 773, v4563>, res_matched: <> ldap_free_request (origid 3, msgid 3) ldap_parse_result ldap_msgfree ldap_err2string ldap_err2string ldap_err2string ldap_modify_ext ldap_send_initial_request ldap_send_server_request ldap_result ld 0x55ef5edb43c0 msgid 4 wait4msg ld 0x55ef5edb43c0 msgid 4 (infinite timeout) wait4msg continue ld 0x55ef5edb43c0 msgid 4 all 1 ** ld 0x55ef5edb43c0 Connections: * host: HIDDEN port: 636 (default) refcnt: 2 status: Connected last used: Wed Aug 26 15:41:08 2020 ** ld 0x55ef5edb43c0 Outstanding Requests: * msgid 4, origid 4, status InProgress outstanding referrals 0, parent count 0 ld 0x55ef5edb43c0 request count 1 (abandoned 0) ** ld 0x55ef5edb43c0 Response Queue: Empty ld 0x55ef5edb43c0 response count 0 ldap_chkResponseList ld 0x55ef5edb43c0 msgid 4 all 1 ldap_chkResponseList returns ld 0x55ef5edb43c0 NULL ldap_int_select read1msg: ld 0x55ef5edb43c0 msgid 4 all 1 read1msg: ld 0x55ef5edb43c0 msgid 4 message type modify read1msg: ld 0x55ef5edb43c0 0 new referrals read1msg: mark request completed, ld 0x55ef5edb43c0 msgid 4 request done: ld 0x55ef5edb43c0 msgid 4 res_errno: 1, res_error: <000004DC: LdapErr: DSID-0C090F45, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563>, res_matched: <> ldap_free_request (origid 4, msgid 4) ldap_parse_result ldap_msgfree ldap_err2string ldap_err2string ldap_msgfree ldap_free_connection 1 1 ldap_send_unbind ldap_free_connection: actually freed ```

Part of /var/log/apache2/ssp_error.log:

ssp_error.log ``` [Wed Aug 26 15:41:08.683323 2020] [php7:warn] [pid 15173] [client HIDDEN:54322] PHP Warning: ldap_bind(): Unable to bind to server: Invalid credentials in /usr/share/self-service-password/pages/change.php on line 131, referer: https:/HIDDEN/index.php [Wed Aug 26 15:41:08.683350 2020] [php7:notice] [pid 15173] [client HIDDEN:54322] LDAP - Bind user error 49 (Invalid credentials), referer: https://HIDDEN/index.php [Wed Aug 26 15:41:08.683359 2020] [php7:notice] [pid 15173] [client HIDDEN:54322] LDAP - Bind user extended_error 80090308: LdapErr: DSID-0C09041C, comment: AcceptSecurityContext error, data 773, v4563 (Invalid credentials), referer: https:/HIDDEN/index.php [Wed Aug 26 15:41:08.683364 2020] [php7:notice] [pid 15173] [client HIDDEN:54322] LDAP - Bind user password needs to be changed, referer: https://HIDDEN/index.php [Wed Aug 26 15:41:08.684127 2020] [php7:warn] [pid 15173] [client HIDDEN:54322] PHP Warning: ldap_modify_batch(): Batch Modify: Operations error in /usr/share/self-service-password/lib/functions.inc.php on line 396, referer: https://HIDDEN/index.php [Wed Aug 26 15:41:08.684141 2020] [php7:notice] [pid 15173] [client HIDDEN:54322] LDAP - Modify password error 1 (Operations error), referer: https://HIDDEN/index.php ```
coudot commented 4 years ago

Hello,

our code is:

        if ( ($errno == 49) && $ad_mode ) {
            if ( ldap_get_option($ldap, 0x0032, $extended_error) ) {
                error_log("LDAP - Bind user extended_error $extended_error  (".ldap_error($ldap).")");
                $extended_error = explode(', ', $extended_error);
                if ( strpos($extended_error[2], '773') or strpos($extended_error[0], 'NT_STATUS_PASSWORD_MUST_CHANGE') ) {
                    error_log("LDAP - Bind user password needs to be changed");
                    $result = "";
                }
                if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $ad_options['change_expired_password'] ) {
                    error_log("LDAP - Bind user password is expired");
                    $result = "";
                }
                unset($extended_error);
            }
        }
    }
    if ( $result === "" )  {

        # Rebind as Manager if needed
        if ( $who_change_password == "manager" ) {
            $bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw);
        }

    }}}}}

Means we only rebind as manager if this is set in $who_change_password.

In your case, the $who_change_password is set to user, so we don't rebind, and then the modification is not accepted by AD. I don't see any obvious solution to this.

EuGras-itools commented 4 years ago

Thanks for quick reply .If the "User must change password at next logon" flag is not set for the user, but the $who_change_password = "manager" option is set, the password will be changed through the administrative reset by manager or through the change by user?

coudot commented 4 years ago

It will be changed through the manager account

EuGras-itools commented 4 years ago

The problem was solved by modifying the php code of the change.php file from line # 138 and below:

        if ( ($errno == 49) && $ad_mode ) {
            if ( ldap_get_option($ldap, 0x0032, $extended_error) ) {
                error_log("LDAP - Bind user extended_error $extended_error  (".ldap_error($ldap).")");
                $extended_error = explode(', ', $extended_error);
                if ( strpos($extended_error[2], '773') or strpos($extended_error[0], 'NT_STATUS_PASSWORD_MUST_CHANGE') ) {
                    error_log("LDAP - Bind user password needs to be changed");
                    $result = "need_manager";
                }
                if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $ad_options['change_expired_password'] ) {
                    error_log("LDAP - Bind user password is expired");
                    $result = "need_manager";
                }
                unset($extended_error);
            }
        }
    }
    if ( $result === "need_manager" )  {

        # Rebind as Manager if needed
        $who_change_password = "manager";
        $bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw);
        $result = "";
    }}}}}
coudot commented 4 years ago

Were you also able to test an account with an expired password?

And what about the fact the group policy does not apply when password is changed by user? Is it ok for you to bypass AD policy if the user must reset its password?

EuGras-itools commented 4 years ago

Yes this is a dirty hack. And when the flag "User must change password at next logon" is setted password group policy wouldn't apply when password changed, because the password is changed by the manager. But in other cases this should work. These group policies are important to us:

EuGras-itools commented 4 years ago

not tested yet

Were you also able to test an account with an expired password?

EuGras-itools commented 4 years ago

I modified the "change.php" file to make group policies work:

When changing the password, an intermediate step of setting a temporary password is used. This temporarily modifies the "UserAccountControl" and "pwdLastSet" attributes.

change.php ``` 0 ) { $mail = $mailValues[0]; } } # Get user UserAccountControl attribute value if ( $ad_mode ) { $uacValueArray = ldap_get_values($ldap, $entry, 'UserAccountControl'); $uacValue = $uacValueArray[0]; } # Check objectClass to allow samba and shadow updates $ocValues = ldap_get_values($ldap, $entry, 'objectClass'); if ( !in_array( 'sambaSamAccount', $ocValues ) and !in_array( 'sambaSAMAccount', $ocValues ) ) { $samba_mode = false; } if ( !in_array( 'shadowAccount', $ocValues ) ) { $shadow_options['update_shadowLastChange'] = false; $shadow_options['update_shadowExpire'] = false; } # Bind with old password $bind = ldap_bind($ldap, $userdn, $oldpassword); if ( !$bind ) { $result = "badcredentials"; $errno = ldap_errno($ldap); if ( $errno ) { error_log("LDAP - Bind user error $errno (".ldap_error($ldap).")"); } if ( ($errno == 49) && $ad_mode ) { if ( ldap_get_option($ldap, 0x0032, $extended_error) ) { error_log("LDAP - Bind user extended_error $extended_error (".ldap_error($ldap).")"); $extended_error = explode(', ', $extended_error); if ( strpos($extended_error[2], '773') or strpos($extended_error[0], 'NT_STATUS_PASSWORD_MUST_CHANGE') ) { error_log("LDAP - Bind user password needs to be changed"); $flag = "need_manager"; $result = ""; } if ( ( strpos($extended_error[2], '532') or strpos($extended_error[0], 'NT_STATUS_ACCOUNT_EXPIRED') ) and $ad_options['change_expired_password'] ) { error_log("LDAP - Bind user password is expired"); $flag = "need_manager"; $result = ""; } unset($extended_error); } } } # Rebind as Manager if needed, set the user password using a randomly generated temporary password if ( $flag === "need_manager" && $who_change_password === "user" ) { $tmp_password = base64_encode(random_bytes(10)); $tmp_who_change_password = "manager"; # Bind as manager $bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw); # Check password strength $result = check_password_strength( $newpassword, $oldpassword, $pwd_policy_config, $login ); # Set temp password for user and temporary set user's attributes for working of group policies next time setting new password if ( $result === "" ) { $result = change_password($ldap, $userdn, $tmp_password, $ad_mode, $ad_options, $samba_mode, $samba_options, $shadow_options, $hash, $hash_options, $tmp_who_change_password, $oldpassword); $userdata["UserAccountControl"] = 66048; $userdata["pwdLastSet"] = 0; $replace = ldap_mod_replace($ldap, $userdn, $userdata); $oldpassword2 = $oldpassword; $oldpassword = $tmp_password; $bind = ldap_bind($ldap, $userdn, $oldpassword); $result = ""; } }}}}} } #============================================================================== # Check password strength #============================================================================== if ( $result === "" ) { $result = check_password_strength( $newpassword, $oldpassword, $pwd_policy_config, $login ); } #============================================================================== # Change password #============================================================================== if ( $result === "" ) { $result = change_password($ldap, $userdn, $newpassword, $ad_mode, $ad_options, $samba_mode, $samba_options, $shadow_options, $hash, $hash_options, $who_change_password, $oldpassword); if ( $flag === "need_manager" && $who_change_password === "user" ) { $bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw); $userdata["UserAccountControl"] = $uacValue; $userdata["pwdLastSet"] = -1; $replace = ldap_mod_replace($ldap, $userdn, $userdata); } if ( $result !== "passwordchanged") { if ( $flag === "need_manager" && $who_change_password === "user" ) { $bind = ldap_bind($ldap, $ldap_binddn, $ldap_bindpw); $result = change_password($ldap, $userdn, $oldpassword2, $ad_mode, $ad_options, $samba_mode, $samba_options, $shadow_options, $hash, $hash_options, $tmp_who_change_password, $oldpassword); $userdata["pwdLastSet"] = 0; $replace = ldap_mod_replace($ldap, $userdn, $userdata); $result = "passworderror"; } else { $result = "passworderror"; } } if ( $result === "passwordchanged" && isset($posthook) ) { $command = escapeshellcmd($posthook).' '.escapeshellarg($login).' '.escapeshellarg($newpassword).' '.escapeshellarg($oldpassword); exec($command, $posthook_output, $posthook_return); } } #============================================================================== # HTML #============================================================================== if ( in_array($result, $obscure_failure_messages) ) { $result = "badcredentials"; } ?>

0 ) { ?>

"; echo " "; echo $messages["changehelp"]; echo "

"; if (isset($messages['changehelpextramessage'])) { echo "

" . $messages['changehelpextramessage'] . "

"; } if ( !$show_menu and ( $use_questions or $use_tokens or $use_sms or $change_sshkey ) ) { echo "

". $messages["changehelpreset"] . "

"; echo "
    "; if ( $use_questions ) { echo "
  • " . $messages["changehelpquestions"] ."
  • "; } if ( $use_tokens ) { echo "
  • " . $messages["changehelptoken"] ."
  • "; } if ( $use_sms ) { echo "
  • " . $messages["changehelpsms"] ."
  • "; } if ( $change_sshkey ) { echo "
  • " . $messages["changehelpsshkey"] . "
  • "; } echo "
"; } echo "
\n"; } ?>
" />
" />
" />
" />
$login, "mail" => $mail, "password" => $newpassword); if ( !send_mail($mailer, $mail, $mail_from, $mail_from_name, $messages["changesubject"], $messages["changemessage"].$mail_signature, $data) ) { error_log("Error while sending change email to $mail (user $login)"); } } if (isset($messages['passwordchangedextramessage'])) { echo "
"; echo "

" . $messages['passwordchangedextramessage'] . "

"; echo "
\n"; } } ?> ```
andrich48 commented 4 years ago

I have the same problem. I dont have an account with expired password for testing. I have an account with valid password but with flag "need to change password on text logon". I cannot use "manager" because its insecure :( But I have a clue - "sambapasswd" utility from "samba 4" suite does that successfully. It first tries to change the password, then after error 49 it tries to re-bind as anonymous and then does some magic and password is changed. Unfortunately I dont fully understand their C++ code to do the same magic in PHP :(

coudot commented 3 years ago

There is a feature request to allow changing an expired password #96

Could you tell us what is working and not working with current code?