两年前我们第一次做AD域控弱密码排查,是因为安全演练临时点名要结果。当天下午我带着一个实习生熬夜跑了半宿脚本,从域控里导出一份几百条弱密码账号清单,往工作群里一甩,当时觉得这事儿干得挺漂亮。可三个月后再复盘,发现那些当初被通报的账号,一大半又悄悄变回弱密码,甚至还有新账号加入。那一刻我才回过味来:这种临时性的、靠人肉触发的弱密码排查,如果不变成一个每天自动跑的日常机制,查出来的结果再好看,本质上也只是一张废纸。
这篇文章不聊虚的,直接把我们从“临时任务”做到“日常机制”的完整路径摊开讲。包括技术路线为什么选了哈希离线检测、核心PowerShell脚本长什么样、怎么用计划任务定时调度、报表如何定向推到责任人面前、整改闭环怎么设计,以及那些文档里死活不会写的坑。如果你也在被领导点名做AD域安全巡检,或者正准备把零散的检查脚本变成一套可持续运行的机制,这篇内容应该能帮你省下不少试错时间。
1. 临时任务为什么总翻车:AD域控弱密码排查的三个死穴
1.1 弱密码在AD域内的杀伤力:一条低质量口令等于白送入场券
先别急着写脚本,得搞清楚查弱密码到底在防什么。很多人觉得域账号密码设复杂一点就没事,但在实际攻击链里,弱密码往往是攻击者进入内网的第一块垫脚石。攻击者根本不会费力去猜某个特定账号的密码,而是会拿高频弱密码列表,对域里几百上千个账号做低密度试探,比如每个账号只尝试两三个最常用的密码。这种手法不会触发账户锁定策略,却能快速筛出那些密码等于admin123、或者跟用户名一模一样的账号。
我见过不少企业,网络边界防护做得严严实实,结果内网一台跳板机上的普通域账号密码是“公司名+年份”,被攻击者撞库撞出来后直接横着走。弱密码问题不解决,后面做再多的EDR、杀软、基线审查,都像是给已经开了个洞的墙刷漆。所以AD域控弱密码排查,本质上查的不是运气,而是给内网安全补最基础的那块门板。
1.2 “来一次查一次、查完就断层”:临时排查模式的三宗罪
临时任务模式听起来执行力强,实际上问题很多。第一个死穴是没有沉淀。每次排查都是靠人肉跑一遍脚本,结果落在哪个文件夹、风险怎么分级、上次谁负责整改,全靠聊天记录和个人记忆。负责写脚本的人一旦休假或者离职,这个能力就断层了,下次检查又得从头摸索。
第二个死穴是没有基线。同样的脚本在不同时间跑出来的结果,没有历史数据做对比。三个月后领导问“弱密码数量到底是降了还是升了”,你拿不出一张趋势图,只能现场再跑一遍,然后跟去年的结果做一次连格式都对不上的对比。没有基线,优化就无从谈起。
第三个死穴是没有闭环。通报归通报,几百条账号清单往群里一扔,业务部门根本不知道哪些账号是自己的、自己该找谁改密码。没有责任人,没有期限,没有复查,那些账号就在清单里躺着,直到下次检查再一次被捞出来。可以说,临时任务模式下的排查,做得越卖力,越容易让人误以为安全状态已经掌握,实际上问题一点没变少。
1.3 机制化的目标:自动检测、持续监控、整改闭环
我们做机制化时,给自己定了五条硬指标。第一条是每天定时自动检测,不需要任何人手动触发;第二条是结果自动分级,并且落到固定格式的报告里;第三条是每个弱密码账号都能找到明确的责任人,不能只报一个账号不管后续;第四条是超期未改的账号要有升级处置机制,不能靠邮件催了就当完成;第五条是每周、每月都能看到风险趋势,用数据驱动下一轮改进。
这几条指标看起来简单,真正落地却要横跨技术、流程和制度三个层面。技术层面解决“能不能查”,流程层面解决“查了之后怎么办”,制度层面解决“为什么必须改”。很多团队卡在第一条就放弃了,其实只要把后面想清楚,第一步反而不难。
2. 技术路线怎么选:哈希离线检测为什么是当前的主流做法
2.1 在线试密码不可行:账号锁定风暴和误报告警没人扛得住
弱密码排查的第一反应,可能是拿字典去在线试密码。这种思路在测试环境跑跑还行,放到生产环境就是灾难。域控默认一般配置了账户锁定阈值,比如5次错误就锁30分钟,在线试密码只要几轮就能把一批账号锁掉。第二天早上业务部门电话会被打爆,运维同事修复账号锁定的时间比排查弱密码的时间还长,属于典型的捡芝麻丢西瓜。
更麻烦的是,在线试探还会触发安全设备的告警。现在主流IPS、EDR对暴力破解和密码喷射都有行为检测规则,一旦脚本频繁发起登录尝试,安全监控平台会立刻把它识别为攻击行为,告警刷屏还是小事,万一被安全组当成真实攻击做了封禁动作,排查任务本身就成了事故。所以我们整体评估后坚决放弃了在线检测这条路。主动去试密码这件事,无论包装得多“温和”,都不适合作为日常巡检手段。
2.2 离线哈希检测的原理:一次抓取、反复比对、业务零打扰
真正靠谱的做法是离线哈希检测。这里先说个基础概念:AD域控并不保存明文密码,NTDS.dit数据库里存的是通过加密算法算出来的密码哈希。所谓离线哈希检测,就是把域控上的账号哈希数据取出来,然后跟一个“已知弱密码的哈希集合”做比对。如果某个账号的哈希和字典中某条明文密码算出的哈希完全相同,那就能100%确定这个账号的密码就是那条明文,不存在概率问题,也不需要破解。
这里要说清楚一个容易混淆的点:拿到哈希不等于要去破解。破解是根据哈希反推明文,耗时不可控,强密码可能算几天都出不来;而我们做检测,是把字典里的明文密码事先全部转成哈希,再直接比对,属于查字典操作。几万条字典配上几万个账号,匹配过程也就是分钟级,这正是一个机制能够每天跑起来的前提。
哈希读取走的是AD的复制协议,属于正常的管理操作。脚本不会修改任何账号属性,不产生错误登录尝试,更不会触发账户锁定。凌晨三四点跑一轮,业务高峰期基本没有感知。这也是为什么离线哈希检测现在成了域环境弱密码巡检的主流路线:既能精确命中,又能做到业务零打扰。
2.3 工具选型:DSInternals、ActiveDirectory模块和字典文件怎么组合
选型阶段我们不做重复造轮子的事。哈希提取和弱密码比对,直接用微软官方开源的PowerShell模块DSInternals,它的两个核心命令很重要:Get-ADReplAccount负责枚举域账号并提取NT哈希,Test-PasswordQuality负责对账号集合执行弱密码检测和字典哈希比对。这两个命令组合起来,已经覆盖了检测的绝大部分需求,不需要再去拼装什么冷门工具。
ActiveDirectory模块则用来做关联。跑完哈希比对后,你会拿到一堆命中弱密码的账号名,但光有账号名没人知道该找谁改密码,需要把AD组织架构里已有的DisplayName、Manager、部门、邮箱这些属性拉出来。ActiveDirectory模块干的就是这件事。字典文件方面,我们一开始用的是Top 10000高频弱密码,后续在实战里逐步扩展,加入企业专属词根。这个后面会细说,但工具组合基本就这三样:DSInternals做主检测,ActiveDirectory模块做组织映射,字典文件做判定基准。
3. 核心脚本落地:PowerShell自动弱密码检测脚本怎么写
3.1 开工前准备:服务账号、最小权限、字典文件与配置文件
动手写脚本之前有三件事必须先做好,不然中途会反复返工。
第一件,创建一个专用的服务账号,比如svc_adcheck。这个账号不应是普通员工的账号,也不能随手拿一个管理员的账号来顶。权限按最小化来配,只需要能读取域内用户信息和在指定服务器上运行计划任务即可。有些团队图省事直接给域管权限,短期跑得顺,长期看风险很大,等哪天审计查起来说不清,你自己都会后悔。
第二件,在计划任务所在的服务器上装好DSInternals模块和ActiveDirectory模块。DSInternals可以用Install-Module DSInternals -Force来装。ActiveDirectory模块在Windows Server上通常需要通过“添加角色和功能”把RSAT管理工具装进去。注意检查PowerShell执行策略,直接绕过执行策略运行脚本是省事,但很容易被安全软件盯上,建议给脚本签名或者明确放行目录。
第三件,准备字典文件。我的建议是别拿网上随便下载的一个txt就往里灌,效率不高且覆盖有限。第一阶段建议至少包含Top 10000的高频弱密码,第二阶段再结合自己企业的情况加规则:公司英文名加123、域名加年份、电话号段、部门名称的变形,这些才是真正能命中内部弱密码的私货。字典文件的格式就是一行一条明文密码,简单直接。
3.2 脚本主流程:拉取所有域账号、哈希比对、逐条打标
脚本主流程我把关键代码贴出来供参考,字段名以你本地实际安装的DSInternals版本为准,建议先跑一条命令看一下对象结构再决定取用哪些字段。
Import-Module DSInternals Import-Module ActiveDirectory # 读取配置文件 $config = Get-Content -Path 'D:\Scripts\weak_check_config.json' -Raw | ConvertFrom-Json # 1. 通过复制协议拉取所有域账号的哈希数据 $accounts = Get-ADReplAccount -All -Server $config.DCServer # 2. 过滤掉禁用账号、计算机账号和系统账号 $targetAccounts = $accounts | Where-Object { $_.AccountEnabled -and $_.SamAccountName -notmatch '\$$' -and $_.SamAccountName -notmatch '^(krbtgt|Guest)$' } # 3. 与字典中的弱密码哈希做比对 $weakResults = $targetAccounts | Test-PasswordQuality -DictionaryFilePath $config.DictionaryPath # 4. 关联AD属性,生成可读报告 $report = @() foreach ($item in $weakResults) { $adUser = Get-ADUser -Identity $item.SamAccountName -Properties DisplayName, Manager, mail, department, pwdLastSet -ErrorAction SilentlyContinue if (-not $adUser) { continue } $report += [pscustomobject]@{ 账号 = $item.SamAccountName 姓名 = $adUser.DisplayName 部门 = $adUser.department 直属负责人 = $adUser.Manager 邮箱 = $adUser.mail 弱密码类型 = $item.MatchedPassword 哈希前八位 = $item.NTHash.Substring(0, 8) 检测时间 = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' } } $timestamp = Get-Date -Format 'yyyyMMdd_HHmm' $report | Export-Csv -Path "D:\Reports\weak_password_$timestamp.csv" -Encoding UTF8 -NoTypeInformation这段脚本里的几个过滤条件值得多说两句。过滤禁用的账号是因为禁用账号本来就不该被纳入整改范围,至少不该和正常账号一视同仁,否则人是找不齐的。过滤计算机账号是因为计算机账号的密码是系统随机生成的,不在弱密码排查范围内。过滤krbtgt和Guest是系统的保留账号,动它们的密码属于高风险操作,最好不要放在自动化流程里。
另外,如果你对哈希的安全性有顾虑,可以不让脚本在输出报表里带完整哈希,只保留前八位作为去重和后续审计的索引。这样即使报表被转发出去,也不会直接泄露可用于离线破解的信息。
3.3 结果标准化:风险等级怎么定,数据字段留哪些
拿到一堆命中结果后,不能全用一个标准处理。我们在配置里把弱密码分成了三档,分别对应不同的处置力度,后续整改流程全靠这个分级驱动。
| 风险等级 | 判定条件 | 处置时限 | 处置动作 |
|---|---|---|---|
| 高危 | 空密码、密码与用户名完全相同、命中Top 100高频字典 | 24小时 | 管理员强制重置,通知本人下次登录必须改密 |
| 中危 | 命中Top 1000字典、用户名简单变形加常见数字序列 | 7个自然日 | 邮件提醒本人自助改密,并提示风险说明 |
| 低危 | 命中扩展字典、仅大小写替换或简单键盘序列 | 30个自然日 | 纳入下月复查观察,仅在连续出现时升级 |
分级是必要的,但要注意别把判定标准写死在一个封闭里。不同企业的密码策略差异很大,有的域里强制16位长密码,空密码基本不存在;有的域里还是8位字符的老策略,命中规则就完全不同。最好的做法是把各级别命中的规则和字典路径全部放进配置文件,比如一个JSON,后续调整规则不用改代码。我们在实际运行中基本每季度都会调一次阈值,字典和规则文件分开维护,谁改了什么一目了然,审计也说得清。
报告字段至少要有账号、姓名、部门、直属负责人、邮箱、风险等级、弱密码类型、密码设置时间、最近登录时间、检测时间。有了这些字段,后面做定向发信、生成统计报表、追责负责人这些环节才不会卡壳。有人会问最近登录时间有什么用?很有用。一个账号密码弱但最近一年都没登录,风险相对低;如果最近几天还在活跃登录,那基本就是高危中的高危,需要优先处置。
4. 从脚本到机制:计划任务调度、报表推送与整改闭环
4.1 用计划任务把脚本变成每日巡检项:配置细节和注意事项
脚本本身不难,难的是让它稳定地每天自动跑。我们的做法是在巡检服务器上用计划任务调度。创建命令大致如下:
schtasks /Create /TN "AD弱密码巡检" ^ /TR "powershell.exe -ExecutionPolicy Bypass -File D:\Scripts\weak_password_check.ps1 -Config D:\Scripts\weak_check_config.json" ^ /SC DAILY /ST 04:00 ^ /RU "DOMAIN\svc_adcheck" /RP "此处不展示密码" ^ /RL HIGHEST命令里的几个细节需要特别注意。一是执行时间选在凌晨3点到5点之间,要避开AD备份时段和业务批量任务。我们最开始没注意,把任务排在凌晨1点,结果跟系统备份撞车,两边抢IO,脚本跑了三个多小时。二是计划任务的运行账号要用前面创建的svc_adcheck,并且在任务计划程序的属性里勾选“不管用户是否登录都要运行”,否则账号没登录会话的时候任务根本不会执行。三是脚本内部要做执行状态记录,简单地在固定路径写一个带时间戳的日志文件;如果某个环节抛异常,计划任务的返回码要是非零,这样后续才能通过监控看到异常。
另外一个实操经验:哪怕计划任务配好了,也要在第一周每天确认结果文件都正常生成。“零结果”有时候并不是真干净,而是脚本某个环节静默失败了。我们曾经因为一个版本更新导致Get-ADReplAccount连不上域控,脚本没有加错误处理,直接输出了一个空结果,差点让安全组误以为自己域环境已经没有任何弱密码。从那时起,脚本就加了一句:如果提取到的账号数小于某个阈值,不输出空报告,而是把异常状态单独写进日志,这才解决问题。
4.2 结果沉淀与推送:CSV存档、HTML报表和即时消息摘要
结果不能躺在服务器的文件夹里,要让需要看的人每天自动收到。我们的做法是每次巡检生成两类文件:一份CSV用于存档和二次分析,一份HTML用于给人阅读。HTML报表除了弱密码总数、高/中/低分布、按部门分组的表格之外,还会把过去30天的趋势用简单表格列出来,虽然不花哨,但足够支撑日常判断。
推送方式上,会同时走两条线:一条是汇总报表,发给运维组和安全组;另一条是定向明细,发给每个弱密码账号的直属负责人。这里有个边界要把握好:在邮件和即时通讯摘要里,不推送完整哈希、不推送密码明文,只报账号、部门、风险等级和问题类型。比如“市场部张三的账号,被检测到命中常见弱密码,请在7天内修改”,这样信息量已经足够执行,又没有额外泄露风险。
有一点我要特别提醒:发给日理万机的业务负责人之前,一定要把话术改成能让人立刻行动的口吻,别把几百条账号做成大附件发出去,指望对方自己筛选。信息只有主动跑到责任人面前,才叫推送;放到群里让别人自己下载,那只是存档。
4.3 整改闭环设计:责任到人、逾期升级、自动处置
机制化的核心不是自动检测,而是整改闭环。检测得再准,改不了密码,就是在给领导演戏。我们的闭环分四步走。
第一步是责任到人。报表要能自动找到每个弱密码账号在AD里的Manager属性,系统会在当天给这个负责人发一封邮件,说明他的下属中哪个账号命中了什么风险等级、需要在什么时间前处理。负责人不需要懂技术,只需要知道“找这个人改密码”就行。
第二步是限期整改。高危账号24小时内必须处置,处置动作是管理员手动重置密码并设置“用户下次登录时须更改密码”;中危账号7天内自助修改;低危账号30天内观察。我们明确不做无人值守的自动改密,原因很简单,改密之后业务系统登录受影响,没有审批链路的自动操作会带来比弱密码本身更大的麻烦。
第三步是逾期升级。脚本每天巡检时会比对历史报告,如果某个账号过了期限还没整改,系统会给它的直属负责人再发一封邮件,同时抄送给安全负责人,并在群内通报。这一步看着有点不留情面,但它确实是把“通报”和“执行”区分开的唯一手段。没有升级机制的闭环,最后结果就是“通知了但没整改”。
第四步是统计复盘。每个月度报告中统计弱密码率(弱密码账号数除以启用用户账号数)、整改率、逾期率,与上月和上季度做对比。整改进度好的部门在群里给予正面反馈;连续三个月不降的部门,就把统计结果直接抄送部门负责人和分管领导。
这套闭环跑通之后,你会发现弱密码数量不是靠安全团队盯出来的,而是靠流程中的每一个责任人共同扛起来的。
5. AD域控弱密码排查看似简单,实际踩坑比想象中多
5.1 权限不足、哈希为空、杀毒软件误报:技术坑逐个拆
技术层面的坑,最常见的三个都跟环境相关。
第一个是权限不足。Get-ADReplAccount提取哈希的操作,要求运行账号具有相当于域控本机SYSTEM的管理权限,普通域用户跑这个命令会直接报“拒绝访问”。我们的做法是用域管理员账号做部署和数据源验证,日常计划任务则使用专用服务账号并配合最小权限委派,同时把授权范围限定在读取AD复制数据上。必须承认,这里有一个天然矛盾:完全无权限的账号跑不了哈希提取,权限太大的账号又有被滥用的风险。折中方案是在域控上以SYSTEM权限运行采集脚本,结果投递到共享目录,再由另一台权限较低的服务器做比对和发信。这个方案对架构要求更高,但隔离效果确实更好,适合对安全要求比较严格的环境。
第二个是提取出来的哈希显示为空或N/A。排查重点要看账号是不是被禁用,以及脚本账号是否对这些账号有读取权限。域内有些受保护账号组和特殊对象,普通委托权限不一定读得到。处理方式很简单,把这些对象从自动化巡检范围内排除,单独用人工流程复核。
第三个是杀毒软件和EDR误报。抓取哈希这个动作本身,和恶意攻击里的“哈希转储”行为在外观上非常接近,安全软件大概率会告警,甚至直接拦截。解决的思路不是跟杀毒软件对着干,而是把脚本目录加入白名单,同时提交一个内部合法用途审批工单,并对脚本文件做数字签名。团队的安全组也会因此注意到你,与其等告警误伤,不如主动同步专项用途,省得两边互相解释。
5.2 字典质量决定结果上限:误报漏报如何动态校准
第一次上线跑的时候,我们用了公开的Top 1000字典,结果出来的弱密码数量少得可怜,当时还沾沾自喜以为环境很干净。后来换了一套更完整的字典,并加入企业专属词条,数量直接翻了几倍。这个反差说明一个道理:弱密码排查的结果上限,是由字典质量决定的,工具本身只是执行器。
在字典建设上,我建议采用“公开字典打底,企业词根补充”的组合方式。公开字典覆盖普遍习惯的弱密码,比如123456、password、qwerty这些;企业词根则是注册域名、公司简称、成立年份、电话号段、产品名这些,并按常见的密码构造规则做变形,比如只加一个年份、只改大小写、尾部加数字序列。有一位同学的做法很值得参考,他们把办公系统里若干服务账号的默认密码格式也加了进去,立刻命中了一批线上风险。
漏报要靠字典持续更新来治理,误报也真实存在。比如账号本身就叫“liang123”,公司人也认识他,但这并不能改变“用户名和密码高度重叠”这个事实,这种账号在攻击者眼里依然是低悬的果子,所以我们坚持把这类结果归为中危,不降级。字典整体要至少每季度对标一次新增的公开泄露密码样本库,不需要全部导入,只取增量部分即可,否则脚本会越跑越慢。
5.3 “报表发了没人看”怎么破:把结果直接推到责任人面前
技术坑填完之后,真正的考验是人的问题。第一周我们把汇总报表发到工作群,群里安静得吓人,安全组以为大家默认认领了问题,实际上根本没人点开附件。后来改变策略:不再发大而全的汇总表,而是针对每个Manager生成专属报表,里面只有他管辖的下属账号的命中情况。一对一推送以后,打开率和响应速度立竿见影。
还有一个实操技巧:单次弱密码重复率连续三周不降,说明这不是技术问题,而是制度执行问题。这个时候再发多少封提醒邮件都没用,把统计结果抄送部门负责人,效果比什么都强。“让结果找到责任人,让责任人感受到压力”,这句话被说滥了,但真正把它落实到报表设计里,才会看到数据层面的变化。
此外,我们每月的复盘数据里还会算一个“新增弱密码占比”,也就是这个月新命中的账号里,有多少是上个月不在名单里的。这个数字如果持续偏高,说明整改动作虽然在做,但新账号还在不断产生弱密码,需要把配置标准往前推一步,比如通过密码策略或者入职流程去堵源头。机制走到这一步,才算是真正进入了主动治理的状态。
我实际落地这套机制下来,最大的体会是技术脚本只占三成,剩下七成都是流程设计。DSInternals和PowerShell都是现成的,把脚本放进计划任务也就半天时间,真正难的是让结果跑到每个责任人眼前,并且有人真的动手改密码。我们内部迭代了好几轮,从最开始每天跑一次推到群里,到按部门自动定向通知,再到把逾期未改的账号临时禁用,每一步都是被现实逼出来的。尤其想提醒一点:不要因为嫌业务部门反应慢,就自己做全自动改密,没有审批链路的自动写操作,出了事故责任很难说清,得不偿失。如果你的域环境迟早也要面对这种检查,别等领导点名才动手,先把一个最小的检测闭环跑起来,哪怕只查高危,后面再逐步加字典、加推送、加升级机制,这套机制就会自己慢慢长出筋骨。最后再分享一个我个人的小习惯:每次更新字典和规则后,都会手动触发一次巡检,亲眼确认结果文件正常生成再离开,这份多花的三分钟,已经替我挡掉了好几次“零结果事故”。