ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WebCrack后台弱口令与万能密码批量检测实践

WebCrack后台弱口令与万能密码批量检测实践

简介:面向网络安全从业者与渗透测试学习者的Web后台弱口令检测工具源码包,聚焦弱口令与万能密码的批量检测场景,可用于授权范围内的安全审计与风险排查。压缩包共18个文件、约114KB,以12个Python脚本为核心,辅以3个txt文本、Markdown说明、PNG截图及gitignore等辅助内容;目录按解析、爆破任务、字典生成、请求头处理、日志记录与配置等模块组织,结构紧凑,便于理解工具整体设计。已有407人学习下载。读者可借此研究批量爆破的任务调度与字典生成思路,观察登录请求构造、结果日志与参数配置之间的关系,并据此梳理弱口令排查流程和脚本改造方向;适合作为安全工具源码阅读与实验练习的参考素材,使用时需遵守法律法规并控制检测范围。

1. 后台弱口令检测,先划清边界再谈工具

前阵子一位做运维的同行遇到一件很具体的麻烦事:他手上要清点的后台入口有四十多个,分散在几套自建系统和几台内网设备上,域名不一样,登录框长得也不一样。领导问的是“这些后台到底有没有在用弱口令”,他手工试了两天,试了不到十个就想放弃。翻了一圈,他找到了 WebCrack——一款开源免费的web 批量爆破工具,主打的正是弱口令和万能密码的批量检测。它干的事很窄:把一堆后台登录地址喂进去,拿字典挨个试,把能进去的挑出来。窄,但在资产盘点这种场景里,比通用扫描器更直接、更好收口。

适合谁:手里有几十上百个“自己负责、有书面授权”的后台入口,需要做一轮口令强度自查的人;或者是做交付前安全基线检查,需要一份“哪些后台还在用 123456”的清单。不适合谁:想拿它去打不属于自己的站点。这类工具一旦越界,性质就变了,后面所有内容都建立在一个前提上——目标必须是你拥有的或已获得书面授权的系统,这一点先摆在最前面,后面不再重复。

2. WebCrack 的运行链路:从目标清单到命中判定的四段拆解

要从“能跑起来”到“跑得准”,得先知道这一条链路里到底经过了几道手。WebCrack 的结构并不复杂,拆开看就是四段:判定逻辑、目标加载、请求调度、结果落盘。每一段都有自己的边界条件,任何一段处理错了,最后的命中清单就是错的,而且是那种“看起来跑完了”的错,最难发现。

2.1 万能密码和弱口令,走的不是同一条判定路径

这两件事经常被混着说,但它们在工具内部是两条独立的判定线,字典结构、请求构造、成功判据都不一样,配置的时候千万别当成一回事。

弱口令这条路是老老实实的“账号 + 密码”组合尝试。字典每行是一组用户名:密码(分隔符要看工具的约定,常见是冒号或制表符),工具把每一组拼成一次正常的登录表单提交,然后看返回结果是否落在“登录成功”的特征上。它的本质是穷举,靠的是字典质量和目标的口令习惯,命中率高不高,八成取决于你字典里有没有覆盖到那批人的命名习惯——比如运维喜欢用系统名+年份,测试环境喜欢用test/test。

万能密码这条路完全不一样,它试的不是“某组账号密码对不对”,而是“这个登录接口的 SQL 拼接有没有绕过空间”。典型 payload 是往用户名和密码里塞类似' or '1'='1这样的构造,赌的是后端把用户输入直接拼进了查询语句。这条路的成功判据比弱口令更脆弱,因为它依赖返回体的差异,而很多框架在登录失败和成功时返回的 HTTP 状态码都是 200,只能靠跳转地址、Cookie 变化或者页面里的某段特征串来区分。

提示:两条线要分开跑、分开存结果。混在同一个任务里,一旦命中,你分不清是“口令弱”还是“接口有注入”,后续的处置动作完全不同。

还有一个容易被忽略的点:万能密码检测对“登录失败次数限制”极其敏感。很多后台有连续失败 5 次锁定账号或封 IP 的策略,弱口令检测撞上这个策略顶多是这条组合失败,万能密码检测如果连着发同一种构造,很容易直接把源 IP 送进黑名单,后面所有目标都跑不动。常见做法是把万能密码检测单独拎出来,用低并发、带间隔的方式跑,不要和弱口令任务混在一起。

判定逻辑这一段决定了你对结果的信任度。我一般的习惯是:先跑弱口令,拿到一批疑似命中,人工抽验两三条,确认判定规则没跑偏,再去跑万能密码。顺序反过来的话,一旦万能密码那批产生大量误报,你会先怀疑工具,再怀疑网络,最后才想到是判定规则的问题,来回折腾的时间比跑任务本身还长。

2.2 目标清单怎么加载、怎么去重

WebCrack 的目标输入是一份 URL 清单,一行一个地址。看起来最简单的一步,实际上是最容易翻车的一步。清洗过的清单和没清洗过的清单,跑出来的覆盖率能差一倍。

需要满足的条件有这么几条:每条地址必须带协议头(http://或https://),不能是裸域名;地址要精确到登录接口或者登录页,不能只写到站点根目录;同一台主机上的不同端口、不同路径要分别列出;清单里不能有重复项,重复项会让同一个目标被跑两遍,浪费时间还会加重对方的风控判定。

下面这段是我常用的预处理脚本,做的事情就是补协议、去重、按主机归组、剔除明显不是登录入口的地址:

import re from urllib.parse import urlparse from collections import OrderedDict RAW = "urls_raw.txt" # 原始清单,一行一个,允许裸域名 OUT = "urls_clean.txt" # 清洗后的清单 def normalize(line: str) -> str: s = line.strip() if not s or s.startswith("#"): return "" # 去掉行内注释和多余空白 s = s.split()[0] # 补协议:没有协议头的按 https 处理 if not re.match(r"^https?://", s, re.I): s = "https://" + s # 去掉末尾多余的斜杠,避免 /admin 和 /admin/ 被当成两条 s = s.rstrip("/") return s seen = OrderedDict() with open(RAW, encoding="utf-8") as f: for line in f: u = normalize(line) if not u: continue p = urlparse(u) if not p.netloc: continue # 以 host+path 为去重键,保留首次出现的完整地址 key = f"{p.scheme}://{p.netloc}{p.path}" if key not in seen: seen[key] = u with open(OUT, "w", encoding="utf-8") as f: for u in seen.values(): f.write(u + "\n") print(f"原始 {sum(1 for _ in open(RAW, encoding='utf-8'))} 行 -> 去重后 {len(seen)} 条")

这段代码的逻辑很直白:normalize负责把一行文本变成一个合法的完整 URL,urlparse拆出主机和路径,用scheme + netloc + path做去重键。参数上要改的只有两个文件名,RAW换成你的原始清单,OUT是输出。之所以用OrderedDict而不是set,是为了保留原始顺序——排查问题的时候,按导入顺序对结果,比按字母序对要快得多。

跑完之后建议再人工扫一眼输出,重点看三类:路径里带?id=这类参数的(大概率不是登录接口)、端口是 8080/8443 但路径写成根目录的(很多管理后台在/manager这种子路径下)、以及同一 IP 不同域名的情况(虚拟主机,可能返回完全不同的页面,不能合并)。

2.3 请求调度与并发控制

请求调度这一段,工具内部做的事情是:从清单里取一个目标,从字典里遍历账号密码组合,对每一组构造一次请求,根据返回结果判定,然后进入下一组。这里面有三个参数直接决定成败——并发线程数、单次请求超时、失败重试次数。

并发线程数不是越大越好。目标如果是自己内网的老系统,撑死能接受 5 到 10 个并发,再高就会出现大量连接超时,超时又被当成“失败”,最后统计出来的失败率虚高,你会误以为字典没覆盖到。目标如果在公网上、前面还挂了一层防护设备,并发一高直接返回 429 或者 403,整批任务全废。我一般的起点是 5 线程,跑一小批(10 个目标左右)观察响应时间,稳定之后再加到 10,很少超过 20。

单次请求超时建议设成 8 到 15 秒之间。设太短,网络稍微抖一下就判失败;设太长,一个挂掉的目标会把整批任务拖住。重试次数我通常设 1 次,只对“连接超时”这一类错误重试,对“返回 4xx/5xx”不重试——返回码本身就是有效信息,重试只会让日志变乱。

这一段还有个隐藏的点:UA 和请求头。有些后台会对非浏览器的 UA 直接返回一个伪装成登录页的错误页,这时候所有判定都会失效。常见做法是手动指定一个常规浏览器的 UA,跑之前先用浏览器访问一次目标,抓一份正常的请求头,照着填进去。这一步不难,但漏掉的人特别多。

2.4 命中结果的结构与落盘方式

结果落盘这一段,看起来只是“把成功的记下来”,实际上决定了你后面能不能复用。WebCrack 输出的结果,我一般会按三个字段去看:目标的完整地址、命中的账号密码(或万能密码标记)、以及判定依据。

判定依据这个字段很多时候工具不直接给,需要你自己补。我的做法是把每次任务的结果目录加一个时间戳,同时在旁边放一份meta.txt,写清楚这一轮用的字典文件名、字典行数、并发数、超时值。这样过两周回头看,你还知道这批结果是在什么条件下跑出来的——否则你只会看到一堆地址,完全想不起来当时字典里有没有包含admin/123456这一条。

还有一个细节:命中结果里的“万能密码”记录,不要和普通弱口令记在同一列。我见过有人把两类结果导进同一张表,然后按“命中数”排序做汇报,结果万能密码那几条被当成“某系统用了弱密码”,处置方向直接跑偏。分表、分类、分颜色,这一点在结果出来之前就要定好。

3. 跑通一轮批量检测:环境、字典、参数

上一章讲的是链路,这一章讲的是把这条链路真正跑起来。三件事按顺序做:环境先通,字典再净,参数最后调。顺序乱了会浪费很多时间,比如环境没通就去调字典,你根本分不清是字典问题还是网络问题。

3.1 运行环境与首次启动

WebCrack 是 Java 那一套的东西,运行前提是本机有可用的 JRE。先确认版本,再启动,别跳过这一步。

# 确认 Java 可用,输出应包含版本号 java -version # 常见启动方式:在解压目录下执行(jar 名以实际文件为准) java -jar WebCrack.jar # 如果内存不够或目标量大,显式给堆内存 java -Xms512m -Xmx2g -jar WebCrack.jar

java -version的输出里要能看到主版本号,看不到就说明 PATH 没配好,先把JAVA_HOME指到 JDK 目录再把bin加进 PATH。启动命令里的-Xms和-Xmx是堆内存的初始值和上限,前者设小一点没关系,后者建议给到 2G——批量任务里每个目标都要缓存一份响应,目标数量一多,默认堆很容易撑不住,表现是程序跑着跑着卡死或者直接退出,日志里还不一定有明显报错。

如果启动之后界面出不来,先看两个地方:一是当前目录下有没有生成日志文件,二是终端里有没有异常堆栈。最常见的两个原因是 JRE 版本太低(老版本的一些 API 在新代码里已经废弃)和路径里有中文或空格。路径这条特别容易被忽略,解压到一个纯英文、无空格的目录下再试,往往就好了。

环境通了之后,先拿一个目标、五行字典跑一次,确认整条链路能走通、结果能落盘。这一步的意义是建立一个基准——后面所有异常,都可以拿这一次的结果做对比。跳过这一步直接上全量,出了问题你没有任何参照物。

3.2 字典的组织与清洗

字典是这类工具的核心资产,质量比数量重要得多。一个五万行的高质量字典,效果远好过一个五十万行的网上随便下的合集。

组织方式上,我一般分三层:基础层是通用弱口令(各种admin、root、test开头,加上123456、admin888、qwerty这类高频组合),行数控制在两百行以内,覆盖大部分“懒得改”的情况;场景层是按系统类型分的,比如 OA 类、监控设备类、数据库管理类,各自有各自的历史默认口令;定制层是根据目标单位的信息推测出来的,比如系统名、年份、常见的缩写组合。三层分开存文件,跑的时候按层递进,先跑基础层,命中了就停,没命中再上场景层。

清洗这一步必须做,而且要用脚本做,手工过一遍五万行是不现实的。要处理的脏数据有几类:行尾的\r(Windows 换行在 Linux 上读会带上这个字符,导致密码末尾多一个不可见字符,永远试不对)、BOM 头(文件开头三个字节,会让第一行的用户名带前缀)、全角字符、首尾空格、以及分隔符不统一(有的用冒号有的用制表符)。

SEP = ":" # 统一分隔符,工具里配置成同一个 SRC = "dict_raw.txt" DST = "dict_clean.txt" out_lines = [] with open(SRC, "r", encoding="utf-8-sig") as f: # utf-8-sig 自动吃掉 BOM for raw in f: line = raw.strip("\r\n").strip() # 去换行和首尾空白 if not line or line.startswith("#"): continue # 全角冒号/制表符统一成半角冒号 line = line.replace(":", ":").replace("\t", SEP) if SEP not in line: continue # 没有分隔符的行直接丢 user, _, pwd = line.partition(SEP) user, pwd = user.strip(), pwd.strip() if not user or not pwd: continue out_lines.append(f"{user}{SEP}{pwd}") # 去重但保序 dedup = list(dict.fromkeys(out_lines)) with open(DST, "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(dedup) + "\n") print(f"{len(out_lines)} 行 -> 去重后 {len(dedup)} 行")

关键点在utf-8-sig这个编码参数——它会自动把文件开头的 BOM 吃掉,比手动判断前三个字节省事。partition而不是split,是为了应对密码里本身带冒号的情况,只按第一个分隔符切,后面的全归密码。输出时指定newline="\n",保证在 Linux 和 Windows 之间来回拷也不会多出\r。

跑完之后做一个自检:随机抽十行,用cat -A看一眼行尾是不是干净的$,如果看到^M$,说明还有残留的\r。

提示:字典清洗完之后,把行数和文件哈希一起记在meta.txt里。同一批目标用不同字典跑出来的结果,只有带上这个信息才能对比。

3.3 并发、超时、重试三个参数的取法

这三个参数是联动的,单独调一个往往没效果。

并发数从 5 起步。判断依据是“单位时间内返回的成功响应数”,不是“任务跑完的速度”。如果 5 线程下大部分目标都能正常返回(不管登录成功还是失败),就说明这个速率是目标能接受的,可以试着加到 10;如果加到 10 之后开始出现大量连接超时或者 429,退回 5,不要再试更高的值。这条线不是固定的,同一个工具打不同的目标,合理的并发数能差十倍。

超时值取 8 到 15 秒。判断依据是“正常响应的 P95 耗时”——先跑十个目标,看日志里响应时间的分布,取一个比大多数响应都长的值,再加 3 秒余量。设成 30 秒那种“反正不会超时”的值,代价是遇到真正挂掉的目标时,整批任务会被拖慢很多,而且你很难从日志里区分“慢”和“死”。

重试次数设 1,且只对连接层面的错误重试。这个设置的意义不是提高成功率,而是过滤掉偶发的网络抖动。如果某个目标在重试之后仍然超时,那它就是真的有问题,应该单独拎出来看,而不是靠加次数硬扛。

还有一个参数经常被漏掉:请求间隔。有些目标对“同一 IP 在短时间内的高频请求”非常敏感,即便并发只有 3,一秒内连发几十次照样会被拦。这种情况下给每次请求之间加一个 100 到 300 毫秒的随机间隔,代价是整体耗时变长,但能让任务真正跑完。

3.4 结果回收与人工二次确认

跑完之后不要直接拿结果去汇报,先做二次确认。工具判定为“命中”的记录里,总有一定比例是误报,尤其是万能密码那部分。

二次确认的做法是:挑出命中记录里的前若干条,用浏览器手工登录一次,看是不是真能进去。这一步不能省,因为有些系统在登录失败时也会返回一个包含“欢迎”字样的页面(比如页头模板里写死了欢迎语),工具按特征串判定就会误判。手工验一遍,至少能把明显的系统性误报排除掉。

确认完之后,把结果按“确定性”分成三档:手工验证通过的是确认命中;工具判定命中但还没验证的是待确认;工具判定失败但响应异常的(比如大量 302 到非登录页)是存疑。三档分开处理,确认命中走整改流程,待确认排进下一轮验证,存疑单独分析。这个分档的动作花不了多少时间,但能避免把误报当成真问题往上捅。

4. 排查笔记:五条踩坑记录

这一章是实打实踩出来的。每一条都按“现象、原因、解决”写,遇到同类问题可以直接对号入座。

4.1 环境与调度层面的四个坑

现象一:清单导进去,任务几秒钟就显示完成了,明显没跑完。

原因几乎每次都是 URL 格式不统一。有的是裸域名没补协议,工具在构造请求时直接失败;有的是地址写到了站点根目录,请求返回的是首页而不是登录页,判定环节拿不到有效特征,直接归到“失败”;还有的是同一个地址末尾带不带斜杠被当成了两条,去重没生效。解决办法就是回到 2.2 那段的预处理脚本,全量过一遍,然后手动抽查输出里前二十条和后二十条,确认协议头、路径、斜杠都规整。这个坑我踩过不止一次,现在的习惯是清单不清洗不进工具。

现象二:某个目标返回 200,页面内容也和登录页一样,但被判定成命中了。

原因在于判定特征选错了。很多后台的登录成功跳转走的是前端路由,URL 变了但服务端返回的 HTML 是一模一样的,工具按“返回体里有没有某个关键词”判定,恰好那个关键词在登录页模板里也有,就误判了。解决办法是换判据:优先看Set-Cookie里有没有新增的会话标识,其次看跳转后的最终 URL,最后才看页面特征串。如果三种都拿不准,就手工验一条,确认之后把这个目标的判定规则单独调整。

现象三:跑到一半开始大面积 403 和 429,剩下的目标全部失败。

这是并发和频率的问题,不是字典的问题。原因是目标前面有频率限制,短时间内的密集请求触发了拦截,而且拦截往往是按源 IP 记的,一旦触发,后面所有目标都会被连带。解决办法是立刻停下来,把并发降到 5 以下,加上 100 到 300 毫秒的随机间隔,然后分批跑——每批十个目标,跑完歇几分钟再跑下一批。如果确认 IP 已经被封,换一个出口或者等封禁窗口过去,硬顶着跑只会让封禁时间越来越长。

现象四:字典里明明有admin/123456,某个后台手工能进,工具却一条没命中。

先查编码,再查空格。常见的原因是字典文件带 BOM,第一行的用户名前面多了三个不可见字节;或者是 Windows 换行在 Linux 上跑,密码末尾带了一个\r;还有一种情况是字典里用的是全角冒号,工具按半角切分,整行被当成一个字段丢掉了。解决办法就是 3.2 那段清洗脚本,用utf-8-sig读、统一分隔符、strip首尾空白。跑之前用cat -A抽样看行尾,是最快的一步自检。

4.2 判定与结果层面的一个坑

现象五:超时记录特别多,占了结果的一半以上,看日志全是连接超时。

原因有两个方向,要分开查。一个是目标本身的问题——服务挂了、端口没开、或者只允许内网访问,这类目标无论怎么调参数都是超时,应该从清单里剔出去单独处理。另一个是本地网络出口的问题——带宽被别的任务占满、DNS 解析慢、或者中间设备对大并发做了限制。区分方法很简单:拿其中一个超时的目标,用curl单独请求一次,看耗时和返回码。curl能通就是并发或调度的问题,curl也不通就是目标本身的问题。解决办法是按这个结论分流,别一股脑加超时时间,那只是把问题往后推。

# 单目标连通性与耗时自检 curl -o /dev/null -s -w "code=%{http_code} time=%{time_total}s\n" \ --max-time 15 "https://example.internal/login"

-o /dev/null丢掉响应体,-s静默模式,-w输出状态码和总耗时,--max-time和工具里的超时设成一致。这条命令的作用是把“网络问题”和“工具问题”切开,是排查超时类问题最快的入口。

5. 把一次检测变成可回归的资产台账

单次跑完的结果,价值有限;能反复对比的结果,才有长期价值。我的做法是把每一轮的结果整理成一张台账,字段固定为:目标地址、命中类型(弱口令/万能密码)、账号、判定依据、验证状态、首次发现时间、最近一次确认时间。这张表用 CSV 存就行,不需要上数据库,关键是字段固定、时间戳完整。

有了台账之后,第二轮检测的意义就变了——不是重新找一遍,而是做回归比对。新增的命中标红,上一轮命中、这一轮不命中的标绿(说明口令改过了),两轮都不命中的是干净资产。这个比对能回答一个很有价值的问题:上次要求整改的那批后台,到底改没改。我用下面这段脚本做比对,输入是两轮的 CSV,输出是三个分组:

import csv def load(path): with open(path, encoding="utf-8") as f: return {r["target"]: r for r in csv.DictReader(f)} old = load("result_round1.csv") new = load("result_round2.csv") fixed = [t for t in old if t not in new] # 已整改 still = [t for t in old if t in new] # 仍未整改 regress = [t for t in new if t not in old] # 新出现 print(f"已整改 {len(fixed)} 条 / 仍未整改 {len(still)} 条 / 新增风险 {len(regress)} 条")

这段代码的核心就是三个集合运算:old有new没有的是已整改,两者都有的是仍未整改,new有old没有的是新增风险。字段名target要和你的 CSV 表头对上。跑出来的三个数字,比一份几百行的原始结果更有说服力——尤其是“仍未整改”那一栏,它把问题从“发现风险”推进到了“跟踪闭环”。

台账还有一层用法:给每个目标加一列“责任人”和“最近一次变更时间”。批量检测的结果一多,很容易变成一份没人认领的清单,加上责任人之后,每一条都能落到具体的人头上,整改才有推动力。这一列是我踩过一次坑之后才加的——第一轮跑了八十多条命中,汇报的时候没人认领,最后不了了之,第二轮跑出来发现一大半还在。

从那以后,我每次跑这类检测,都强制先建好台账模板再动手,跑完直接往里填,绝不等到结果出来再整理。工具跑出来的是一份原始数据,台账才是能拿出去的东西。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进