如何检查输入的密码是否是该用户的有效密码?

May*_*hux 18 bash

场景:

在 bash 脚本中,我必须检查用户提供的密码是否是有效的用户密码。

即假设我有一个密码为 PA 的用户 A .. 在脚本中我要求用户 A 输入他的密码,那么如何检查输入的字符串是否真的是他的密码?...

Eli*_*gan 17

由于您想在 shell 脚本中执行此操作,因此在How to check password with Linux 中有一些贡献(在Unix.SE 上由 AB 建议)特别相关:

要手动检查字符串是否真的是某个用户的密码,您必须使用与用户影子条目中相同的哈希算法对其进行哈希处理,并使用与用户影子条目中相同的。然后它可以与存储在那里的密码哈希进行比较。

我已经编写了一个完整的工作脚本来演示如何执行此操作。

  • 如果你给它命名chkpass,你可以运行它,它会从标准输入中读取一行并检查它是否是密码。chkpass useruser
  • 安装whois 安装 whois包以获取mkpasswd此脚本所依赖的实用程序。
  • 此脚本必须以 root 身份运行才能成功。
  • 在使用此脚本或其任何部分进行实际工作之前,请参阅下面的安全说明
#!/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
Run Code Online (Sandbox Code Playgroud)

安全注意事项

这可能不是正确的方法。

这样的方法是否应该被认为是安全的和适当的取决于您尚未提供的用例的详细信息(在撰写本文时)。

它没有经过审计。

尽管我在编写此脚本时已尽量小心谨慎,但尚未针对安全漏洞对其进行适当审核。它旨在作为演示,如果作为项目的一部分发布,它将是“alpha”软件。此外...

“正在观看”的另一个用户可能会发现该用户的salt

由于如何mkpasswd接受盐数据的限制,此脚本包含一个已知的安全漏洞,您可能会或可能不会根据用例认为可接受。默认情况下,Ubuntu 和大多数其他 GNU/Linux 系统上的用户可以查看其他用户(包括 root)运行的进程的信息,包括他们的命令行参数。用户的输入和存储的密码哈希都不会作为命令行参数传递给任何外部实用程序。但,从所提取的shadow数据库,给定为一个命令行参数mkpasswd,因为这是实用程序接受盐作为输入的唯一途径。

如果

  • 系统上的另一个用户,
  • 任何有能力让任何用户帐户(例如,www-data)运行其代码的人,
  • 任何可以查看有关正在运行的进程的信息的人(包括通过手动检查 中的条目/proc

能够检查mkpasswd该脚本运行时的命令行参数,然后他们可以从shadow数据库中获取用户盐的副本。他们可能必须能够猜测该命令何时运行,但这有时是可以实现的。

使用您的 salt 的攻击者并不像使用您的 salt和 hash的攻击者那么糟糕,但这并不理想。salt 没有提供足够的信息让别人发现你的密码。但它确实允许某人在该系统上生成特定于该用户的彩虹表预先计算的字典哈希。这最初是没有价值的,但是如果您的安全性在以后受到损害并且获得了完整的哈希值,那么它可以被更快地破解以在用户有机会更改密码之前获得用户的密码。

因此,在更复杂的攻击场景中,这个安全漏洞是一个加剧因素,而不是一个完全可利用的漏洞。您可能认为上述情况有些牵强。但是我不愿意推荐任何将任何非公开数据泄露/etc/shadow给非 root 用户的方法用于一般的实际使用。

您可以通过以下方式完全避免此问题:

  • Perl语言编写或其他语言,可以让你的脚本的一部分,你调用C函数,如显示在吉尔的回答相关Unix.SE问题
  • 用这种语言编写整个脚本/程序,而不是使用 bash。(根据您标记问题的方式,您似乎更喜欢使用 bash。)

请注意如何调用此脚本。

如果您允许不受信任的用户以 root 身份运行此脚本或以 root 身份运行任何调用此脚本的进程,请小心。通过改变环境,他们可以让这个脚本——或任何以 root 身份运行的脚本——做任何事情。除非您可以防止这种情况发生,否则您不得允许用户提升权限来运行 shell 脚本。

10.4。有关更多信息,请参阅David A. Wheeler 的Secure Programming for Linux and Unix HOWTO 中的Shell Scripting Languages(sh 和 csh Derivatives)。虽然他的演讲侧重于 setuid 脚本,但如果其他机制不能正确清理环境,则它们可能会成为一些相同问题的牺牲品。

其他注意事项

它仅支持从shadow数据库读取哈希值。

必须隐藏密码才能使该脚本工作(即,它们的散列值应该在一个单独的/etc/shadow文件中,只有 root 可以读取,而不是在 中/etc/passwd)。

在 Ubuntu 中应该始终如此。在任何情况下,如果需要的脚本可以平凡扩展到从读取密码的哈希值passwd,以及shadow

IFS修改此脚本时请记住。

我一IFS=$开始就设置了,因为一个shadow entry的hash字段中的三个数据是用$.分隔的。

  • 它们也有一个前导$,这就是为什么散列类型和盐分别是"${ent[1]}"and"${ent[2]}"而不是"${ent[0]}"and 的原因"${ent[1]}"

此脚本中唯一$IFS确定 shell 如何拆分组合单词的地方是

  • 当这些数据被拆分成一个数组时,通过从不带引号的$( )命令替换中初始化它:

    set -f; ent=($(getent shadow "$1" | awk -F: '{print $2}')); set +f
    
    Run Code Online (Sandbox Code Playgroud)
  • 当数组重组为字符串以与 from 的完整字段进行比较时shadow"${ent[*]}"表达式为:

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

如果您修改脚本并让它在其他情况下执行分词(或词连接),则需要IFS为不同的命令或脚本的不同部分设置不同的值。

如果您不牢记这一点并假设$IFS设置为通常的空格 ( $' \t\n'),您最终可能会使您的脚本以一些看起来非常奇怪的方式运行。


mur*_*uru 10

您可以sudo为此滥用。sudo-l选项,用于测试用户拥有的 sudo 权限,以及-S从标准输入读取密码。但是,无论用户具有什么权限级别,如果成功通过身份验证,sudo都会以退出状态 0 返回。因此,您可以将任何其他退出状态作为身份验证无效的指示(假设sudo本身没有任何问题,例如权限错误或无效sudoers配置)。

就像是:

#! /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
Run Code Online (Sandbox Code Playgroud)

此脚本在很大程度上取决于sudoers配置。我已经假设了默认设置。可能导致它失败的事情:

  • targetpwrunaspw已设置
  • listpwnever
  • 等等。

其他问题包括(感谢 Eliah):

  • 错误的尝试将被登录 /var/log/auth.log
  • sudo必须以您要为其进行身份验证的用户身份运行。除非您有sudo权限运行sudo -u foo sudo -lS,否则您必须以目标用户身份运行脚本。

现在,我使用 here-docs 的原因是为了防止窃听。通过使用topps或其他工具来检查进程,可以更容易地揭示用作命令行一部分的变量。

  • 这种方式看起来很棒。尽管它不适用于具有不常见的 `sudo` 配置(如你所说)并且带有每次尝试记录的混乱 `/var/log/auth.log` 的系统,但它快速、简单,并且使用现有的实用程序,而不是而不是重新发明轮子(可以这么说)。我建议为 `read` 设置 `IFS=`,以防止填充空白的用户输入和未填充的密码错误成功,以及填充空白的用户输入和填充密码的错误失败。(我的解决方案有一个类似的错误。)您可能还想解释如何以正在检查密码的用户身份运行它。 (2认同)

kos*_*kos 5

另一种方法(其理论内容可能比实际应用更有趣)。

用户的密码存储在/etc/shadow.

存储在此处的密码在最新的 Ubuntu 版本中使用SHA-512.

具体来说,在创建密码时,明文密码通过SHA-512.

一种解决方案是对给定密码进行加盐/加密,并将其与存储在给定用户/etc/shadow条目中的加密用户密码进行匹配。

为了快速细分密码在每个用户/etc/shadow条目中的存储方式,/etc/shadow以下是foo具有密码的用户的示例条目bar

foo:$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/:16566:0:99999:7:::
Run Code Online (Sandbox Code Playgroud)
  • foo: 用户名
  • 6: 密码的加密类型
  • lWS1oJnmDlaXrx1F: 密码的加密盐
  • h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/:SHA-512加盐/加密密码

为了匹配给bar定用户的给定密码foo,首先要做的是获取盐:

$ sudo getent shadow foo | cut -d$ -f3
lWS1oJnmDlaXrx1F
Run Code Online (Sandbox Code Playgroud)

然后应该得到完整的加盐/加密密码:

$ sudo getent shadow foo | cut -d: -f2
$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/
Run Code Online (Sandbox Code Playgroud)

然后可以对给定的密码进行加盐/加密,并与存储在/etc/shadow以下位置的加盐/加密的用户密码进行匹配:

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

他们匹配!一切都放在bash脚本中:

#!/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"
Run Code Online (Sandbox Code Playgroud)

输出:

$ ./script.sh 
Username >foo
Password >bar
Password matches
$ ./script.sh 
Username >foo
Password >bar1
Password doesn't match
Run Code Online (Sandbox Code Playgroud)

  • 我认为这实际上非常实用,尽管我可能有偏见:这与我使用的方法相同。不过,您的实施和演示完全不同——这*绝对*是一个有价值的答案。虽然我使用 `mkpasswd` 从用户输入和现有的 salt(并包括附加功能和诊断)生成哈希值,而您编写了一个简单的 Python 程序来实现 `mkpasswd` 最重要的功能并使用它。我建议你在读取密码输入时设置 `IFS=`,并用 `"` 引用 `${password}`,这样使用空格的密码尝试不会破坏脚本。 (2认同)