场景:
在 bash 脚本中,我必须检查用户给出的密码是否是有效的用户密码。
即假设我有一个用户 A,其密码为 PA。在脚本中我要求用户 A 输入他的密码,那么如何检查输入的字符串是否真的是他的密码?...
答案1
由于您想在 shell 脚本中执行此操作,因此如何使用 Linux 检查密码?(在Unix操作系统,AB 建议) 尤其重要:
要手动检查字符串是否确实是某个用户的密码,必须使用与用户影子条目相同的哈希算法对其进行哈希处理,相同盐就像在用户的影子条目中一样。然后可以将其与存储在那里的密码哈希进行比较。
我已经编写了一个完整且可用的脚本来演示如何做到这一点。
- 如果你命名它
chkpass
,你可以运行,它将从标准输入读取一行并检查它是否chkpass user
user
的密码。 - 安装谁是 包来获取
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
安全注意事项
这可能不是正确的方法。
这种方法是否应被视为安全且适当,取决于您尚未提供的有关用例的详细信息(截至撰写本文时)。
尚未经过审计。
尽管我在写这个剧本时已经尽力小心谨慎,尚未对安全漏洞进行适当的审核。它旨在作为演示,如果作为项目的一部分发布,则将是“alpha”软件。此外...
正在“观看”的另一个用户可能会发现该用户的盐。
由于mkpasswd
接受盐数据的方式存在限制,此脚本包含已知的安全漏洞,根据使用情况,您可能会认为可以接受,也可能不接受。默认情况下,Ubuntu 和大多数其他 GNU/Linux 系统上的用户可以查看其他用户(包括 root)运行的进程的信息,包括他们的命令行参数。用户的输入和存储的密码哈希都不会作为命令行参数传递给任何外部实用程序。但盐从数据库中提取shadow
,是作为命令行参数给出mkpasswd
,因为这是实用程序接受盐作为输入的唯一方法。
如果
- 系统上的另一个用户,或者
- 任何有能力让任何用户账户(例如
www-data
)运行其代码的人,或者 - 任何可以查看正在运行的进程信息的人(包括手动检查中的条目
/proc
)
能够检查mkpasswd
此脚本运行时的命令行参数,然后他们可以从数据库中获取用户盐的副本shadow
。他们可能必须能够猜测什么时候该命令已运行,但有时这是可以实现的。
用你的盐攻击并不比用你的盐攻击的人坏和哈希,但这并不理想。盐值无法提供足够的信息让别人发现你的密码。但它确实允许某人生成彩虹桌或者预先计算的字典哈希值特定于该系统上的该用户。这最初毫无意义,但如果您的安全受到威胁稍后并获取完整哈希值后,就可以破裂在用户有机会更改密码之前更快地获取用户密码。
因此,这个安全漏洞在更复杂的攻击场景中是一个加剧因素,而不是一个完全可利用的漏洞。你可能会认为上述情况是牵强附会的。但我不愿意推荐任何在现实世界中普遍使用的方法,因为这种方法会泄露任何/etc/shadow
非 root 用户的非公开数据。
您可以通过以下方式完全避免此问题:
- 使用 Perl 或其他允许调用 C 函数的语言编写部分脚本,例如Gilles 的回答中显示到相关的 Unix.SE 问题,或者
- 用这种语言编写整个脚本/程序,而不是使用 bash。(根据您标记问题的方式,似乎您更喜欢使用 bash。)
请谨慎调用该脚本。
如果您允许不受信任的用户以 root 身份运行此脚本或以 root 身份运行调用此脚本的任何进程,当心通过改变环境,他们可以制作这个脚本——或者任何以 root 身份运行的脚本--do任何事物除非您可以防止这种情况发生,否则您不能允许用户提升权限来运行 shell 脚本。
看10.4. Shell 脚本语言(sh 和 csh 衍生语言)David A. Wheeler 的Linux 和 Unix 的安全编程方法了解更多信息。虽然他的演讲重点介绍了 setuid 脚本,但如果其他机制没有正确地清理环境,它们也可能会遇到同样的问题。
其他说明
它仅支持从数据库读取哈希shadow
。
必须隐藏密码才能使此脚本正常工作(即,它们的哈希值应放在/etc/shadow
只有 root 可以读取的单独文件中,而不是/etc/passwd
)。
在 Ubuntu 中应该始终如此。无论如何,如果需要,可以轻松扩展脚本以从passwd
和读取密码哈希值shadow
。
IFS
修改此脚本时请记住这一点。
我IFS=$
在一开始就设置了,因为影子条目的哈希字段中的三个数据是用 分隔的$
。
- 它们还具有前导
$
,这就是为什么哈希类型和盐分别是"${ent[1]}"
和"${ent[2]}"
而不是"${ent[0]}"
和"${ent[1]}"
。
此脚本中唯一$IFS
确定 shell 如何分裂或者结合单词是
当这些数据被分成一个数组时,通过以下不带引号的
$(
)
命令替换来初始化它:set -f; ent=($(getent shadow "$1" | awk -F: '{print $2}')); set +f
当将数组重构为字符串与中的完整字段进行比较时
shadow
,"${ent[*]}"
表达式为:if [[ "${ent[*]}" = "$(mkpasswd -sm $hashtype -S "${ent[2]}" <<<"$pass")" ]]
如果您修改脚本并让其在其他情况下执行单词拆分(或单词合并),则需要IFS
为不同的命令或脚本的不同部分设置不同的值。
如果您不记住这一点并假设$IFS
将其设置为通常的空格($' \t\n'
),那么您的脚本最终可能会以一些非常奇怪的方式运行。
答案2
您可以滥用sudo
此功能。sudo
有-l
选项,用于测试用户拥有的 sudo 权限,以及-S
从 stdin 读取密码。但是,无论用户拥有什么权限级别,如果成功通过身份验证,则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
此脚本很大程度上取决于sudoers
配置。我采用的是默认设置。可能导致其失败的情况如下:
targetpw
或runaspw
设置listpw
是never
- ETC。
其他问题包括(感谢 Eliah):
- 错误的尝试将被记录
/var/log/auth.log
sudo
必须以您为其验证的用户身份运行。除非您拥有sudo
可以运行的权限sudo -u foo sudo -lS
,否则这意味着您必须以目标用户身份运行该脚本。
top
现在,我使用 here-docs 的原因是为了防止窃听。使用或ps
其他工具检查进程可以更轻松地揭示用作命令行一部分的变量。
答案3
另一种方法(其理论内容可能比其实际应用更有趣)。
用户的密码存储在 中/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:::
foo
: 用户名6
:密码的加密类型lWS1oJnmDlaXrx1F
:密码的加密盐h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/
:SHA-512
加盐/加密的密码
为了匹配bar
给定用户的给定密码foo
,首先要做的是获取盐:
$ sudo getent shadow foo | cut -d$ -f3
lWS1oJnmDlaXrx1F
然后就可以得到完整的加盐/加密密码:
$ sudo getent shadow foo | cut -d: -f2
$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/
然后可以对给定的密码进行加盐/加密,并将其与存储在中的加盐/加密的用户密码进行匹配/etc/shadow
:
$ python -c 'import crypt; print crypt.crypt("bar", "$6$lWS1oJnmDlaXrx1F")'
$6$lWS1oJnmDlaXrx1F$h4vuzZVBwIE1Z6vT7N.spwbxYig9e/OHOIH.VDv9JPaC3.OtTusPFzma7g.R/oSZFW5QOI7IDdDY01G0zTGQE/
他们匹配!所有内容都放入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"
输出:
$ ./script.sh
Username >foo
Password >bar
Password matches
$ ./script.sh
Username >foo
Password >bar1
Password doesn't match
答案4
为此,我使用了我创建的以下程序:
密码有效[用户密码]