Ubuntu – How to check the password entered is a valid password for this user

bash

The scenario:

In a bash script, I have to check if a password given by a user is a valid user password.

I.e suppose I have a user A with password PA.. In the script I asked user A to enter his password, So how to check if the string entered is really his password?…

Best Answer

Since you want to do this in a shell script, a couple of contributions in How to check password with Linux? (on Unix.SE, suggested by A.B.) are especially relevant:

To manually check if a string is really some user's password, you must hash it with the same hash algorithm as in the user's shadow entry, with the same salt as in the user's shadow entry. Then it can be compared with the password hash stored there.

I've written a complete, working script demonstrating how to do this.

  • If you name it chkpass, you can run chkpass user and it will read a line from standard input and check if it's user's password.
  • Install the whois Install whois package to obtain the mkpasswd utility on which this script depends.
  • This script must be run as root to succeed.
  • Before using this script or any part of it to do real work, please see Security Notes below.
#!/usr/bin/env bash

xcorrect=0 xwrong=1 enouser=2 enodata=3 esyntax=4 ehash=5  IFS=$
die() {
    printf '%s: %s\n' "$0" "$2" >&2
    exit $1
}
report() {
    if (($1 == xcorrect))
        then echo 'Correct password.'
        else echo 'Wrong password.'
    fi
    exit $1
}

(($# == 1)) || die $esyntax "Usage: $(basename "$0") <username>"
case "$(getent passwd "$1" | awk -F: '{print $2}')" in
    x)  ;;
    '') die $enouser "error: user '$1' not found";;
    *)  die $enodata "error: $1's password appears unshadowed!";;
esac

if [ -t 0 ]; then
    IFS= read -rsp "[$(basename "$0")] password for $1: " pass
    printf '\n'
else
    IFS= read -r pass
fi

set -f; ent=($(getent shadow "$1" | awk -F: '{print $2}')); set +f
case "${ent[1]}" in
    1) hashtype=md5;;   5) hashtype=sha-256;;   6) hashtype=sha-512;;
    '') case "${ent[0]}" in
            \*|!)   report $xwrong;;
            '')     die $enodata "error: no shadow entry (are you root?)";;
            *)      die $enodata 'error: failure parsing shadow entry';;
        esac;;
    *)  die $ehash "error: password hash type is unsupported";;
esac

if [[ "${ent[*]}" = "$(mkpasswd -sm $hashtype -S "${ent[2]}" <<<"$pass")" ]]
    then report $xcorrect
    else report $xwrong
fi

Security Notes

It might not be the right approach.

Whether or not an approach like this should be considered secure and otherwise appropriate depends on details about your use case that you haven't provided (as of this writing).

It has not been audited.

Although I've tried to exercise care while writing this script, it has not been properly audited for security vulnerabilities. It is intended as a demonstration, and would be "alpha" software if released as part of a project. Furthermore...

Another user who's "watching" may be able to discover the user's salt.

Due to limitations in how mkpasswd accepts salt data, this script contains a known security flaw, which you may or may not consider acceptable depending on use case. By default, users on Ubuntu and most other GNU/Linux systems can view information about processes run by other users (including root), including their command-line arguments. Neither the user's input nor the stored password hash is passed as a command-line argument to any external utility. But the salt, extracted from the shadow database, is given as a command-line argument to mkpasswd, since this is the only way that utility accepts a salt as input.

If

  • another user on the system, or
  • anyone who has the ability to make any user account (e.g., www-data) run their code, or
  • anyone who otherwise can view information about running processes (including by manually inspecting entries in /proc)

is able to check the command-line arguments to mkpasswd as it is run by this script, then they can obtain a copy of the the user's salt from the shadow database. They might have to be able to guess when that command is run, but that is sometimes achievable.

An attacker with your salt is not as bad as an attacker with your salt and hash, but it's not ideal. The salt doesn't provide enough information for someone to discover your password. But it does allow someone to generate rainbow tables or pre-computed dictionary hashes specific to that user on that system. This is initially worthless, but if your security is compromised at a later date and the full hash is obtained, it could then be cracked more quickly to obtain the user's password before they get a chance to change it.

Thus this security flaw is an exacerbating factor in a more complex attack scenario rather than a fully exploitable vulnerability. And you might consider the above situation far-fetched. But I am reluctant to recommend any method for general, real-world use that leaks any non-public data from /etc/shadow to a non-root user.

You can avoid this problem completely by:

  • writing part of your script in Perl or some other language that lets you call C functions, as shown in Gilles's answer to the related Unix.SE question, or
  • writing your whole script/program in such a language, rather than using bash. (Based on the way you've tagged the question, it appears you prefer to use bash.)

Be careful how you call this script.

If you allow an untrusted user to run this script as root or to run any process as root that calls this script, be careful. By changing the environment, they can make this script--or any script that runs as root--do anything. Unless you can prevent this from occurring, you must not allow users elevated privileges for running shell scripts.

See 10.4. Shell Scripting Languages (sh and csh Derivatives) in David A. Wheeler's Secure Programming for Linux and Unix HOWTO for more information on this. While his presentation focuses on setuid scripts, other mechanisms can fall prey to some of the same problems if they don't correctly sanitize the environment.

Other Notes

It supports reading hashes from the shadow database only.

Passwords must be shadowed for this script to work (i.e., their hashes should be in a separate /etc/shadow file that only root can read, not in /etc/passwd).

This should always be the case in Ubuntu. In any case, if needed the script can be trivially extended to read password hashes from passwd as well as shadow.

Keep IFS in mind when modifying this script.

I set IFS=$ at the beginning, since the three data in the hash field of a shadow entry are separated by $.

  • They also have a leading $, which is why the hash type and salt are "${ent[1]}" and "${ent[2]}" rather than "${ent[0]}" and "${ent[1]}", respectively.

The only places in this script where $IFS determines how the shell splits or combines words are

  • when these data are split into an array, by initializing it from the unquoted $( ) command substitution in:

    set -f; ent=($(getent shadow "$1" | awk -F: '{print $2}')); set +f
    
  • when the array is reconstituted into a string to compare to the full field from shadow, the "${ent[*]}" expression in:

    if [[ "${ent[*]}" = "$(mkpasswd -sm $hashtype -S "${ent[2]}" <<<"$pass")" ]]
    

If you modify the script and have it perform word splitting (or word joining) in other situations, you'll need to set IFS to different values for different commands or different parts of the script.

If you don't keep this in mind and assume $IFS is set to the usual whitespace ($' \t\n'), you could end up making your script behave in some pretty weird-seeming ways.