ffuf 目录枚举是 Web 资产梳理中最常用的一步:它用字典并发请求路径,按响应码、长度、词数过滤噪声,快速暴露未在导航中出现的后台、备份、调试接口。对防守方而言,同一套命令的价值在于「用攻击者视角检查自己的暴露面」。本文给出 ffuf 的完整实战命令,并把扫描结果转成可执行的加固清单,而不是停在「扫出一堆 200」。
适用场景
- 新接手站点的资产盘点:拿到域名后需要快速摸清有哪些隐藏路径,尤其是历史遗留的后台、测试目录、上传点。
- 上线前的暴露面自查:开发环境误开到生产、备份文件未清理、
.git目录可读,这三类问题用目录枚举最快发现。 - 多站点同 IP 的虚拟主机梳理:一台服务器挂几十个站点时,靠 DNS 反查不全,vhost 枚举能找出未在 DNS 中登记的站点。
- 护网或渗透测试的授权探测:在书面授权范围内做路径发现,为后续验证提供入口清单。
必须强调:目录枚举属于主动探测,只应在自有资产或取得书面授权的目标上执行,并且要限制并发速率,避免把目标打成拒绝服务。
前置条件
- ffuf 1.5 以上版本,Go 编译的单文件二进制,无需运行时依赖。
- 一份合适的字典。推荐 SecLists 的
Discovery/Web-Content与Discovery/DNS子集,不要一上来就用几十万行的大字典。 - 目标站点的基线响应特征:先请求一个必然不存在的随机路径,记录其状态码与响应长度,这是后续过滤的依据。
- 授权文件与速率约定。生产环境建议并发不超过 20、每秒请求不超过 50。
原理说明
ffuf 的工作模型很简单:把字典中的每一行替换到 URL 的 FUZZ 占位符位置,发起并发 HTTP 请求,然后按响应特征过滤并输出。它的性能优势来自 Go 的协程池,单机可以轻松跑到每秒数百请求,因此限速是使用者的责任而不是工具的限制。
真正的技术含量在过滤。现代站点普遍做了软 404:任何不存在的路径都返回 200 加一个「页面不存在」模板页,状态码完全失去区分度。此时必须靠响应长度(-fs)、词数(-fw)、行数(-fl)来剔除模板页。判断方法是先用 -fr 正则匹配模板页特征文本,或者直接对比几个随机路径的长度是否完全一致。
虚拟主机枚举是另一条线。当目标 IP 上挂了多个站点时,目录枚举只能看到默认站点。vhost 模式把字典值塞进 Host 头,靠响应差异判断该 Host 是否存在:存在的站点会返回自己的内容,不存在的会落到默认站点。这一步常用响应长度差异做判别。
操作步骤
第一步:安装与基线探测
# 安装(Linux amd64)
cd /tmp
wget -q https://github.com/ffuf/ffuf/releases/latest/download/ffuf_2.1.0_linux_amd64.tar.gz
tar xzf ffuf_2.1.0_linux_amd64.tar.gz
install -m 0755 ffuf /usr/local/bin/ffuf
ffuf -V
# 拉取字典
git clone --depth 1 https://github.com/danielmiessler/SecLists.git /opt/SecLists
# 基线:请求三个随机路径,观察状态码与长度是否一致
for i in 1 2 3; do
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" \
"https://target.example.com/$(openssl rand -hex 6)"
done
如果三条输出的长度完全一致,说明站点存在软 404,后续必须用 -fs 过滤这个长度值。
第二步:基础目录枚举
ffuf -u https://target.example.com/FUZZ \
-w /opt/SecLists/Discovery/Web-Content/raft-medium-directories.txt \
-mc 200,204,301,302,307,401,403 \
-fs 4821 \
-t 20 \
-rate 50 \
-timeout 10 \
-o /tmp/ffuf_dir.json -of json
参数含义:-mc 只保留有意义的状态码,其中 401 与 403 尤其重要,它们说明路径存在但被拒绝,往往是后台入口;-fs 4821 过滤掉软 404 模板页的长度;-t 20 并发线程数;-rate 50 全局每秒请求上限,这是保护目标的关键参数。
第三步:加过滤器与递归
实际站点噪声更多,需要组合多个过滤条件,并对发现的目录做递归:
ffuf -u https://target.example.com/FUZZ \
-w /opt/SecLists/Discovery/Web-Content/raft-medium-files.txt \
-mc all -fc 404 \
-fs 4821,0 \
-fw 312 \
-fr "Page Not Found|访问的页面不存在" \
-recursion -recursion-depth 2 \
-recursion-strategy default \
-e .php,.bak,.zip,.tar.gz,.sql,.env \
-t 20 -rate 50 \
-o /tmp/ffuf_rec.json -of json
要点说明:-e 扩展名爆破能直接命中 index.php.bak、db.sql 这类高危残留,比纯目录扫描的收益率高得多;-recursion-depth 2 限制递归层数,避免字典膨胀导致请求量失控;-fr 用正则剔除模板页文本,比单纯靠长度更稳。
第四步:虚拟主机枚举
先确认目标 IP 上是否有未登记站点。这一步不加 -fs 而是先用 -fs 0 观察原始分布,找出默认站点的响应长度再过滤:
# 找出默认站点响应长度
curl -s -o /dev/null -w "%{size_download}\n" -H "Host: not-exist-random.example.com" https://TARGET_IP/
# vhost 枚举
ffuf -u https://TARGET_IP/ \
-H "Host: FUZZ.example.com" \
-w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt \
-mc 200,301,302,403 \
-fs 612 \
-t 10 -rate 30 \
-o /tmp/ffuf_vhost.json -of json
-k 参数在证书不匹配时需要加上,但会同时关闭证书校验,仅建议在测试环境使用。生产目标建议用 --servername 或直接解析到源站 IP 后再扫,避免 TLS 握手失败产生大量误判。
第五步:结果整理与去噪
把 JSON 结果转成清单,按状态码分组,方便逐条核实:
python3 - <<'EOF'
import json, collections
d = json.load(open('/tmp/ffuf_rec.json'))
buckets = collections.defaultdict(list)
for r in d.get('results', []):
buckets[r['status']].append((r['length'], r['url']))
for code in sorted(buckets):
print(f"=== {code} ({len(buckets[code])}) ===")
for ln, url in sorted(buckets[code])[:40]:
print(f"{ln:>7} {url}")
EOF
输出中优先看 401/403(存在但受限)与长度异常的 200(可能是真实页面而非模板)。301 跳转要跟一次,确认跳转目标是登录页还是外部域名,后者可能是开放重定向。
配置验证
扫描结果必须逐条人工核实,工具输出只是线索。建议按下面的顺序验证:
- 确认真实存在:对每个 200 结果用
curl -I复核状态码与Content-Type,排除 WAF 返回的拦截页。 - 确认是否应公开:
.git/config、.env、*.sql、backup.zip这类路径一旦可访问即为高危,无需进一步判断。 - 确认速率未造成影响:扫描期间观察目标 5xx 比例与响应时间,若明显上升说明速率过高,需下调
-rate。 - 确认结果可复现:换一个出口 IP 复扫关键路径,排除 CDN 或 WAF 的地域策略干扰。
# 批量复核 200 结果
jq -r '.results[] | select(.status==200) | .url' /tmp/ffuf_rec.json \
| while read -r u; do
printf "%s " "$u"
curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download}\n" "$u"
done
常见问题
Q1:扫出来几百个 200,全是同一个长度,怎么处理?
这是典型的软 404。先用随机路径确认模板页长度,然后用 -fs 精确过滤该长度值。如果模板页长度会随 URL 变化(比如页面上回显了请求路径),单靠长度无效,此时改用 -fr 匹配模板页里的固定文案,或加 -fw/-fl 组合过滤。极端情况下可以用 -mr 做正匹配,只保留包含特定关键词的响应。
Q2:扫描被 WAF 拦截,大量 403 或 429,是否继续?
先停。403 与 429 大量出现说明已触发限频,继续加并发只会让出口 IP 被封更久,而且拦截页会污染结果。正确做法是降到 -rate 10、-t 5,加 -p 1-3 做请求间隔随机化,并换用干净的出口。若目标是自有资产,应从源站 IP 直连并临时把扫描 IP 加入白名单,扫完立即移除。
Q3:vhost 枚举结果和 DNS 查询结果对不上,以哪个为准?
以 vhost 枚举为准来发现「物理上存在」的站点,以 DNS 为准来判断「公网可解析」的站点。两者差集就是风险点:vhost 能访问但 DNS 无记录,说明该站点只靠 Host 头区分,一旦攻击者猜到域名就能直连,而它往往缺少统一的安全策略。这类站点应优先接入 WAF 或直接下线。
总结
ffuf 目录枚举的产出质量取决于三件事:基线是否准确、过滤器是否贴合目标、速率是否克制。-mc 决定看什么,-fs/-fr 决定不看什么,-rate 决定会不会把事做坏。把这三项定下来,几百行字典就能给出高信噪比的清单。
更关键的是后半程:扫出来的路径要逐条归类为「应公开」「应鉴权」「应删除」,并把「应删除」的部分固化成上线检查项,例如构建流程里禁止打包 .env 与备份文件、Nginx 层显式 deny \.(git|svn|bak|sql)$。只扫不修,下一次扫描结果不会变。