改密事故复盘:一条 echo 命令把口令改成了另一个值

一次常规的服务账户改密,passwd 回显“成功”,可这个账户再也没登进去。问题不在 PAM、不在键盘布局、也不在锁定策略——在双引号:$ 后面那串名字被 shell 当成变量,展开成了空串。下面是完整复盘。

项目 内容
案例类型 变更事故(口令变更 · 人为操作)
发生时间 2026-11-05
系统环境 CentOS 7(RHEL 系),root 执行 passwd --stdin
涉及账户 svc-ops(服务账户,示例已脱敏)
影响 该账户 8 分钟内无法登录;有带外通道,未造成业务中断
定级 P2(可自行推导恢复)
根因 shell 双引号内的 $变量 被参数展开,口令被静默截短
关键词 参数展开 · passwd --stdin · 口令保管 · 变更验证

本案例口令全部为虚构示例:意图口令 Sv9@kL$DbPass7#nW,实际生效口令 Sv9@kL#nW。


一、现象

改密命令回显”成功”,但用原样口令登录被拒;再用”看起来完整”的口令试几次仍然被拒。

[root@node01 ~]# echo "Sv9@kL$DbPass7#nW" | passwd --stdin svc-ops
Changing password for user svc-ops.
passwd: all authentication tokens updated successfully.

[root@node01 ~]# ssh svc-ops@10.20.30.40
svc-ops@10.20.30.40's password:        # 照抄 Sv9@kL$DbPass7#nW
Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).

典型误判顺序:先怀疑键盘布局 / 输入法 / CapsLock → 再怀疑 PAM 拒绝弱口令 → 再怀疑 pam_tally2 锁号 → 都没问题,于是卡住。

二、时间线

时间 动作 结果
T+0 echo "...$..." | passwd --stdin svc-ops 回显成功
T+1 SSH 登录(原口令) Permission denied
T+3 排除 CapsLock / 输入法 / 全角 无效
T+5 查 secure 日志、pam_tally2 无锁定、无拒绝记录
T+6 按”双引号变量展开”规则反推候选 得到 Sv9@kL#nW
T+8 用候选口令登录 成功,定位完成

三、最小复现

$ bash -c 'echo "Sv9@kL$DbPass7#nW"'
Sv9@kL#nW

只要 DbPass7 这个变量不存在,它就被替换成空串——命令不报错,口令静默变短。

对照一下三种引号的差别:

$ bash -c 'echo "Sv9@kL$DbPass7#nW"'      # 双引号:展开 → Sv9@kL#nW
$ bash -c "echo 'Sv9@kL\$DbPass7#nW'"    # 单引号:原样 → Sv9@kL$DbPass7#nW
$ printf '%s\n' 'Sv9@kL$DbPass7#nW'      # printf + 单引号:原样(推荐)

四、根因

4.1 规则

POSIX 规定:双引号内仍保留特殊含义的只有 $、`、\ 和 " 本身(交互式 bash 还有 ! 历史展开)。其余字符——包括 #、%、@、空格——在双引号里都是字面量。

$ 后面紧跟名字时,shell 取最长的合法标识符作为变量名,合法字符是 [A-Za-z0-9_]:

"Sv9@kL"  +  "$DbPass7"  +  "#nW"
              └── 变量名取到 D b P a s s 7 结束(下一个字符是 #)

DbPass7 未定义 → 展开为空串 → 实际口令 Sv9@kL#nW。

4.2 边界在哪

意图口令 变量名 原因 实际生效
Sv9@kL$DbPass7#nW DbPass7 # 不属于名字 Sv9@kL#nW
Sv9@kL$Db-Pass7#nW Db - 不属于名字 Sv9@kL-Pass7#nW
Sv9@kL${DbPass7}#nW DbPass7 花括号明确边界 Sv9@kL#nW

4.3 为什么没人发现

  1. 回显是成功:passwd --stdin 拿到一行就改,改的正是它收到的那行,所以必然成功。
  2. set -x 帮不上忙:调试输出打印的是展开后的 argv,看到的就是 echo Sv9@kL#nW,反而掩盖了原意。要看原样文本得用 set -v(打印读入的原始行)或命令审计日志。
  3. 残留口令仍然”像密码”:Sv9@kL#nW 有 #、@、大小写混排,肉眼不像残缺。

五、处置路径(按代价从低到高)

① 按规则反推候选 → 直接试登录(本案例走的是这条,8 分钟恢复)
② 还有 root 会话(tmux / 其他终端)→ passwd svc-ops 交互式重设
③ 只有 SSH 且 root 被禁 → 云控制台 VNC、IPMI、串口 console
④ 彻底进不去 → 单用户模式(rd.break)改 root 再 passwd;或挂 rescue 盘

反推候选的算法很简单:把 $ 后面那串 [A-Za-z0-9_] 整段删掉,剩下的就是实际口令。变量越多,候选越多,但仍是有限枚举。

六、验证:用 shadow 的盐值反查

恢复前想确认”到底改成了什么”,不必反复试登录,可以用 /etc/shadow 里的盐重新算一遍(需 root):

# 1. 取出哈希
[root@node01 ~]# awk -F: '$1=="svc-ops"{print $2}' /etc/shadow
$6$abcd1234$q.3Bzdf7UWVY5.fpDKH9Bz6ZxlVMENwQntib/0zPHR.L1f0Kc0W1GwY7.vxWtwdQlITZY/mHqYNaYPfRx2j7D.

# 2. 用候选口令 + 同一盐值重算,逐字比对
[root@node01 ~]# openssl passwd -6 -salt abcd1234 'Sv9@kL#nW'
$6$abcd1234$q.3Bzdf7UWVY5.fpDKH9Bz6ZxlVMENwQntib/0zPHR.L1f0Kc0W1GwY7.vxWtwdQlITZY/mHqYNaYPfRx2j7D.

前缀决定算法:$6$ → openssl passwd -6(sha512,CentOS 7 默认),$5$ → -5,$1$ → -1。哈希以 ! 或 * 开头表示账户被锁,不能当口令判断依据。

七、正确写法对照

写法 是否安全 说明
echo "…$X…" | passwd --stdin u ❌ 本案例;$X 被展开
echo '…$X…' | passwd --stdin u ✅ 单引号内不展开
printf '%s\n' '…$X…' | passwd --stdin u ✅ 推荐 不受 -n/-e/反斜杠影响
printf 'u:%s\n' '…$X…' | chpasswd ✅ 推荐 免二次输入,适合批量;口令内不能含 :
passwd u 交互式 ✅ 首选 全程不经 shell,人少时最稳
cat <<'EOF' | passwd --stdin u ✅ 引号版 heredoc 不展开
从 600 权限文件读 ✅ 用于批量,用完 shred -u

八、相邻陷阱清单

陷阱 表现 规避
反引号 ` 在双引号内 被当命令替换执行 口令含反引号必须单引号
! 历史展开(交互 bash) event not found,或命令被改写 单引号;或 set +H
\ 在双引号内 对 $、`、"、\ 仍具转义作用 单引号
chpasswd 遇 : 字段错位,用户名/口令被切分 口令禁用 :,或改用 passwd --stdin
Debian/Ubuntu 无 passwd --stdin 用 chpasswd
Windows 编辑过的脚本 行尾 \r 混入口令 dos2unix 或 tr -d '\r'
sh(dash)里的 echo 会解释 -n、\ 一律 printf

九、预防清单

P0

  • 改密命令中禁止出现”双引号 + $“的组合;脚本里口令一律走单引号 / printf / 引号版 heredoc。
  • 改密后必须做一次真实登录验证(su - u -c true,或从其他主机 SSH),不能只看 passwd 的回显。

P1

  • 改密脚本加 --dry-run:只打印”将要写入的字符串长度 + 命中的字符集”,不打印明文,长度不符即中止。
  • 变更记录只记口令指纹(如 sha256sum 前 8 位),不落明文,便于事后确认”到底改了哪个版本”。
  • 生产改密走双人复核;口令临时值放密码保管箱,不走聊天工具。

P2

  • 定期核查带外通道(VNC / IPMI / 串口)可用性,避免”只能靠密码”。
  • 服务账户至少保留一条密钥登录通道,口令变更失败时不至于彻底失联。

十、自测题

1.(单选)echo "ab$Cd9x#ef" | passwd --stdin u,实际写入的口令是?
A. ab$Cd9x#ef B. ab9x#ef C. ab#ef D. abCd9x#ef

2.(判断)双引号里的 # 会被当作注释,注释之后的内容被丢弃。

3.(填空)echo "a$B-c$D9" 中,第一个被展开的变量名是 ______。

4.(单选)在 bash 中查看”变量展开前的原始行”,应使用?
A. set -x B. set -v C. set -e D. set -u

5.(场景)chpasswd 批量改密时报 line 3: missing new password,最可能的原因是什么?

6.(场景)账户登录失败,/etc/shadow 里哈希为 $6$Xk9p$…,且以 ! 开头。这说明什么?能否据此判断口令被改错过?

参考答案
  1. C。$Cd9x 是完整变量名(# 终止名字),未定义则空,剩 ab#ef。
  2. 错。双引号内 # 只是普通字符,不构成注释。
  3. B(- 不是名字字符,名字在 B 处结束);第二次是 D9。
  4. B。-v 打印读入的原始行;-x 打印的是展开后的 argv,正好掩盖问题。
  5. 口令里含 :,chpasswd 按 用户名:口令 切分时字段错位;也可能该行含空白/CRLF。
  6. ! 前缀表示账户被 passwd -l 或 PAM 锁定,不能据此判断口令是否被改错;需先解锁再验证口令。

一句话总结:双引号替 shell 留了一个后门——$ 后面那串名字被整段换成空,口令”改成功了”,但改成了另一个值;口令进 shell,只用单引号。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注