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

Solution 1:

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:

  • rozcietrzewiacz's answer on generating a password hash that matches an entry in /etc/shadow gives part of the solution.
  • Daniel Alder's comment explains the different syntax of the mkpasswd command present in Debian (and Ubuntu).

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.

Solution 2:

You can misuse sudo for this. sudo has the -l option, for testing the sudo privileges that the user has, and -S for reading in the password from stdin. However, no matter what privilege level the user has, if successfully authenticated, sudo returns with exit status 0. So, you can take any other exit status as indication that the authentication didn't work (assuming sudo itself doesn't have any problems, like permission errors or invalid sudoers configuration).

Something like:

#! /bin/bash
IFS= read -rs PASSWD
sudo -k
if sudo -lS &> /dev/null << EOF
$PASSWD
EOF
then
    echo 'Correct password.'
else 
    echo 'Wrong password.'
fi

This script depends quite a bit on the sudoers configuration. I have assumed the default setup. Things that can cause it to fail:

  • targetpw or runaspw is set
  • listpw is never
  • etc.

Other problems include (thanks Eliah):

  • The incorrect attempts will be logged in /var/log/auth.log
  • sudo must be run as the user you're authenticating for. Unless you have sudo privileges so that you can run sudo -u foo sudo -lS, that means you'll have to run the script as the target user.

Now, the reason I used here-docs is to hamper eavesdropping. A variable used as part of the command line is more easily revealed by using top or ps or other tools to inspect processes.

Solution 3:

Another method (probably more interesting for its theoretical contents than for its pratical applications).

User's passwords are stored in /etc/shadow.

The passwords stored here are encrypted, in the latest Ubuntu releases using SHA-512.

Specifically, upon the password creation, the password in clear text is salted and encrypted through SHA-512.

One solution would be then to salt / encrypt the given password and match it against the encrypted user's password stored in the given user's /etc/shadow entry.

To give a quick breakdown of how the passwords are stored in each user /etc/shadow entry, here's a sample /etc/shadow entry for a user foo with password bar:

foo:$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/:16566:0:99999:7:::
  • foo: username
  • 6: password's encryption type
  • lWS1oJnmDlaXrx1F: password's encryption salt
  • h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/: SHA-512 salted / encrypted password

In order to match the given password bar for the given user foo, the first thing to do is to get the salt:

$ sudo getent shadow foo | cut -d$ -f3
lWS1oJnmDlaXrx1F

Then one should get the full salted / encrypted password:

$ sudo getent shadow foo | cut -d: -f2
$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/

Then the given password can be salted / encrypted and matched against the salted / encrypted user's password stored in /etc/shadow:

$ python -c 'import crypt; print crypt.crypt("bar", "$6$lWS1oJnmDlaXrx1F")'
$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/

They match! Everything put into a bash script:

#!/bin/bash

read -p "Username >" username
IFS= read -p "Password >" password
salt=$(sudo getent shadow $username | cut -d$ -f3)
epassword=$(sudo getent shadow $username | cut -d: -f2)
match=$(python -c 'import crypt; print crypt.crypt("'"${password}"'", "$6$'${salt}'")')
[ ${match} == ${epassword} ] && echo "Password matches" || echo "Password doesn't match"

Output:

$ ./script.sh 
Username >foo
Password >bar
Password matches
$ ./script.sh 
Username >foo
Password >bar1
Password doesn't match

Solution 4:

After some searching, I discovered an easy way to check the validity of a user's password using su. Here's a short script demonstrating. You can save it to a file, add executable permissions, and then invoke it using ./pw_check.sh username.

#!/bin/bash

# This script accepts a username as a CLI argument,
# requests the password for that user, and
# then tests the validity of the given password for the given user

function show_usage() {
    echo "Usage: ${0} username"
    exit 1
}

if [[ $# -ne 1 ]]; then
    show_usage
fi

username=$1

# IFS=<space> - set IFS to an empty string (no character will be used to split,
#   therefore no splitting will occur)
#   so that read will read the entire line 
#   and see it as one word that will be assigned to the line variable
# -r - do not allow backslashes to escape any characters
# -s - do not echo input coming from a terminal
# -p - output the string PROMPT without a trailing newline before attempting to read
IFS= read -r -s -p "Enter ${username}'s password: " password;
echo -e "\n";

# Test that the given password is correct by trying to run a command using it
echo $password | /bin/su --command true - ${username};
retval=$?;

if [ $retval -eq 0 ]; then
    echo "Yay - the password is valid!";
else
    echo "Password is not valid";
fi

Basically, it accepts a username as an argument, prompts for the password of that user, and then attempts to run the true command as that user using su. We then check the return value of the attempt to run the true command as the given user and use the return value to determine success/failure and thus the validity of the given password.