拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

本地密码哈希读取与审计:从shadow字段到安全加固实战

本地密码哈希读取与审计:从shadow字段到安全加固实战

1. 为什么盯着本地 Hash 看:一次巡检揪出的两个隐患

有一次我例行巡检公司的一台 Linux 服务器,cat /etc/shadow一打开就发现两个问题:一个账号的密码字段以!开头,说明这个账户被锁了;另一个账号对应的字段居然是空的,意思是它根本没有密码。这台机器部署了 SSH 服务,虽然平时靠密钥登录,但空密码账户的存在意味着任何能触达登录端口的人,都有机会在 PAM 配置宽松的情况下绕过认证。那天的巡检,其实就是一次最典型的"读取本地密码 Hash"。

读取本地密码 Hash,说白了就是把系统里本地账号的密码哈希值从存储位置取出来,并看懂它。它不是黑产里那种"破解密码"的入侵动作,也不是单纯把一串字符打印到屏幕上就完事。它首先是一项安全审计能力:运维人员通过它检查有没有空密码账户、有没有被锁的僵尸账户、密码过期策略是不是真的在生效;安全评估人员在拿到书面授权后,也需要先完整地读一遍本机所有账号的哈希,才能判断整体凭据暴露面有多大。

1.1 读取本地 Hash 到底在做什么

很多人一听到"Hash"就想到哈希碰撞、彩虹表、暴力破解,这些都是误解。实际上,现代 Linux 和 Windows 系统都不会明文保存密码,而是保存经过加盐和迭代计算的哈希值。读取本地 Hash,就是把/etc/shadow或 SAM 数据库里这些哈希值摘出来,再根据哈希的前缀、字段结构判断账号状态。它解决的核心问题有三个:一是"这个系统有没有空密码账户",二是"哪些账户被锁定、哪些账户的哈希不可用",三是"系统用的密码算法是否已经过时到可以轻松被碰撞"。这三个问题在等保测评、基线加固、日常健康检查里都是必查项。

我见过不少运维同行,服务器跑了好几年,从来没主动看过一眼 shadow 文件。他们觉得密码是密码,Hash 是 Hash,反正登录时系统自己会判断,不需要人工介入。可一旦出事,比如发现某个僵尸账户被人登录过,再去追踪就晚了。把"读取本地 Hash"当成定期巡检项目的一部分,和看磁盘空间、看日志占用一样,应该成为基础习惯。

1.2 哪些场景真正需要读 Hash

第一个场景是安全基线检查。公司做等级保护或者内部安全合规时,需要核对所有服务器账号的状态,这时候要批量读取所有本地账号的哈希状态,确认没有空密码、没有长期不动的高权限僵尸账号。第二个场景是密码策略验收,比如你在/etc/login.defs里配置了密码 90 天过期,但实际系统里有些老账户没套上这个策略,通过读取 shadow 的过期字段就能验证。第三个场景比较特殊,是自己管理的机器忘记密码后的排查。注意,忘记密码时多数情况下你要做的是重置,而不是"读哈希还原明文",这个区分我后面会讲。

所以这篇博文的核心思路是:先知道 Hash 存在哪、长什么样,再学会读取工具和识别方法,最后把读到的结果变成加固动作。整个链路不需要任何黑产手段,它就是一名系统管理员和一线安全工程师的基本功。

2. Linux 下密码哈希的存在位置与 /etc/shadow 字段拆解

Linux 系统的本地密码哈希,绝大多数发行版都存在/etc/shadow这个文件里。这个文件不是给普通用户看的,它的权限通常为-rw-r-----,属主是 root,所属组是 shadow,普通用户执行cat /etc/shadow会直接 Permission denied。只有在使用 sudo 或具备 root 权限的前提下才能读取。

2.1 passwd 和 shadow 的分工

很多人会把/etc/passwd和/etc/shadow搞混。/etc/passwd保存的是账号的基础信息,比如用户名、UID、主组、家目录、登录 shell,密码字段几乎清一色是一个x占位符,真正的密码哈希早就被挪到 shadow 文件里了。这样设计的历史原因是早年 passwd 文件需要让所有进程可读,如果里面直接放哈希,等于把哈希公开给所有本地用户。后来引入 shadow 机制,哈希被隔离到仅 root 和部分特权进程可读的文件中,普通用户即使看到 passwd 里那个x,也拿不到任何敏感信息。

2.2 九个字段逐项说明

/etc/shadow每一行对应一个账号,字段之间用冒号分隔,完整结构是九段:

tom:$6$rounds=656000$EhmXXXX$aaaaaaaaaa...:18622:0:99999:7:::

第一个字段是用户名,第二个字段是密码哈希或状态标记,第三个字段是"最后一次修改密码时间",单位是自 1970-01-01 起的天数,比如 18622 表示距离 1970 年 1 月 1 日过去了 18622 天。第四个字段是两次修改密码的最小间隔天数,0 表示可以随时改。第五个字段是密码最长有效天数,99999 表示永不过期。第六个字段是密码过期前提前多少天提醒,7 表示提前一周开始提示。第七个字段是密码过期后多少天锁定账号,通常是空。第八个字段是账号失效日期,也是空。第九个字段保留未用。

你可能已经发现了,第三个字段到第七个字段,就是 Linux 密码过期策略的原始数据源。我前面说的等保检查,很多时候就是在看这些数字到底配没配。如果第五个字段是 99999,说明这台机器上该账户的密码从不过期,这和热搜里"linux密码过期提醒通知"直接相关——提醒通知靠的并不是某个后台服务凭空发消息,而是登录 PAM 读取这些字段后,在控制台或 SSH 登录时把"密码将在 N 天后过期"打印出来。

2.3 权限要求与读取工具

读取 shadow 文件,最直接的是:

sudo cat /etc/shadow

但如果你只是想要某个特定账号的信息,用getent shadow更精确:

sudo getent shadow tom

getent的好处是它会走系统 NSS 模块,不受文件中注释、回车等格式问题干扰。想要逐行拆字段,awk很顺手:

sudo awk -F: '{print $1, $2, $3, $5}' /etc/shadow

上面的命令会输出用户名、哈希、上次修改时间和最大有效天数,一眼就能看出哪些账号的密码策略是异常的。另外,vipw -s可以安全编辑 shadow 文件,它会在保存前做格式校验,避免手抖把行写坏;pwck则适合事后验证 shadow 与 passwd 的一致性,我习惯在每次巡检脚本跑完之后执行一次pwck -q。

提示:shadow 文件里第二字段为!或者*开头的账号并不代表密码哈希不存在,而是表示这个哈希被禁用了,一般出现在账户刚创建未设密码、管理员手动锁定等场景。做审计时不能简单认为"这个账号没有哈希就是安全的",要看具体标记。

3. Windows 的 SAM 数据库:锁定中的注册表怎么取出来

Windows 下的本地密码哈希不叫 shadow,它存放在注册表的一个独立配置单元里,路径是C:\Windows\System32\config\SAM。与 Linux 的差异在于,Windows 运行期间 SAM 文件被系统独占锁定,哪怕你是管理员,直接copy这个文件也会报"正在被另一个进程使用"。

3.1 SAM 为什么不能直接复制

SAM 里面有 SYSKEY 加密,哈希并不是明文存放在文件里的,同时还被系统核心进程占用。所以想完整拿到本地账号哈希,常规思路不是"复制文件",而是通过受支持的系统机制把注册表配置单元导出来,或者借助卷影副本绕过文件锁定。这中间涉及两个关键文件:SAM 和 SYSTEM。SAM 存储账号与哈希本体,SYSTEM 存储解密 SAM 所需的部分密钥信息(SYSKEY),两者缺一不可。导出的时候务必一起拿。

3.2 注册表导出与卷影复制两种取法

第一种方法是用管理员权限打开 CMD,执行reg save:

reg save HKLM\SAM sam.hiv /y reg save HKLM\SYSTEM system.hiv /y

这条命令会把注册表中的 SAM 和 SYSTEM 配置单元完整导出为文件,/y参数表示覆盖同名文件。导出的两个文件,配合后面的离线解析工具,就能还原出本地账号的哈希。需要注意,导出的文件带有系统的安全描述符,在传输到其他分析机器时要保证是在受控环境内操作。

第二种方法是卷影复制。系统管理场景下,如果目标机器不方便执行注册表导出,可以用 VSS 把整个系统盘快照出来,然后从快照里拿 SAM。示例命令:

vssadmin create shadow /for=C:

执行后会生成一个卷影设备路径,比如\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1。接着从卷影中复制 DA 那两个文件:

copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\config\SAM D:\audit\SAM copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\config\SYSTEM D:\audit\SYSTEM

这种方法的优点是不需要复制正在占用的文件,也能在系统跑着的情况下拿到一致性较好的配置单元副本。卷影复制在 Windows 的备份还原功能里本来就大量使用,属于正规的系统管理接口。

3.3 离线解析:secretsdump 把 hash 变成人能看懂的格式

拿到sam.hiv和system.hiv之后,需要离线解析。我用得最多的是 Impacket 套件里的secretsdump,它可以在不触碰目标系统的情况下,在本地分析 SAM 与 SYSTEM 文件:

secretsdump.py -sam sam.hiv -system system.hiv LOCAL

输出大致长这样:

Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::

每一行由四段组成,分别是用户名、RID、LM Hash、NT Hash。现在的新系统基本不再使用 LM Hash,所以 LM 字段几乎全是aad3b435b51404eeaad3b435b51404ee这个固定空值。NT Hash 才是真正需要关注的对象,它是 32 位的十六进制字符串。如果你看到某个账户的 NT Hash 是31d6cfe0d16ae931b73c59d7e0c089c0,那说明这个账户的密码是空字符串——这是审计时要第一时间标记的高危账户。

提示:secretsdump 这类工具属于安全测试常用工具,我只建议在自有机器、已获得书面授权的评估环境或者搭建的靶场中使用。不要试图拿它去处理别人系统的 SAM 文件,那属于违法行为。

4. 哈希字符串辨识:从 $ 前缀看出算法与风险

拿到哈希字符串之后,最关键的并不是"看到一串乱码",而是快速判断它是什么算法、什么状态。Linux 的哈希字段其实是结构化的,不是随便写的。

4.1 特殊标记:空字段、*、!、!!

先看第二字段为空的情况。这个账号在 shadow 里没有任何哈希,等同于没有密码。在某些系统上,这种账号甚至可以直接通过本地终端登录,审计时必须第一时间处理。*开头表示账户是系统服务账户或已经禁用密码登录,例如*LK*、*!*在某些发行版里也能见到,含义类似。!开头代表该账户的密码被锁定,!!则常见于新账户尚未设置密码且被锁定的状态。还有一种容易被忽略的情况:哈希以!开头但后面还跟着一串$6$...,这表示账户曾经设置过密码,后来被锁定但哈希并没有被清除。审计脚本如果只判断"以!开头就是锁定",会把这种"已设置过密码但又锁定"的账号和"从未设置密码"的账号混为一谈,所以识别时要把!后面的内容也拿出来看。

4.2 算法对照表:$1$/$5$/$6$/$y$/$2b$/ argon2

现代 Linux 哈希字符串通常以$算法标识$盐$实际哈希的形式出现。下表是实际运维中最高频的几种:

前缀算法常见场景审计关注点
$1$MD5老系统、嵌入式设备算法过弱,建议禁止新账户使用
$2a$/$2b$/$2y$bcrypt部分应用、旧系统强度尚可,注意 rounds 是否过低
$5$SHA-256老版本 Linux 可选项不如$6$常见
$6$SHA-512绝大多数现代 Linux 的默认值审计基线里的主流合格算法
$y$yescryptRHEL 9、Fedora、新版 Debian新算法,强度高
$argon2i$/$argon2id$Argon2部分新系统、应用抗 GPU 破解能力强

需要注意,算法标识不是随便能换的。系统用的是哪一种,取决于发行版编译时选择的密码算法,以及/etc/login.defs里的ENCRYPT_METHOD。如果一台服务器上既有老账号的$1$,又有新账号的$6$,说明系统经历了升级或配置变更,老账号的弱哈希不会自动升级,这就给审计留下了清晰的切入点:优先处理还在使用$1$的老账户。

4.3 盐和 rounds 在审计中的信息量

哈希字符串中间那一段带$包裹的内容是盐(salt),它的作用是让相同密码产生不同哈希,防止两个账号密码相同就直接暴露。盐本身不是秘密,它跟着哈希存,但它的存在意味着你不能用预先算好的固定彩虹表直接匹配。另外要注意$6$或$y$后面可能跟着rounds=N参数,这表示哈希迭代次数。比如$6$rounds=656000$....就是在告诉系统要迭代 656000 次。迭代次数越多,每次密码验证消耗的 CPU 时间越长,暴力破解时需要尝试每个密码的成本也越高。审计时如果发现老账号的 rounds 值很低,即使算法是 SHA-512,强度也是打折扣的。

为了快速识别,我常用一小段 Python 脚本直接判断哈希属于哪种算法:

def identify(hash_str: str) -> str: if not hash_str: return "空哈希(无密码)" if hash_str.startswith("!"): return f"锁定账户, 后缀: {hash_str[1:60]}" if hash_str.startswith("*"): return "系统账户或禁用登录" algo = hash_str.split("$")[1] if hash_str.startswith("$") else "?" algos = { "1": "MD5", "5": "SHA-256", "6": "SHA-512", "y": "yescrypt", "2a": "bcrypt", "2b": "bcrypt", "2y": "bcrypt", "argon2i": "Argon2i", "argon2id": "Argon2id", } return algos.get(algo, algo)

把这段代码接到读取 shadow 的逻辑里,几十行就能做出一个初版的本地哈希审计小工具。后面第 5 部分我会给出一份可以直接用的脚本。

5. 实操:写一个本地哈希审计脚本,代替肉眼翻 shadow

人工cat /etc/shadow在服务器数量少的时候还能接受,但一旦面对几十台机器,逐行看字段非常容易漏。我用 Python 写过一个审计脚本,作用是遍历本地所有账号,按哈希状态分类输出,帮我把"空密码""锁定账户""弱算法账户"一次性列出来。

5.1 用 Python 的 spwd 模块读取并分类

Linux 的 Python 标准库里有一个spwd模块,专门用于读取 shadow 数据库,前提是进程必须有 root 权限。脚本逻辑分三步:先用pwd.getpwall()拿到全部本地账号,再通过spwd.getspnam()获取每个账号的 shadow 记录,最后对哈希字段做状态归类。

#!/usr/bin/env python3 # 本地密码哈希审计脚本,需要在 root 或 sudo 下运行 # 用法: sudo python3 audit_hash.py import pwd import spwd def hash_status(s: str) -> str: if not s or s == "": return "危险:密码字段为空(无密码)" if s.startswith("!!"): return "锁定:双重感叹号(无密码且被锁)" if s.startswith("!") and len(s) > 1: return "锁定:原哈希被禁用" if s.startswith("!"): return "锁定:哈希未设置" if s.startswith("*"): return "锁定:系统账户(星号)" if s.startswith("$"): algo = s.split("$")[1] names = { "1": "有密码:MD5(弱)", "5": "有密码:SHA-256", "6": "有密码:SHA-512", "y": "有密码:yescrypt", "2a": "有密码:bcrypt", "2b": "有密码:bcrypt", "2y": "有密码:bcrypt", "argon2i": "有密码:Argon2i", "argon2id": "有密码:Argon2id", } return names.get(algo, f"有密码:算法 {algo}") return "未知格式" def main(): print(f"{'用户名':<20}{'状态':<30}{'哈希前缀'}") print("-" * 85) for u in pwd.getpwall(): try: sp = spwd.getspnam(u.pw_name) except KeyError: continue status = hash_status(sp.sp_pwdp or "") prefix = (sp.sp_pwdp or "")[:35] print(f"{sp.sp_namp:<20}{status:<30}{prefix}") if __name__ == "__main__": main()

如果是在 Ubuntu 20.04、22.04 上,运行sudo python3 audit_hash.py即可。脚本会输出类似下面的内容:

tom 有密码:SHA-512 $6$rounds=656000$EhmXXXX... mysql 锁定:系统账户(星号) * test 危险:密码字段为空(无密码) zabbix 锁定:原哈希被禁用 !$6$rounds=...

看到test那一行,基本就是高危信号了,接下来要做的是立刻给该账号设密码或者直接锁定。看到zabbix的!$6$,说明这是个曾经能用密码登录、后来被锁定的账号,如果该账号已经不再使用,最好连同用户一起删除。

5.2 输出解读与运行细节

脚本里我把 UID 小于 1000 的系统账号也列出来了,因为很多系统服务账户虽然不能交互登录,但同样需要检查哈希状态。比如mysql账户如果显示"星号锁定",这是正常的;如果显示"有密码:SHA-512",反而值得关注,说明一个服务账户存在实际可用的密码哈希,一旦服务被攻破,这个哈希可能成为横向移动的凭据。另外,脚本打印的是哈希前缀而不是完整哈希,这是有意的。审计记录里只需要算法和状态,不需要把完整的哈希原样留在日志里,降低泄露风险。

有些发行版的 Python 环境中,spwd模块需要机器上具备 GNU shadow 相关库,标准安装基本都带了。如果你用的是 macOS 或者 BSD 环境,情况会略有不同,本文只讨论 Linux 场景。拿到脚本输出后,我会习惯性追加执行一步:

sudo pwck -q

pwck用来校验/etc/passwd与/etc/shadow的一致性,如果有账号只存在于一个文件中,日志会有提示,免得脚本统计到"幽灵账号"。

5.3 Windows 哈希的同类体检

Windows 那边没有 Linux 这种结构清晰的字段,但审计思路可以复用。secretsdump导出的哈希中,重点看 NT Hash。把所有非固定值的 NT Hash 收集到一个清单,作为"存在实际密码哈希"的账户,而 NT Hash 等于31d6cfe0d16ae931b73c59d7e0c089c0的账户视为空密码账户。如果某账户的哈希带 LM Hash 且不是固定空值,说明系统还启用了过时且脆弱的 LM 认证,需要在组策略中关闭。这样 Windows 和 Linux 的审计在"输出高危清单"这条线上就统一了:空密码、锁定状态、弱算法、可登录的非预期账号,都是必查项。

6. 读完之后要动手:从哈希状态引出加固动作

审计的终点不是输出一份报告,而是把这些状态变成实际加固动作。我几乎每次跑完哈希审计脚本,都会顺带把下面几类问题处理掉。

6.1 空密码与锁定账户的处理

空密码账户直接处理方式是先设置一个随机强密码,再考虑是否锁定。对普通用户,用passwd设置新密码:

sudo passwd test

如果确认该账号不再需要,就直接锁定或移除:

sudo usermod -L test sudo userdel -r test

注意usermod -L会在密码哈希前加一个!,本质上是禁用这个哈希,而不是删除它。要恢复的话用usermod -U。不过我的建议是,对僵尸账号别想着恢复,直接删除才是最干净的。对于锁定类型的账号,比如日志里显示原哈希被禁用的老账户,如果业务上确实需要启用,则重新设置密码,否则一删了之。删除前一定要确认该账号没有持有在跑的进程,可以用ps -u 用户名先看一眼。

6.2 密码过期策略与到期提醒配置

前面 shadow 字段的第三到第七位,就是密码过期策略的数据源。如果大量账号的第五字段是 99999,那就等于密码永不过期,这通常过不了安全合规检查。配置方式有两种。

第一种是修改/etc/login.defs:

PASS_MAX_DAYS 90 PASS_MIN_DAYS 7 PASS_WARN_AGE 14

修改后只对之后新建的账户生效,存量账户需要用chage命令挨个调整:

sudo chage -M 90 -m 7 -W 14 tom

-M设置最大有效天数,-m设置两次修改最小间隔,-W设置过期前提醒天数。配置完成后可以用chage -l tom查看当前策略是否生效。这个字段配置好之后,用户登录时终端会出现类似"密码将在 14 天后过期"的提示,这就是热搜里"linux密码过期提醒通知"的实际机制。它不依赖额外的邮件系统,只要 PAM 配置了 pam_lastlog 或对应模块,控制台和 SSH 登录就会自动输出提醒。想让提醒更显眼,也可以在/etc/ssh/sshd_config里配合 Banner 在登录前展示策略说明,但核心的过期计数逻辑还是来自 shadow 字段。

6.3 算法强度与下一代哈希的选择

当我发现系统里还有$1$开头的老哈希时,会对这些账号执行一次密码重置,强制用户重新设置密码,这样系统会把新密码按照/etc/login.defs里配置的算法重新哈希。具体重置方式很简单:

sudo chage -d 0 用户名

-d 0会把最后一次修改密码时间清零,强制该用户下次登录时必须修改密码。这样既完成了密码过期,又顺手让系统按照新一代算法重新生成哈希。对于新系统,我建议在/etc/login.defs里明确将ENCRYPT_METHOD保持为发行版默认值,不要为了"兼容老工具"把它降级为 MD5。MD5 哈希在今天的 GPU 算力下已经很不安全,能不用就不用。

7. 边界、误区与我的操作习惯

喜欢和安全沾边的操作,边界永远比技术本身更重要。读取本地密码 Hash 这件事,同样有明确的红线。

7.1 授权边界:哪些机器可以读

自己管理的服务器、正式工单里分配给你的机器、搭建的本地靶场环境,这些都是可以放心操作的范围。但任何一台不属于你、没有书面授权、不属于你所在组织资产范围的机器,都不要尝试读取它的哈希。哪怕只是"好奇"或"测试工具",在未授权情况下触碰他人系统凭据数据,性质完全不同。安全审计的合理路径是:拿到授权 -> 在受控环境操作 -> 输出报告给相关方 -> 删除中间产物。这条路径里,授权是第一步,也是不可省略的一步。

7.2 很容易踩的坑

第一个坑是把"读取哈希"和"恢复密码"混为一谈。哈希是单向的,你看到系统登录时校验的是一串哈希,但它不会主动变成明文密码。忘记密码时应该做的是重置,Linux 用chpasswd、单用户模式等,Windows 用启动修复或安全模式工具,而不是反复分析哈希。第二个坑是误判!开头。有些发行版里!!表示哈希为空且被锁定,有些发行版!后跟哈希表示锁定但不删除,处理方式完全不同,一定要先确认分布特征再批量操作。第三个坑是把哈希字符串随手贴到网上。如果审计过程中你需要比对某个哈希值,请先脱敏,把中间大部分字符打码,只保留算法前缀和后几位用于区分。完整的哈希一旦公开,配合在线碰撞服务,等于把账号凭据半公开了。

7.3 安全处理哈希数据的好习惯

我自己的习惯是:每次审计完,把系统里所有账号的哈希状态整理成表格,存到权限受限的本地文件夹,不放到公网云笔记里。脚本输出只保留用户名、算法、策略字段,不保留完整哈希。做完加固之后,把auth.log、secure日志里有关于passwd、useradd、chage的关键动作拉出来检查一遍,确保没有异常的新增账户。最后再分享一个小技巧:给审计脚本加一个简单的"阶段对比"逻辑,记录上次运行时的账号清单,下次运行后自动比对,有新增账号或哈希状态变化的,单独告警。这套流程跑顺之后,密码哈希从"一次性的神秘数据"就变成了日常巡检里最普通、也最可靠的一个指标项。

返回列表