Scott M. Mcdermott

UNIX Systems & Network Administrator
available for contract or salaried positions

chpass.php

<?php include("nocache.php"); ?>

<html>

<head>
<title>
PASSWORD CHANGE PAGE
</title>
</head>

<body>

<link
        rel     = stylesheet
        type    = text/css
        href    = /style/mystyle.css
>

<h2>
Password change page for NIS, LDAP and NT accounts
</h2>

<p>

<?php

error_reporting(E_ALL);

define ("ALL",  1);
define ("NIS",  2);
define ("LDAP", 3);
define ("NT",   4);

define ("MAX_NIS_PASSLEN",      8);
define ("MAX_LDAP_PASSLEN",     12);
define ("MAX_NT_PASSLEN",       12);
define ("MAX_MAX_PASSLEN",
        max(array(MAX_NIS_PASSLEN, MAX_LDAP_PASSLEN, MAX_NT_PASSLEN)));

/* there should be three files, .{pw[di],hwm}; passed to PWOpen() */
define ("CRACKLIB_PREFIX",      "/usr/lib/cracklib_dict");


$accounts = array(

        NIS => array(
                "name"          => "NIS",               /* used in text */
                "what"          => "Change " .
                                   "NIS " .
                                   "(Network Information Services)" .
                                   "account",
                                                        /* used in header box */
                "old"           => "n_old",             /* oldpass POST var */
                "new1"          => "n_new",             /* 1st newpass POST */
                "new2"          => "n_new_ver",         /* 2nd newpass POST */
                "validate_func" => "nis_validate",      /* validate callback */
                "chpasswd_func" => "nis_chpasswd",      /* change callback */

                "description"   => "Email account login.  Also used to " .
                                   "access all UNIX-based services, such " .
                                   "as NFS exports."
        ),

        LDAP => array(
                "name"          => "LDAP",
                "what"          => "Change " .
                                   "LDAP " .
                                   "(Lightweight Directory Access Protocol) " .
                                   "account",
                "old"           => "l_old",
                "new1"          => "l_new",
                "new2"          => "l_new_ver",
                "validate_func" => "ldap_validate",
                "chpasswd_func" => "ldap_chpasswd",

                "description"   => "Used for Intranet portal and related " .
                                   "functions, such as internal mailing " .
                                   "lists and email aliases"
        ),

        NT => array(
                "name"          => "Windows NT",
                "what"          => "Change " .
                                   "NT " .
                                   "(Windows NT domain) " .
                                   "account",
                "old"           => "w_old",
                "new1"          => "w_new",
                "new2"          => "w_new_ver",
                "validate_func" => "nt_validate",
                "chpasswd_func" => "nt_chpasswd",

                "description"   => "Login required for Windows printing, " .
                                   "networked files, home directory and VPN."
        ),

        ALL => array(
                "name"          => "(All accounts)",
                "what"          => "Change All accounts at once",
                "old"           => "a_old",
                "new1"          => "a_new",
                "new2"          => "a_new_ver",
                "validate_func" => "all_validate",
                "chpasswd_func" => "all_chpasswd",

                "description"   => "Changes all three accounts at once. " .
                                   "Requires that all old passwords are the " .
                                   "same.  If your accounts use more than " .
                                   "one password you will be notified and " .
                                   "must fill out each individual account " .
                                   "below.<p><em>Using any of these fields " .
                                   "causes the individual account fields " .
                                   "below to be ignored. </em>",

                "user"          => " "  /* must set blank for ALL only */
        )
);

/* each element is a string depicting a problem with the input */
$errors = array();

$ld_host = "ldap.yourcompany.com";
$ld_base = "o=company,c=us";
$ld_port = 389;

if (!($lc = ldap_connect($ld_host, $ld_port)))
        _die("unable to connect to LDAP server");
if (!($lb = ldap_bind($lc)))
        _die("anonymous bind to LDAP server failed");

$accounts[LDAP]["user"] = $HTTP_SERVER_VARS["REMOTE_USER"];
$accounts[NT]  ["user"] = $accounts[LDAP]["user"];
$accounts[NIS] ["user"] = ldap_get_attr($accounts[LDAP]["user"], "pop3user");
?>

<p>
This page will let you change your Company passwords.

<p>
The following accounts can be changed.  Each account has fields next to it
which you can use to enter a new password for that account, and a field in
which you must enter your old account password to authorize the change.  <em>
If you leave any of the fields blank for a given account, the remaining fields
will be ignored and the password for it will remain unchanged. </em>

<p>
Select the `submit' button once you have entered new and old passwords for all
the accounts you wish changed.

<p>

<?php

foreach (array(ALL, NIS, LDAP, NT) as $which) {
        $account = &$accounts[$which];
        sane_assign($account["oldpass"], $HTTP_POST_VARS[$account["old"]]);
        sane_assign($account["newpass1"], $HTTP_POST_VARS[$account["new1"]]);
        sane_assign($account["newpass2"], $HTTP_POST_VARS[$account["new2"]]);
}

if (isset($HTTP_POST_VARS["submit"])) {

        foreach (array(ALL, NIS, LDAP, NT) as $which) {

                $old = &$accounts[$which]["oldpass"];
                $new1 = &$accounts[$which]["newpass1"];
                $new2 = &$accounts[$which]["newpass2"];

                if (!$old || !$new1 || !$new2)
                        continue;

                if ($which == ALL)
                        $using_all = TRUE;

                if (!verify($new1, $new2, $which) ||
                    !sanitize($new1, $which)) {
                        $new1 = $new2 = NULL;
                }
                if (!validate($accounts[$which]["user"], $old, $which)) {
                        $old = NULL;
                }
                if ($which == ALL)
                        break;
        }
}
if (isset($errors[0])) {
        echo("<br><hr>");
        display_errors($errors);
}

?>

<hr>

<p>
<form method=post action=<?php echo(basename(__FILE__)) ?>>

<link
        rel     = stylesheet
        type    = text/css
        href    = /share/style/nicetable.css
>
<table
        border          = 0
        cellspacing     = 2
        cellpadding     = 10
        width           = 98%
        align           = center
>
<?php

foreach (array(ALL, LDAP, NIS, NT) as $which) {

        $account = &$accounts[$which];
?>
        <tr>
                <td class=bigtablehead colspan=3>
                <?php echo($account["what"])?>
                <strong>
                <em> <?php echo($account["user"])?> </em>
                </strong>
                </td>
        <tr>
                <td class=tablelabelcell colspan=3>
                <?php echo($account["description"])?>
                </td>
        <tr>
                <td class=tablecell> old password </td>
                <td class=tablecell> new password </td>
                <td class=tablecell> confirm new password </td>
        <tr>
<?php
        foreach (array(array("old", "oldpass"),
                       array("new1", "newpass1"),
                       array("new2", "newpass2")) as $inputs) {
?>
                <td class=tablealtcell>
                <input
                        type            = password
                        size            = <?php echo(MAX_MAX_PASSLEN . "\n")?>
                        maxlength       = <?php echo(MAX_MAX_PASSLEN . "\n")?>
                        name            = "<?php echo($account[$inputs[0]])?>"
                        value           = "<?php echo($account[$inputs[1]])?>"
                >
                </td>
<?php
        }
?>
        <tr><td colspan=3></td></tr>
<?php
}
?>

</table>
<br>
<p>
<hr>
<p>
<input type=submit name=submit value=submit>
</form>

</html>

<?php
ldap_close($lc);
?>

<!-- end script -->

<?php

function
_die($death_string)
{
        $admin_mail = "admin@yourcompany.com";
        die("$death_string, " . "please notify " . $admin_mail);
}

function
display_errors($errors)
{
        global $errors;

        echo("<br><p><strong>\n" .
             "The following errors were found in your form data:\n".
             "</strong><ul>\n");

        foreach ($errors as $current_error)
                echo("<li><em>$current_error</em>\n");

        echo("</ul>\n" .
             "<p>\n" .
             "These errors must be corrected, and the form re-submitted, " .
             "in order for your password changes to be processed.  " .
             "For assistance selecting a password, please read " .
             "<a href=/XXX/TODO/FIXME> this document</a>.  If you believe " .
             "these errors are themselves incorrect, please report the " .
             "error to <a href=admin@yourcompany.com> " .
             "admin@yourcompany.com</a>\n");
}
?>

<?php

/*
 * Assigns val to var iff val passes tests which check it for characters or
 * length which could be used maliciously.  Otherwise assigns an empty string.
 */
function
sane_assign(&$var, &$val)
{
        global $errors;

        if (strlen($val) > MAX_MAX_PASSLEN) {
                array_push($errors,
                           "One of the form fields you input exceeds the " .
                           "absolute maximum of " . MAX_MAX_PASSLEN . " " .
                           "characters; clearing its input field to be safe. ");
                $val = NULL;
        }
        if (ereg("[\"'` ]+", $val)) {
                array_push($errors,
                           "One of your passwords uses characters which " .
                           "could be used maliciously (those from the set " .
                           "(\", ', `, space)). Clearing its input fields " .
                           "to be safe. ");
                $val = NULL;
        }

        $var = $val;
}

/*
 * Gets the named attribute for the given uid and dies if
 * 
 *      - there is more than one DN that is returned for the UID
 *      - there are no attributes of the named type matched
 *
 * only returns the first value if
 *
 *      - there is more than one attribute in the returned single entry
 *      - there is more than one value for the single attribute in the single
 *        entry
 *
 * normally these assumptions work fine, but this is an area that should be
 * made more robust XXX TODO
 */
function
ldap_get_attr($uid, $attr)
{
        global $ld_base;
        global $lc;

        if (!($ls = ldap_search($lc, $ld_base, "(uid=$uid)", array("$attr"))))
                _die("your LDAP entry failed a search for the $attr attribute");
        if (($ln = ldap_count_entries($lc, $ls)) != 1)
                _die("your LDAP UID matched an invalid " . $ln . " " . "DNs");

        $le = ldap_first_entry($lc, $ls);
        $lf = ldap_first_attribute($lc, $le, $lr);

        if (!($value = ldap_get_values($lc, $le, $lf)))
                _die("unable to get first attribute value for NIS search");

        return (array_shift($value));
}

function
verify_crypt_string($crypt_pw, $plain_pw)
{
        $salt = substr($crypt_pw, 0, 2);
        $crypted = crypt($plain_pw, $salt);

        if (!strcmp($crypt_pw, $crypted))
                return (TRUE);  // verified
        else
                return (FALSE); // incorrect
}

function
make_badpass($whichbad)
{
        global $errors;
        global $using_all;
        global $accounts;

        $badpass = "old password for $whichbad ";
        if ($using_all) $badpass .= " (from {$accounts[ALL]['name']}) ";
        $badpass .= "is incorrect.";

        return $badpass;
}

/* verify that string s1 == s2 */
function
verify(&$s1, &$s2, $which)
{
        global $errors;
        global $accounts;

        if (strcmp($s1, $s2)) {
                array_push($errors, "new passwords given for " .
                           $accounts[$which]["name"] .
                           " account do not match");
                $s1 = NULL;
                return (FALSE);
        }
        return $s1;
}

?>

<?php

# authenticate
function
validate($u, $p, $which)
{
        global $accounts;

        /* if the field is blank, the user doesn't want to change this
         * password */
        if ($p == NULL)
                return (FALSE);

        return($accounts[$which]["validate_func"]($u, $p));
}

/*
 * check desired new password for stupidity before accepting (passes through
 * libcrack)
 */
function
sanitize($p, $which)
{
        global $errors;
        global $accounts;

        if ($p == NULL)
                return (FALSE);

        $dict = crack_opendict(CRACKLIB_PREFIX);
        $ret = crack_check($dict, $p);
        $ret = crack_getlastmessage();
        crack_closedict($dict);

        if (!strcmp($ret, "strong password"))
                return (TRUE);
        else {
                array_push($errors, "the new password you supplied for your " .
                           $accounts[$which]["name"] .
                           " account is unacceptable (" .  $ret . ")");
                return (FALSE);
        }
}

function
nis_validate($u, $p)
{
        global $errors;

        if (strlen($p) > MAX_NIS_PASSLEN) {
                array_push($errors,
                           "NIS passwords are not significant after 8 chars");
                return (NULL);
        }

        if (!($nis_domain = yp_get_default_domain()))
                _die("couldn't get default NIS domain");
        if (!($nis_match = yp_match($nis_domain, "passwd.byname", $u)))
                _die("couldn't match NIS username $u in passwd.byname map");

        /* XXX TODO there's got to be a way to combine these two; s/);$/)[1];/
         * does not work */
        $crypt_string = explode(":", $nis_match, 3);
        $crypt_string = $crypt_string[1];

        if (!verify_crypt_string($crypt_string, $p)) {
                array_push($errors, make_badpass("NIS"));
                return (NULL);
        }

        return($p);
}

/*
 * This routine validates the input LDAP password $p against the userPassword
 * attribute in the database for the LDAP user $u.  The attributes contain, at
 * offset 0, a brace-enclosed string which is either `crypt' or `sha' (and in
 * both cases), describing the digest algorithm used to store the password.
 * We first determine the algorithm used, chop off its identifier, and then
 * test a digest on the input password with the stored digest value, wrapped in
 * base64 encoding (this is how the LDAP server keeps it).  If they are the
 * same we know the password is correct.  We depend on crypt() and mdhash()
 * here so php needs to be built with libmd or this will fail.
 *
 * $ls; earch result
 * $ln; ents
 * $le; entry
 * $lr; entry offset reference
 */
function
ldap_validate($u, $p)
{
        global $ld_base;
        global $lc;
        global $accounts;
        global $errors;

        $lname = $accounts[LDAP]["user"];

        $passvalue = ldap_get_attr($lname, "userPassword");
        if (!eregi("^\{(crypt|sha)}(.*$)", $passvalue, $passarr))
                _die("invalid LDAP password format encountered");

        switch (strtolower($passarr[1])) {

                case "crypt":
                        if (!verify_crypt_string($passarr[2], $p)) {
                                $err = make_badpass("LDAP-{$passarr[1]}");
                                array_push($errors, $err);
                                return(NULL);
                        }
                        break;

                case "sha":

                        $hash_result = mhash(MHASH_SHA1, $p);
                        if ($passarr[2] != base64_encode($hash_result)) {
                                $err = make_badpass("LDAP-{$passarr[1]}");
                                array_push($errors, $err);
                                return(NULL);
                        }
                        break;

                default:
                        _die("default switch reached in ldap_validate()");
        }

        return($p);
}

function
nt_validate($u, $p)
{
        global $errors;

        $user = escapeshellcmd($u);
        $pass = escapeshellcmd($p);

        # in process list but we control this host and no user logins
        system("/bin/bash -c 'echo -e \"$pass\\n$pass\\n$pass\" | " .
               "/usr/local/bin/smbpasswd -U $user -r ro1-pdc -s &>/dev/null'",
               $retval);
        if ($retval == 0)
                return TRUE;
        else {
                array_push($errors, make_badpass("NT"));
                return FALSE;
        }
}

/*
 * Calls all the validation functions with the oldpass and newpass given.  Read
 * description in $accounts[ALL] instantiation.
 */
function
all_validate($u, $p)
{
        global $accounts;

        $ret = TRUE;
        $keys = array_keys($accounts);
        $keys = array_slice($keys, 0, -1); // don't call ourself

        foreach ($keys as $key) {
                $u = &$accounts[$key]["user"];
                if (!($accounts[$key]["validate_func"]($u, $p)))
                        $ret = FALSE;
        }

        if (!$ret)
                return FALSE;
        else
                return TRUE;
}

?>