做安全测试的人多多少少都遇到过这种场景:手头有一批密文或者哈希,常规密码猜测字典跑完一轮,命中率惨不忍睹,转头想自己做一份专属字典,却不知道从哪里下手。网上的通用字典动辄几个GB,看着很唬人,但真正命中时靠的往往是运气——因为那是“大家的密码”,不是“他的密码”。
这里说句大实话:绝大多数人设置密码时,根本不是在想一个随机字符串,而是在拼凑自己的生活细节。比如“姓名拼音+生日”、“英文名+手机号尾号”、“邮箱前缀+年份”、“女友名字+1314”,这些模式在脑子里根深蒂固。所以当你手里已经掌握了目标对象的个人化信息时,完全可以通过一套自动化脚本,把这些公开信息转换成一批高度贴近对方习惯的密码候选,生成一份个人化密码猜测字典。
这就是我今天要分享的项目:一个基于个人化信息生成密码猜测字典的自动化脚本。它可以接收目标的一张“信息画像”,自动完成特征词提取、数字扩展、常见的密码变形组合、去重裁剪,最终产出一份可直接用于密码安全审计、弱密码自检、员工安全意识培训的小而精字典。
先说明白它的定位:它不是用来取代通用字典的,而是用来“补刀”的。通用字典覆盖大众化弱密码,个人化字典解决的是“这个人会怎么想密码”的问题。适合在授权测试、内部红队项目、CTF、密码强度评估里使用,也适合安全培训时给员工做个直观演示。
下面我把这个项目的设计思路、核心实现、踩坑记录和建议边界一次性讲清楚。
1. 不是先写代码,而是先想清楚“密码是怎么被想出来的”
很多人拿到这个需求,第一反应是写个脚本处理字符串、做排列组合,搞个笛卡尔积。但真正做下来你会发现,最难的不是代码,而是怎么把“人脑里的密码规则”翻译成机器能执行的组合模板。
1.1 为什么通用字典解决不了个人化问题
通用字典的典型代表是rockyou、probable_w2l等,它们本质上是大规模泄露密码的统计结果。这类字典对“password123”、“qwerty”、“iloveyou”这种全球性弱密码确实有效,但一旦遇到中文用户习惯的“zhangwei19920815”、“zw@7613”这种结构,基本就只能靠猜了。
我做过一组经验对比(不是严谨论文,是多次授权测试的参考值):同样是100万级别的字典规模,通用字典在针对某个具体目标时的实际命中率可能只有5%到15%,但如果手里有几条可靠的个人信息,用个人化字典规模砍到几万条时,命中率反而能到30%甚至更高。道理很简单——密码不是一个均匀分布的东西,它高度集中在每个用户自己的生活语境里。
所以这个脚本的核心价值不是“量”,而是“准”。它把目标在社交平台、注册资料、公开主页、旧密码习惯里暴露的信息,转换成一批对方大概率会用的密码候选。
1.2 设计目标:要人味,不要暴力排列
我在设计脚本时给自己定了几个硬性标准:
第一,生成的字典必须带“人味”。不能只是把所有词和数字做笛卡尔积,那样出来的东西既多又没用。要把密码习惯抽象成模板,比如“基础词+年份”、“基础词+特殊符号+手机尾号”、“姓名+生日”这种真实存在的思维路径。
第二,配置文件必须和代码分离。目标信息经常变,如果每次都要改Python代码,那就失去了“自动化”的意义。我用YAML配置文件描述一个人,脚本只负责读配置、算候选、输出字典。
第三,字典规模必须可控。组合爆炸是这类脚本最容易踩的坑,基础词30个、数字表20个、模板20个,最后一算几千万条,根本没法用。所以脚本必须内置去重、长度过滤、总量上限、优先级分层这些能力。
第四,输出格式要干净。生成的字典不只是给人看的,最终要丢给hashcat或者其他工具去跑,所以换行、编码、去重、排序都不能有坑。
1.3 技术选型:Python加几个标准库就够了
整个脚本我用Python 3写成,依赖尽量少,核心用到的模块是argparse、itertools、yaml、json、pathlib,以及可选的颜色输出和进度条。为什么不用现成的crunch或cewl?
crunch确实强大,但它擅长的是基于字符集和位数的排列组合,它不理解“姓名拼音”和“生日”之间的关系,也不懂得“中文名转拼音首字母”这种操作。cewl擅长从网站上抓取关键词,但抓回来的是一个个零散的词,缺少结构化的身份信息维度,更没法按“家庭成员名+纪念日”这种逻辑组织。
自己写的好处是逻辑完全可控。我需要的是一个能理解“这个人”的工具,而不是一个泛用的字符生成器。说白了,crunch是给“暴力人”用的,我这个脚本是给“懂人的人”用的。
2. 个人信息画像:把目标已知信息整理成结构化配置
个人化字典的上限,取决于你手里有多少真实信息。信息整理这一步是整个项目的地基,做扎实了,后面生成器才有效果。
2.1 核心信息维度与配置格式
我设计了一套YAML配置格式,基本覆盖了做个人化字典时需要的高价值信息。不同字段的重要程度不一样,建议在采集阶段先抓高权重的几个,比如姓名、生日、手机尾号、旧密码习惯。
name: chinese: "张伟" pinyin: "zhangwei" initials: "zw" alias: ["weiwei", "zw92"] birthday: "1992-08-15" phone_tail: "7613" email_prefix: "zhangwei92" company: "ABC科技" school: "北京某大学" family: partner: "liyan" child: "zhangyue" pet: "mimi" hobbies: - "basketball" - "guitar" memorable_years: - "2010" - "2014" - "2019" lucky_numbers: - "7" - "1314"这套配置里,name字段是核心。chinese是中文名,pinyin是完整拼音,initials是首字母缩写,alias放各种昵称和旧网名。birthday会同时派生出多种格式:完整八位、六位、月日、年份等。phone_tail可能只有后四位,但同样有很高的信息量。company、school、hometown这类归到“组织与地理”维度,虽然权重不如姓名生日,但很多密码习惯会把公司名、学校名放进去,特别是那种被强制要求大小写加数字的场景。
family和hobbies这两个字段容易被忽略,但实际命中率极高。把伴侣名字、孩子名字、宠物名作为基础词放进字典,往往能覆盖到那种“看似复杂实则走心”的密码类型。
2.2 中文拼音与别名的自动化处理
中文用户的密码习惯和英文用户有明显差异,脚本里必须内置几层转换逻辑。
第一层是中文名转拼音。既然配置里给了完整拼音和首字母,脚本就直接使用。如果只有中文名,可以再用pypinyin转,但为了减少依赖,我推荐直接让使用者填好默认的信息入口。
第二层是衍生写法。比如“张伟”(Zhang Wei),实际密码里会出现这些变体:zhangwei、zhang_wei、zhangwei、zw、zhw、weizhang、wzhang、wei。这些衍生词不需要每个都让用户手填,脚本会在构建基础词表时自动扩展。
第三层是别名与旧昵称。很多人真正用来做密码的不是本名,而是自己长期使用的网名、邮箱前缀、游戏ID。alias字段一定要尽量收集,甚至比本名还重要。我见过不少案例,密码里出现的完全是某个早期QQ昵称,和真名毫无关系。
至于大小写和分隔符,不要在这一步处理,那个留给后面的变形规则,否则词表会膨胀得非常快。
2.3 公开信息收集的合规边界
这个部分我必须说清楚:个人化信息收集只应该来自合法获取的已知信息,比如对方自己公开的资料、授权测试中客户明确提供的员工名单、你在CTF题目里拿到的线索、或者安全意识培训中同事自愿提供的信息。
千万不要为了生成字典去写爬虫抓人隐私,也不要去碰那些需要绕过权限才能拿到的数据。脚本本身是无辜的,但数据来源不合规,整个测试就变味了。
我在实际项目中遇到过客户只给了一个姓名加一个手机号的情况,也遇到过从公开渠道合法整理出几十条信息的情况。信息越多字典越准,但哪怕只有一条可靠的“姓名+年份”,也值得把这套流程跑一遍。
3. 核心生成逻辑:特征词、变形与组合模板
这一步是整个脚本的心脏。前面整理的信息画像还只是原料,生成器要做的是把原料加工成有实际命中概率的密码候选。
3.1 特征词、数字与特殊符号的构建
先设计三类基础元素:基础词表、数字表、特殊字符表。
基础词表的构建逻辑是把配置里所有字段转成可用字符串,然后做去重和过滤。比如“张伟”会产生zhangwei、zw、weiwei、zw92、liyan、zhangyue、mimi、basketball、guitar、ABC科技、北京某大学这一批词。数字表则会把生日解析成多种常见格式,再把手机尾号、纪念年份、幸运数字汇总进去,最后追加一些用户密码中高频出现的数字尾巴,如520、1314、521、666、888。特殊字符表就是那一小批常见符号。
def build_numbers(cfg): nums = set() bd = cfg.get("birthday", "") if bd: parts = bd.split("-") year, month, day = parts[0], parts[1], parts[2] nums.add(year) # 1992 nums.add(year[2:]) # 92 nums.add(f"{month}{day}") # 0815 nums.add(f"{year}{month}{day}") # 19920815 nums.add(day + month) # 1508 nums.add(cfg.get("phone_tail", "")) nums.update(cfg.get("memorable_years", [])) nums.update(cfg.get("lucky_numbers", [])) nums.update(["123", "520", "1314", "521", "666", "888"]) nums.discard("") return sorted(nums, key=len, reverse=True)注意一个细节:数字表里有些字段可能是纯数字字符串,有些可能是带前导零的月份,不要统一转int,否则“0815”会变成“815”,就少了用户当年输入时的手感。
3.2 变形规则与组合模板
基础词表构建完成后,不是直接拿去和数字表做笛卡尔积,必须先做一轮“变形扩展”。常见的变形包括首字母大写、全大写、全小写、leet替换(a变成@,e变成3,i变成1,o变成0,s变成$)、末尾补1、末尾补0等。
leet替换尤其有用,因为很多平台强制要求密码里必须有特殊字符,用户的惯用做法就是用@替代a、用1替代i。我在脚本里维护了一张映射表:
leet_map = { "a": "@", "e": "3", "i": "1", "o": "0", "s": "$", "g": "9", "b": "8" } def apply_leet(word): for src, dst in leet_map.items(): word = word.replace(src, dst) return word组合模板是整个脚本的灵魂。我根据大量授权测试的样本习惯,归纳出几个命中率明显偏高的模板顺序。注意模板的顺序很重要,因为后面的总量截断会优先保留靠前的候选,所以一定要把“最像人会用的组合”放在前面。
templates = [ "base", # zhangwei "base+year", # zhangwei1992 / 1992zhangwei "base+special+year", # zhangwei@1992 "base+phone", # zhangwei7613 "base+special+phone", # zhangwei#7613 "base+base", # zhangweiliyan "base+special+base", # zhangwei@liyan "year+special+base", # 1992@zhangwei ]每个模板组合不断追加数字、追加特殊字符、追加家庭成员名,不会一次性全部拼满,否则生成的字典规模会快速失控。正确做法是先用低阶模板拼出一个小而精的字典,测试命中情况后再按需打开更高阶的模板。
3.3 去重、截断与输出
生成过程中会产生大量重复项,比如“zhangwei1992”可能同时来自base+year和base+phone两个模板的跨界组合。所以脚本里用一个set去重,再按长度过滤,默认过滤掉小于6位和大于20位的候选。这一步不能省,因为很多在线系统对密码长度有下限要求,太短的候选跑也是白跑。
去重后还要面对一个现实问题:最终字典可能还是太大。我的做法是引入一个总量上限参数--limit,默认值可以设成20万条。达到上限后不再继续扩展,用通过“模板顺序+词表顺序”保留下来的都是优先级最高的候选。实际使用中我通常先跑一个1万条的小字典试水,看看命中情况,再决定要不要放开限制。
输出格式方面,用一行一条、LF换行、UTF-8编码。这点我在后面问题排查部分会详细展开,因为Windows环境下很容易踩换行符和编码的坑。
4. 完整实现与运行演示:一套可复用的Python脚本
前面把逻辑讲清楚了,这节直接看代码。脚本不复杂,去掉注释大概两百多行,核心逻辑集中在构建词表、生成候选、去重裁剪三个函数里。
4.1 命令行入口与主流程
命令行参数只保留四个必要的:--config指定YAML配置文件,--output指定输出字典文件,--limit指定生成数量上限,--min-len和--max-len控制长度范围。
python3 gen_dict.py -c target.yaml -o dict.txt --limit 200000 --min-len 6 --max-len 20主流程分四步执行:加载配置、生成基础元素、遍历组合模板、去重裁剪后写入文件。每步之间用日志把词表大小、数字表大小、生成总量及时打印出来,方便判断是哪个环节导致字典膨胀。
4.2 一个虚构目标的配置与运行结果
完整代码放到这里篇幅会太长,拆解核心函数更实用。我用一个虚构人物演示整个流程。假设目标信息如下:中文名张伟,拼音zhangwei,首字母zw,昵称weiwei,生日1992-08-15,手机尾号7613,邮箱前缀zhangwei92,伴侣liyan,宠物mimi,爱好basketball和guitar,纪念年2019。
这个配置喂给脚本后,基础词表大致会包含zhangwei、weiwei、zw、zw92、liyan、mimi、basketball、guitar等几十个词。数字表包含1992、92、0815、1508、19920815、7613、2019、520、1314等一批数字。经过变形和组合后,生成的字典开头大概长这样:
zhangwei zhangwei1992 1992zhangwei zhangwei@1992 zhangwei0815 zw1992 Zw@1992 weiwei2019 liyan1314 zhangyue0713注意看这些候选,它们不是随机拼出来的,都是按照“人脑里真实的密码公式”推出来的。“liyan1314”这种组合,在通用字典里出现的概率几乎为零,但在个人化字典里命中率非常高——很多人的密码就是伴侣名字加一个表白数字。
我建议你运行完之后,先不要急着把整个字典丢进工具跑,而是自己人眼扫一遍前面几百条,看看有没有那种“目标本人看到会愣一下”的候选。如果一条都没有,说明配置里的信息还不够全,或者模板顺序需要调整。
4.3 与常用密码测试工具的衔接
生成字典只是第一步,最终要交给工具去跑。我自己最常用的场景是hashcat的离线哈希测试。比如把生成的字典保存为dict.txt,哈希文件是hash.txt,跑字典模式:
hashcat -a 0 -m 0 hash.txt dict.txt这里多说一句,hashcat这种工具本身是安全审计和密码恢复领域的常规工具,用在授权测试、自己搭建的靶场、CTF比赛里是完全没有问题的。字典生成器只是负责给它提供更精准的输入,而不是制造攻击能力。
也有不少人把这套脚本产出的字典和hydra这类在线测试工具做联动,但我建议你在正式用之前先确认清楚:目标系统是否具备明确授权?测试范围是否覆盖这个目标?测试时段是否在允许范围内?这些合规步骤比字典本身重要一百倍。
5. 实战中的常见问题与排查技巧
这部分内容是我反复跑这个脚本后总结出来的,每条都是实际踩过的坑,值得仔细看一遍。
5.1 字典数量爆炸的控制策略
最常见的问题是:配置信息不多,但脚本跑完生成了几千万条候选。原因几乎都是组合模板没有控制好,尤其是包含多个基础词的组合模板,一旦基础词表里有几十个词,笛卡尔积的规模立刻指数级上升。
我建议用“分层扩展”策略应对。第一层只生成单个基础词的变形和加数字的模板,第二层才加入基础词与基础词组合、特殊字符组合。每层生成的候选先做去重和数量统计,如果第一层就已经超出预期总量,就不要再展开第二层。脚本里可以通过--limit参数配合分层逻辑,实际上就是把爆炸点提前挡住。
另一个技巧是优先保留短模板产出的候选。一个8到12位的“姓名+生日”候选概率远高于一个20位的“姓名+姓氏+伴侣名+特殊字符+四位数字”的超长组合。长度本身就是权重信号,短而合理优先输出。
5.2 命中率低时的调整思路
如果生成出来的字典跑了半天没有任何命中,先不要急着加更多模板,先做一件事:回看配置信息是不是太单薄。个人化字典的准确率直接取决于基础词表的质量,词表里只有两三个词,再强的生成器也变不出花样。
其次检查特殊字符表。很多平台强制要求密码包含特殊字符,所以用户一定会给某个基础词加上@、!、#这类符号。如果目标的使用场景是这类平台,而你的模板里又忽略了“基础词+特殊字符+年份”这个组合,命中率自然会低得很。
还有一种情况是顺序问题。比如目标习惯“生日开头+姓名结尾”,你的模板只生成了“姓名+生日”,就会漏掉真实候选。我的做法是每个核心组合模板都生成正反两个方向,也就是“A+B”和“B+A”都保留。
5.3 编码、换行与兼容性
这个坑非常隐蔽。Windows环境下,用open函数直接写txt文件时,默认会用系统区域的编码,很可能在中文环境里写出GBK内容,而hashcat在Linux环境里读取时会出现乱码。我最后统一用UTF-8编码写入,并且在打开文件时显式指定newline="\n",避免把Windows的\r\n也写进字典,导致某些工具判断候选时出错。
with open(output_path, "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(candidates))此外,终端输出时如果包含中文字符,在Windows的cmd里偶尔会报GBK编码错误,解决办法是给终端输出包装一个异常处理,或者只打印ASCII字段。我在脚本里把所有日志信息都用英文加数据输出,彻底避开编码问题。
5.4 自动化集成的工程实践
这个脚本设计出来就是为了自动化。我在内部项目里经常把它接进一个pipeline:由信息收集模块产出YAML配置,脚本读取配置后生成字典,最后通知下游工具开始跑测试。
工程化的几个关键点是要保证配置、代码、输出路径完全分离,脚本内部不要出现任何硬编码的目标信息;参数全部走命令行或环境变量,方便在不同项目间复用;每次生成后记录日志和字典行数,便于复盘不同信息维度对命中率的实际贡献。
如果你要定时重跑,比如每周更新一次目标画像并重新生成字典,直接把脚本挂进crontab或者CI的定时任务即可。因为整个脚本是无状态的,输入一份配置,输出一个字典文件,不会留下中间状态,非常适合批处理。
6. 边界与安全意识:这些规则的另一面
写到这里,我必须把最重要的事情说透:这样一套生成密码猜测字典的自动化脚本,本质上是一把双刃剑。
6.1 授权永远在第一位
所有密码猜测、密码恢复、弱密码验证相关的工作,都必须建立在明确授权和合法用途的前提下。适合用这个脚本的场景包括:你自己拥有的账号做密码找回测试、公司在内部授权范围里做员工弱密码审计、你搭的靶机或者CTF题目、以及安全意识培训中的演示环节。没有授权,哪怕只是跑字典这个动作,都可能触碰法律红线。
我见过有些测试人员最喜欢问的一句话是“这个工具能不能打某某系统”,每次听到这种问题第一反应都是先确认:你有没有书面授权?没有授权就是不能碰,这不是胆子小,是这行的基本职业道德。
6.2 这个脚本对普通用户的警示价值
从另一个角度看,这个脚本对普通用户也很有教育意义。我给不少团队做过安全意识培训,现场演示的效果往往比讲PPT好得多。找一位自愿配合的同事,收集几条他公开过的信息,生成一份个人化字典,再让他自己看看里面有没有“感觉眼熟”的密码候选。很多人看完都会愣住,因为他们发现自己觉得“很私密、很安全”的密码,其实早就写在了自己公开信息的排列组合里。
这也是我一直坚持用密码管理器生成随机长密码的原因。我不能保证所有人都记住一长串随机字符,但至少可以用工具把“个人化组合”这条路径彻底断掉。密码的安全边界不该建立在“大家都猜不到我的习惯”上,而应该建立在“即使知道我的全部习惯,也算不出我的密码”上。
再说个小技巧:如果你拿这个脚本做培训演示,一定先设置好总量限制,别让字典膨胀到几十万条,培训现场几万条足够震撼了。另外要记得提前跑一遍,避免现场因为编码或者路径问题翻车,那种尴尬经历过一次就不想再经历第二次。
这个项目做到最后,我的体会是:技术难度真的不高,真正值钱的是对“密码是人性问题”这个判断的坚持。当你把目标画像、组合模板、去重策略这些环节一步步跑通之后,你会发现它本质上是把“了解一个人”这件事,翻译成了机器能处理的规则。理解人,然后用工具去验证这个理解,这大概就是个人化字典这个项目最有意思的地方。