资讯动态

基于Go重写wafw00f:构建高性能可嵌入的WAF识别工具

发布时间:2026/9/2 6:40:30 来源:尧图企业网站定制
简介go-wafw00f 是一份用 Golang 重新实现的 WAF 指纹识别工具项目以开源工具 WAFW00F 为蓝本面向渗透测试工程师、红队成员和安全开发人员解决 Python 环境配置繁琐、无法快速部署的问题。项目采用 Go 编写主程序但保留并复用了 WAFW00F 数量众多的 Python 规则库通过正则解析将规则转换为 JSON 格式实现不依赖 Python 运行时的情况下完成 WAF 识别可用于快速检测网站是否存在 WAF 并标识具体防护产品。压缩包体积仅 87KB内容包含 Go 源码、Python 规则库与解析逻辑适合直接阅读或二次开发作者还设计了规则集缓存机制每次执行先检测 JSON 文件从而提升运行效率未来规划引入协程并发加速。目前已有 466 人学习浏览。通过学习这份资源读者不仅能获得一个可运行的 WAF 探测工具雏形还能理解 WAF 指纹规则的组织方式、Go 正则解析与缓存落地等关键思路为自研安全工具或参与 WAF 识别研究提供参考。 wafw00f 这个工具在安全评估圈子里应该不陌生它是用 Python 写的开源 WAF 识别工具能通过发送探测请求判断目标站点前面挂了什么 Web 应用防火墙。平时做资产梳理、上线前自检或者红蓝对抗前的情报收集我都会先跑一遍它。但用久了就发现几个痛点Python 环境部署麻烦依赖动不动就冲突单线程跑批量目标效率偏低想把它的检测能力集成到自己的 Go 工具链里还得搞个子进程调来调去。所以我就动了用 Go 重写一遍的念头把 wafw00f 的检测逻辑完整迁移过来做成了一个能当库用、也能当命令行工具用的 go-wafw00f。这篇文章会把我的设计思路、核心模块拆解、匹配引擎的实现细节以及调试过程中踩过的坑完整记录下来。内容偏向工程实现适合有 Go 基础、对安全工具开发感兴趣的读者。如果你只是想找个工具用这里面的并发模型和指纹库组织方式同样有参考价值。1. 为什么用 Go 重写 wafw00f从 Python 到 Golang 的选型思考1.1 wafw00f 到底在做什么先简单梳理一下 wafw00f 的检测原理。它本身不扫描漏洞也不做攻击核心动作就是“识别”对目标 URL 发送一组精心构造的 HTTP 请求一部分是正常流量一部分是明显带攻击特征的载荷比如 SQL 注入语句、XSS payload、异常 HTTP 方法然后对比响应差异。正常请求能通过恶意请求被拦截说明中间有东西在过滤而且拦截响应通常会带上特征。比如 Cloudflare 会在响应头里带cf-ray同时种下__cf_bm这类 CookieModSecurity 拦截后响应状态码往往是 406 或者 403响应体里可能包含ModSecurity字样安全狗则习惯在 Cookie 里写入safedog-flow-item。wafw00f 的指纹库里积累了上百种这类特征匹配到了就报告 WAF 名称。所以它的本质不是漏洞探测而是一种“指纹识别 行为比对”的检测器。这个定位很重要它决定了重写时哪些逻辑是核心资产——指纹库、探测请求模板、匹配算法这三样缺一不可。1.2 Go 重写的收益在哪里选 Go 而不是继续在 Python 生态里打转我是从这几个角度考虑的第一个是部署形态。Go 编译出来就是单个二进制文件扔到服务器上就能跑不需要装 Python 解释器不需要pip install -r requirements.txt。这对内网巡检场景太关键了很多隔离环境根本没有 Python 环境或者版本老得没法看。第二个是并发能力。wafw00f 原版在批量检测几百个目标时速度慢是硬伤。Go 的 goroutine 天然适合这种“每个目标独立检测”的任务模型我只需要控制好并发数和 HTTP 连接池就能在保持礼貌的前提下把吞吐量拉高一个量级。第三个是集成能力这也是标题里“嵌入”这个词的核心含义。Python 工具想嵌入到 Go 项目里通常只能通过子进程调用解析输出文本链路长且不稳定。Go 重写后整个检测逻辑就是一个可以被 import 的库其他工具可以直接调用Detect(url)拿到结果。我自己的资产扫描器就是这么集成的代码干净利落。第四个是标准库够用。net/http配合crypto/tls基本覆盖了 HTTP 客户端的全部需求指纹库用 YAML 或者 JSON 管理完全不需要引入重型框架。Go 1.21 之后标准库里的slices、maps包也让匹配逻辑写起来顺手很多。2. 项目整体设计模块划分与数据流2.1 目录结构与职责边界我最初犯过一个错误把所有逻辑塞进一个 main.go结果代码到一千行就乱得没法维护了。后来参考了 Kubernetes 项目的 layout 惯例按职责拆成了下面这样go-wafw00f/ ├── cmd/ │ └── go-wafw00f/ │ └── main.go ├── internal/ │ ├── runner/ # 批量任务编排、并发控制 │ ├── detector/ # 单目标检测逻辑 │ ├── fingerprint/ # 指纹模型、加载与匹配 │ ├── probe/ # 探测请求模板 │ └── output/ # 结果格式化与输出 ├── pkg/ │ └── go-wafw00f/ # 对外暴露的库 API ├── rules/ │ └── fingerprints.yaml └── go.mod这里有个关键决策把detector放在internal下同时在pkg里暴露一个薄封装。因为 my 工具链是内部项目不希望外部直接依赖内部实现所以内部模块全走internal保护但如果以后想开源给别人用pkg/go-wafw00f这个干净的 API 入口就是现成的。数据流很清晰runner读取目标列表按并发模型分发任务每个任务调detectordetector 依次执行探测请求每次请求结果汇聚后送进fingerprint匹配器命中结果返回给 runner最终由output统一写出。这个单向数据流的好处是每个模块都能独立测试我在调试匹配逻辑时只需要构造一个假响应喂给匹配器完全不用起真实 HTTP 服务。2.2 指纹库从 Python 字典到可配置规则wafw00f 原版的指纹是写在 Python 字典里的每个 WAF 对应一组正则表达式。如果直接翻译成 Go 的 map代码会非常臃肿而且加一条规则要重新编译一次。所以我把指纹库彻底数据化了用 YAML 管理程序启动时加载。每条指纹模型长这样- name: cloudflare headers: - key: server value: cloudflare - key: cf-ray value: cookies: - __cf_bm body: - cf-error-details status: [] confidence: 90字段含义我不多说但有两个设计细节值得提一下。一是header的 value 支持空字符串表示“只要响应头里有这个 key 就算命中”这对cf-ray这种每次请求都会变的 header 很有用二是加了confidence置信度字段不同特征的权重不同匹配结果不再是非黑即白而是按分数排序输出最可能的几个候选这个后面讲匹配引擎时会详细说。规则文件用 YAML 而不是 JSON纯粹是考虑可维护性。YAML 允许注释我可以在每条规则旁边标注“这个指纹是根据厂商文档更新的”之类的备注对后续维护帮助很大。2.3 并发引擎worker pool 的落地方式批量检测任务天然适合 worker pool 模式但我一开始直接用go funcsync.WaitGroup写结果并发一高就被目标站点断开连接。后来改成信号量限制并发数的方式func (r *Runner) Run(ctx context.Context, targets []string) error { sem : make(chan struct{}, r.concurrency) var wg sync.WaitGroup for _, target : range targets { wg.Add(1) sem - struct{}{} go func(u string) { defer wg.Done() defer func() { -sem }() result : r.detector.Detect(ctx, u) r.output.Write(result) }(target) } wg.Wait() return nil }这里的sem就是一个令牌桶只有拿到令牌的 goroutine 才会发起 HTTP 请求。并发数默认 10可以通过-c参数调整。我实测下来这个数字对绝大多数 Web 站点都很安全不会被限流同时批量检测 1000 个目标的速度非常可观。有一点要注意sem - struct{}{}这行如果把控制粒度放在目标列表循环外会导致主流程提前退出空结构体通道的阻塞逻辑要理解清楚。这个坑我调试了快一小时。3. 核心实现探测请求与指纹匹配的细节3.1 HTTP 客户端超时、TLS 与重定向的三件套HTTP 客户端是检测准确性的地基。我踩过几个坑总结下来有三个参数必须亲手调超时设置。http.Client的Timeout字段必须设置不然后果是 goroutine 永久阻塞任务卡死。我设置为 10 秒对绝大多数站点足够。但要注意Timeout是包括读取响应 body 的全部时间如果目标页面很大或者网络很差容易误报超时。更稳妥的方案是用context.WithTimeout在请求级别控制这样每个探测请求有独立的超时互不干扰。TLS 处理。内网站点很多用的是自签名证书所以InsecureSkipVerify必须设为 true否则一大半目标都会在 TLS 握手阶段挂掉。但如果 go-wafw00f 要用来做外网资产识别这个选项反而会降低安全性。我的做法是加一个-k参数默认走标准证书校验内网场景手动开启跳过校验。重定向策略。wafw00f 原版默认跟随重定向但这样有个问题目标站点把请求重定向到登录页响应其实已经变了指纹匹配结果失真。我的方案是自定义CheckRedirect直接返回http.ErrUseLastResponse拿到第一次响应就做判断。因为 WAF 一般在最外层重定向发生之前它已经完成拦截了第一次响应反而最能反映 WAF 特征。transport : http.Transport{ MaxIdleConnsPerHost: r.concurrency, TLSClientConfig: tls.Config{InsecureSkipVerify: r.insecure}, } client : http.Client{ Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Transport: transport, }3.2 探测请求正常流量和攻击载荷怎么组合探测请求的构造是检测能不能生效的关键。我参考 wafw00f 的探测思路把请求分成了几类第一类是正常请求用GET /带上常见浏览器 User-Agent目的一个是拿到基线响应另一个是确认目标确实存活。第二类是攻击特征明显的请求包括 SQL 注入 payload?id1 OR 11、XSS 脚本scriptalert(1)/script、路径穿越../../etc/passwd等。这些 payload 不追求真正打穿什么只需要让 WAF 的检测引擎产生反应。第三类是非正常 HTTP 方法比如TRACE、OPTIONS有些 WAF 对非常见方法有特殊响应。实现上我用一个探测模板切片每种模板定义了请求方法、路径和追加的 Headervar probes []ProbeTemplate{ { Name: normal, Method: http.MethodGet, Path: /, Headers: map[string]string{ User-Agent: Mozilla/5.0 ..., }, }, { Name: sqli, Method: http.MethodGet, Path: /?id1%27%20OR%20%271%27%271, Headers: map[string]string{ User-Agent: Mozilla/5.0 ..., }, }, { Name: xss, Method: http.MethodGet, Path: /?q%3Cscript%3Ealert(1)%3C/script%3E, }, }每个探测请求独立发送、独立记录响应最后把全部响应汇总到匹配器。多探测组合的好处很多最核心的是能降低误报率——前面提到有些正常请求本身就可能触发安全策略但如果多个不同特征的请求都命中同一个指纹那这个判断就非常可靠了。3.3 匹配引擎多维特征打分与去噪指纹匹配是 go-wafw00f 的核心我实现了 header、cookie、body、状态码四维打分机制。匹配不区分大小写header key 统一用textproto规范化避免大小写不一致导致漏报。func (d *Detector) matchFingerprints(resp *http.Response, body []byte) []MatchResult { var results []MatchResult for _, fp : range d.fingerprints { score : 0 matched : 0 for _, hr : range fp.Headers { value : resp.Header.Get(hr.Key) if value { continue } matched if hr.Value || strings.Contains(strings.ToLower(value), strings.ToLower(hr.Value)) { score } } for _, ck : range fp.Cookies { if hasCookie(resp.Cookies(), ck) { matched score } } bodyStr : string(body) for _, pattern : range fp.Body { matched if strings.Contains(strings.ToLower(bodyStr), strings.ToLower(pattern)) { score } } for _, status : range fp.Status { matched if resp.StatusCode status { score } } if matched 0 score 0 { confidence : int(float64(score) / float64(matched) * float64(fp.Confidence)) results append(results, MatchResult{ Name: fp.Name, Confidence: confidence, }) } } sort.Slice(results, func(i, j int) bool { return results[i].Confidence results[j].Confidence }) return results }打分逻辑的核心是比值计算命中的特征数除以应该匹配的特征数再乘以基础置信度。如果某条指纹定义了 3 个特征只有 1 个命中置信度就是 33%基本会被过滤掉如果 2 个以上命中才会进入候选列表。为了防止误报我在外层加了过滤条件只有置信度超过 50 的候选才会最终输出。这个阈值我调过很多次40 太松一堆不相关的结果70 太严真 WAF 特征不明显时会漏报。50 到 60 之间是甜点区你可以在自己环境里实测调整。3.4 命令行与输出JSON 和 CSV 的集成姿势命令行我用的是标准库flag没有引入 cobra。原因很简单这个工具参数不超过五个不需要子命令体系flag 足够。参数包括-u单目标、-f目标文件、-c并发数、-o输出格式table/json/csv、-k跳过 TLS 校验。输出格式我用了一个统一结构体各个格式各自实现 writer 接口type Result struct { URL string json:url Fingerprint []MatchResult json:fingerprints Detected bool json:detected Duration float64 json:duration_ms }JSON 输出对程序集成最友好我自己的扫描器就是解析这个格式拿 WAF 信息。CSV 格式适合直接丢进 Excel 里整理报告。表格格式纯给命令行人看的加了颜色高亮命中 WAF 的标红无 WAF 的标绿。4. 常见问题与排查技巧实录4.1 误报多80% 是特征重叠问题go-wafw00f 跑了一段时间最大的问题是误报。我遇到过一个站点所有请求都返回 403结果 Cloudflare、ModSecurity、安全狗全匹配上了。最后定位发现目标服务器本身配置了 IP 白名单所有请求都被服务器层的访问控制拦截了响应特征碰巧跟某些 WAF 的拦截页相似。这个问题的根源是特征重叠。解决思路有几个一是增加基线对比。如果正常请求和恶意请求返回完全相同的响应说明拦截不是 WAF 做的而是服务器本身策略。所以在匹配前我会做一层预判正常请求的响应和恶意请求的响应如果状态码和 body 高度相似直接降级为“疑似无 WAF”。二是多探测交叉验证。刚才说的多探测模板在这里起作用了如果 SQLi、XSS、路径穿越三个探测全部落在同一个指纹上这个判断的可靠性就很高。单次命中不再报告只有两次以上命中才输出。三是置信度阈值动态化。目标站点如果本身有特殊安全策略比如强制 HTTPS、带 HSTS 头这些特征容易跟 WAF 指纹重叠。我在实现里加了一个“基础响应特征采集”先把站点的常见响应头都拉回来匹配时排除掉这些基线特征降噪效果非常明显。4.2 并发把目标打挂了限速与退避并发数开太高目标站点直接返回 429 或者断开连接这个我在测试阶段经常碰到。一开始以为是代码 bug抓包一看全是connection reset by peer。后来学乖了在并发控制之外又加了一层限速逻辑每个 goroutine 在发起下一次请求前随机 sleep 200 到 500 毫秒避免所有 goroutine 同时打出请求造成瞬时流量尖峰。这里有个原则安全检测工具要尽量不影响目标系统正常运行。尤其是批量扫描场景源 IP 被目标拉黑是小事把客户生产环境打挂了才是大事故。所以我的默认配置相当保守并发 10每请求间隔至少 100ms整体速率不超过 50 QPS。被限流后的处理也很重要。如果检测过程中遇到 429 或 503说明目标在拒绝服务我会把当前目标标记为“rate limited”等 3 秒后重试一次。如果重试还是被限就放弃这个目标在报告里标注原因而不是无限重试耗尽时间。4.3 指纹库维护跟上 WAF 更新的节奏指纹库是 wafw00f 的核心资产也是 go-wafw00f 最容易过期的地方。WAF 产品更新很快厂商会调整响应头、Cookie 名、拦截页面样式旧指纹很快就失效了。我维护指纹库的方式是双轨制一是跟踪原版 wafw00f 的更新。因为我是按数据驱动方式设计的指纹库原版更新规则时我只需要把新增的 Python 字典转成 YAML 格式合并进来。我写了一个小脚本能解析 wafw00f 的wafw00f/plugins/目录下的 Python 文件提取特征表达式生成 YAML工作量从原来的几小时压缩到几分钟。二是自己收集新 WAF 特征。去目标站点手动触发拦截抓响应头、Cookie、页面特征然后补充进指纹库。这个操作要符合授权范围我在做项目评估时碰到一些小的云 WAF 产品原版 wafw00f 识别不了就手动抓特征补充上了。每次补充都会在 YAML 里写注释记录抓取时间和目标环境方便后续回溯。还有一个细节指纹库版本管理也很重要。我在 YAML 文件头部加了version字段程序启动时打印加载的指纹库版本方便排查“结果不对是不是规则太旧”的问题。这个字段配合 CI 流程每次更新都打 tag回滚也方便。5. 写在最后的小技巧项目做到现在我最深的体会是指纹识别工具的瓶颈从来不在语言而在指纹库的质量和更新速度。Go 重写 wafw00f 解决了部署和集成的问题但如果指纹库不维护工具很快会变成摆设。所以如果你也要做类似工具我的建议是把 70% 的精力放在规则建设上代码反而是其次。最后分享一个我实际项目中的小技巧go-wafw00f 除了做命令行工具我还把它封装成了 HTTP 服务提供/detect?urlxxx的 API内网的其他系统可以直接调用。这样整个团队的资产梳理流程里WAF 识别就变成了一个基础设施服务谁需要谁调比每人本地跑一把脚本高效得多。这个扩展方向如果你也需要完全可以在现有代码基础上快速实现。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价