← 返回总目录

Day 161 — 渗透测试框架:流程编排 · 信息收集 → 检测 → 利用 · 自动化扫描框架

网络安全开发 623 行 5 个可运行脚本

Day 161 — 渗透测试框架:流程编排 · 信息收集 → 检测 → 利用 · 自动化扫描框架

Phase 10 — 网络安全开发(Day 146–165) · 主题:渗透测试框架(实战)

使用边界(重要,先读这一节):
- 本课只对你自己拥有、或已获得书面授权的目标做测试。
- 本课不提供任何真实攻击载荷(exploit / webshell / 提权脚本一律没有)。
利用(exploitation) 这一阶段在代码里是空实现 + 占位插件,
它只负责"标记需要人工验证的点位",不会发出任何攻击请求。
- 框架的硬门禁是 Scope:默认只允许 127.0.0.1 + 实验端口段。
未经授权的主机/端口会直接抛 AuthorizationError 并以退出码 2 终止。
- 框架的探测全部是只读的:TCP connect、读取服务主动发来的问候语(banner),
不发送任何自定义数据、不做爆破登录、不提交表单。
- 教材中出现的 IP 一律是文档专用网段(203.0.113.0/24、198.51.100.0/24、
192.0.2.0/24)或回环地址,账号均为占位名。

1. 学习目标

完成本课后,你应该能够:

2. 概念解释

2.1 渗透测试框架到底是什么

一个常见的误解是:"框架 = 点一下按钮就拿下目标"。真实的渗透测试框架解决的是
流程与约束问题,它由四块组成,缺一块就不能叫框架:

组成 解决什么问题 失败的后果(真实案例式的教训)
范围模型(Scope) 我能碰谁、不能碰谁 扫到隔壁客户的网段 → 合规事故,不是"技术失误"
阶段编排(Orchestration) 先做什么后做什么、结果如何流转 还没确认存活就开始扫描 → 大量无效工作和误报
插件/模块(Plugins) 具体检测项可插拔、可替换 检测逻辑散落在脚本里 → 无法复用、无法单独测试
证据与报告(Evidence) 谁在什么时候对什么做了什么 无法复现、无法交付、无法自证清白

一句话概括:

框架 = 范围约束 × 阶段编排 × 可插拔能力 × 可追溯证据

请注意这个式子里没有"攻击载荷"——载荷是可以换的零件,约束与证据才是框架的骨架。
这也解释了为什么本课把代码重心放在 Scope、编排器、审计日志上,
而把"利用"阶段留成空实现。

2.2 为什么需要框架:四个真实痛点

  1. 防遗忘:人手工做渗透测试会漏步骤。框架把"必做步骤"写成代码,
    漏做会体现在报告的阶段清单里(而不是凭记忆说"我做过")。
  2. 防越界:口头约定的范围在紧张的执行过程中一定会被突破
    ("顺手扫一下这个 C 段吧")。把范围写成数据结构,越界直接抛异常。
  3. 可复现:半年后客户问"这个结论怎么来的",框架能给出:
    当时的范围、参数、原始结果(JSON)、审计日志。手工流水账给不出这些。
  4. 可测试:插件可以被单元测试。一个"检测器"如果只在真实目标上跑过,
    它实际上从未被验证过。

2.3 阶段模型:从侦察到报告

工业界常用的流程模型是 PTES(Penetration Testing Execution Standard)七阶段。
本课把概念模型裁剪成能自动化、且不越界的四段:

    ┌────────────┐   ┌────────────┐   ┌────────────┐   ┌───────────┐   ┌──────────┐
    │ 0 授权与   │──▶│ 1 信息收集 │──▶│ 2 检测     │──▶│ 3 利用    │──▶│ 4 报告   │
    │   范围校验 │   │  侦察/扫描 │   │  漏洞判定  │   │ 人工验证  │   │ 证据交付 │
    └────────────┘   └────────────┘   └────────────┘   └───────────┘   └──────────┘
       代码执行          全自动只读       全自动只读       ★ 本课为空实现 ★    全自动
       强制门禁          允许            允许(只读)     (只留人工入口)     允许

阶段之间是数据流关系,不是"可选菜单":

为什么要严格分段? 因为分段让失败可以被定位。如果扫描和判定混在一个脚本里,
"结果不对"时你无法区分是"资产判断错了"还是"检测逻辑错了"——
这正是 Day 160 日志分析里"解析/检测/报告"三段分离的同一个道理。

2.4 授权与范围:为什么必须是数据结构

口头授权在真实项目里的样子:

"客户的 Web 站点你可以测,但是不要动数据库服务器,
不要做拒绝服务,只在下班后 20:00–23:00 做。"

这句话里有四类约束:目标范围、排除项、禁止手法、时间窗口。
四类都必须落到代码里:

python
@dataclass
class Scope:
    networks: list[str]          # 允许的目标(CIDR)
    ports: list[tuple[int, int]] # 允许的端口区间
    excluded: list[str]          # 显式排除(黑名单优先于白名单!)
    allow_raw_sockets: bool = False   # 本课永远 False
    allow_write_actions: bool = False # 本课永远 False
    ticket: str = ""             # 授权单号(证据链的一部分)
    expires: str = ""            # 授权截止时间

关键设计:黑名单优先。 即使某个主机同时在白名单里,
只要命中 excluded 就拒绝。为什么?因为排除项通常对应"业务不能停"的机器,
一次越界的代价远大于少扫一台。

更关键的一点:范围校验发生在"动作之前",而不是"动作之后"。
很多脚本的做法是"先连接,看连上了再判断是否在范围内"——
这已经把包发出去了。正确顺序是:

解析目标 → 生成 Target 列表 → ★ scope.check(target) ★ → 才有资格创建 socket
                                    ↑
                        越界在这里抛异常,socket 从未被创建

2.5 插件化:注册表模式的收益与代价

框架不写死检测项,检测项通过注册表挂进来:

python
@framework.plugin("http_banner")
def http_banner(ctx):
    ...

为什么值得?三个收益:

  1. 框架代码稳定:加检测项不需要改编排器,降低回归风险。
  2. 可单独测试:每个插件可以用假 ctx 跑单元测试,不需要真实网络。
  3. 可裁剪合规:客户不让做某类检测时,不注册即可,而不是"注册了但记得别调"。

代价也要说清(这是很多教程省略的部分):

所以本课的注册表是有序注册表(记录注册顺序 + 显式 priority),
并且 ctx 是只读 dataclass(插件只能读,写回结果要 return Finding),
这样隐式耦合被掐死在设计层面。

2.6 Metasploit 的定位:理解概念,而不是抄命令

ROADMAP 里提到"Metasploit 基础"。这里只做概念层面的说明,
原因是:Msfconsole 的价值在于"模块生态 + 会话管理",而它的使用教程
不是本课能承担的责任,也不该由教材承担风险。你需要理解的概念:

概念 含义 对应本课的哪些代码
Module 一个可执行单元(auxiliary/exploit/post) 本课的 Plugin
Auxiliary 只做扫描/嗅探/枚举,不做利用 本课的阶段 1/2
Exploit 触发漏洞并建立会话 本课空实现(阶段 3)
Payload 利用成功后执行的内容 本课完全不涉及
Encoder 变形载荷以规避检测 本课完全不涉及(规避检测属于攻击能力)
Workspace 按项目隔离数据 本课的 --scope 文件 + 输出目录
Session 已建立的交互通道 本课没有(无利用能力)

一句话:框架的架构思想可以学,攻击载荷不学也不会影响你把它用在授权测试里——
因为授权测试里真正耗时的 90% 是范围管理、资产盘点、结果复核、报告交付,
而不是"敲 exploit"。

3. 原理深入

3.1 TCP connect 扫描的三态语义

本课用 socket.connect_ex(),它内部就是完整的 TCP 三次握手:

客户端                     目标
   │──── SYN ──────────────▶│   connect() 发起
   │◀─── SYN + ACK ─────────│   端口有人在监听 → 返回 0        → OPEN
   │──── ACK ──────────────▶│   连接建立(随后我们立即关闭,不发送任何数据)

   │──── SYN ──────────────▶│
   │◀─── RST ───────────────│   没有监听者 → ECONNREFUSED     → CLOSED

   │──── SYN ──────────────▶│
   │        (无回应)       │   被防火墙静默丢弃 → timeout     → FILTERED

三态的工程含义完全不一样:

状态 errno 含义 报告里应该怎么写
open 0 有服务在监听 "端口开放,需进一步确认服务与版本"
closed ECONNREFUSED(111) 主机活着但没有服务 "主机存活,该端口无服务"
filtered timeout / EHOSTUNREACH 测不出,有设备在拦 "不可判定:被过滤或链路问题"
error 其他(DNS、权限、网络不可达) 扫描本身失败 "该目标本轮无有效结论"

最常见的错误:把 filtered 统计成 closed。 后果是报告写着
"该网段仅 3 个端口开放",而实际上有 50 个端口因为防火墙静默丢包
而处于"不可判定"状态——这是结论级的错误,不是精度问题。

3.2 为什么不用 SYN 半开扫描(raw socket)

教科书里的"半开扫描"需要 raw socket 自己构造 TCP 包,本课刻意不做,三个原因:

  1. 需要 root:裸 socket 要 CAP_NET_RAW,而"必须用 root 才能跑"的框架会
    诱导使用者全程 root,扩大攻击面。
  2. 越界风险更高:自己构造包意味着可以构造任意包,范围约束更难保证。
  3. 合规与观感:半开扫描的行为特征与攻击工具高度相似,
    在授权测试里没有必要为了省一次握手去冒这个风险。

connect 扫描的代价是:每次扫描都会在目标留下完整连接记录(会被记日志)。
这不是缺陷,是特性——授权测试应该留下痕迹,且框架应主动告知客户
"你会在日志里看到这些连接"。

3.3 并发模型:线程池 + 令牌桶

扫描是典型的 I/O 密集任务:每个端口 99% 的时间在等超时。
两种可选方案:

方案 优点 缺点 本课选择
ThreadPoolExecutor 代码直观、socket 阻塞语义天然适配 线程数上限、GIL(这里无影响,纯 I/O) ✅ 采用
asyncio 万级并发更省资源 需要异步客户端、超时语义更绕 ❌ 教学成本高

但"并发"必须配一个"限速",否则:

所以框架里有一个令牌桶(token bucket):每秒最多放行 rate 个连接,
并发数 workers 限制同时在途的连接数。两个参数管的是不同的事:

workers  → 同时在途的连接数(背压:防止本地端口耗尽、防止把目标打满)
rate     → 每秒新增连接数(礼貌:控制对目标/中间设备的冲击)

3.4 编排器:为什么是"阶段对象"而不是"一个大函数"

python
@dataclass
class Phase:
    name: str
    fn: Callable[[Framework, list[Asset]], PhaseResult]
    optional: bool = False

每个阶段返回 PhaseResult(ok / skipped / failed / reason / findings)。
编排器做三件事:

  1. 依赖检查:阶段 2 需要阶段 1 的产物,若阶段 1 失败(例如全部 timeout),
    阶段 2 应该 skipped 而不是"在空列表上算出 0 个问题"。
    —— 后者会让报告显示"未发现问题",属于静默失败,是最危险的失败模式。
  2. 统一审计:每个阶段的开始/结束/耗时/产出数量都写进审计日志。
  3. 失败传播:critical 阶段失败 → 立即终止;optional 阶段失败 → 记警告继续。

3.5 审计日志:框架的第一等产物

审计日志的每一行回答四个问题:谁、何时、对什么、做了什么。

jsonl
{"ts": "2026-09-20T09:10:01+08:00", "event": "scope_check", "subject": "127.0.0.1:8000", "result": "in_scope", "detail": {"ticket": "LAB-SELF-001"}}
{"ts": "2026-09-20T09:10:02+08:00", "event": "probe", "subject": "127.0.0.1:8000", "result": "open", "detail": {"elapsed_ms": 0.6}}
{"ts": "2026-09-20T09:10:03+08:00", "event": "phase_end", "subject": "scan", "result": "ok", "detail": {"assets": 1}}

为什么它比报告更重要?

结论:没有审计日志的"渗透测试"无法交付。

4. 核心机制详解:编排状态机与限速实现

4.1 阶段结果的状态机

阶段不能只有"成功/失败"两态,因为"跳过"和"失败"必须被区分:

       ┌──────────────────────────────────────────────────────────┐
       │                    编排器 run(phases)                    │
       └──────────────────────────────────────────────────────────┘
                                  │
              ┌───────────────────┼───────────────────┐
              ▼                   ▼                   ▼
        ┌──────────┐        ┌──────────┐        ┌──────────┐
        │  ok      │        │ skipped  │        │ failed   │
        │ 有产出   │        │ 依赖不满足│        │ 抛异常   │
        └────┬─────┘        └────┬─────┘        └────┬─────┘
             │                   │                   │
             │                   │                   ├─ optional? → warn + 继续
             │                   │                   └─ critical? → 终止 + 退出码 4
             │                   │
             └───────────┬───────┘
                         ▼
              下一阶段:把 skipped 的产物视为"不可用"
              (绝不允许在空输入上计算"0 个问题")

用代码表达(pentest_core.py 的实际实现):

python
@dataclass
class PhaseResult:
    name: str
    status: str          # ok / skipped / failed
    reason: str = ""
    findings: list = field(default_factory=list)

    @property
    def usable(self) -> bool:
        return self.status == "ok"

编排器里的关键判断(这是整份框架最容易被写错的一行):

python
for phase in phases:
    if not deps_satisfied(phase, done):
        result = PhaseResult(phase.name, "skipped", "上游阶段未产出可用资产")
    else:
        try:
            result = phase.fn(self, assets)
        except Exception as exc:
            result = PhaseResult(phase.name, "failed", f"{type(exc).__name__}: {exc}")
    ...

为什么"Skipped 必须存在"? 因为报告里的一句话差别巨大:

前者在客户眼里等于"没事",后者意味着"这次测试无结论,需要排期重测"。
把无结论伪装成结论,是自动化测试最严重的缺陷。

4.2 令牌桶限速:算法与实现

令牌桶的规则很简单:

桶容量 capacity(例如 = workers)
每 1/rate 秒补充 1 个令牌(不超过容量)
每次要发一个连接请求 → 先取 1 个令牌;取不到就等待
capacity = 5, rate = 2/s

  tokens
    5 ┤●●●●●
      │        ┌─ 匀速补令牌 ─┐
    3 ┤●●●     │  +1 每 0.5s  │
    1 ┤●       └──────────────┘
    0 ┤     ●───●───●───●        ← 取用(每次放行一个连接)
      └────┬───┬───┬───┬───┬───▶ t(s)
           0   0.5  1.0 1.5 2.0

与"并发上限"的区别再说一遍,因为这两个参数经常被混淆:

参数 语义 调大后的后果 调小后的后果
workers 同时在途的连接数 本地端口/TIME_WAIT 堆积,目标连接表压力 扫得慢,超时误判增多
rate 每秒新增连接数 IDS 报警、被客户安全团队叫停 速度慢但"礼貌"

实测经验值(回环/局域网教学环境):workers=8, rate=50/s 足够让
127.0.0.1 的 200 个端口在 1 秒左右扫完;真实授权环境建议从
rate=5/s 起步,根据客户许可往上调——这不是"能多快"的问题,
而是"客户允许多快"的问题
。

4.3 超时的选择:为什么是 0.5 秒而不是 0.1 秒

超时值决定 filtered 的判定,而 filtered 是不可判定态,越少越好:

超时 回环 同局域网 广域网(跨区域)
0.1s 足够 大量假 filtered 几乎全 filtered
0.5s 足够 基本可靠 部分假 filtered
1–3s 浪费(本地毫秒级就返回) 可靠 可靠

本课默认 0.5s:在实验环境(回环)下,正常端口的 connect 是微秒级返回的,
超时参数只影响"被丢弃的包"要等多久。报告里必须回显实际超时值——
否则"开放 3 个端口"这个数字无法被复核(用 0.1s 扫出来的 filtered
和用 3s 扫出来的完全是两回事)。

4.4 幂等与去重

同一个 (host, port) 可能被多个阶段/插件重复探测,框架层做两件事:

  1. 去重队列:Target 是 frozen dataclass,放进 set 自动去重;
  2. 结果缓存:同一轮运行内 (host, port) 只探测一次,
    结果存在 Framework.cache,第二次命中直接复用(并记录 cache_hit)。

为什么重要?因为重复探测会污染审计日志:日志里出现 3 次同一端口,
复核者无法判断是"框架重试"还是"三个人在扫"。缓存让日志与事实一一对应。

4.5 失败重试:只重试"网络抖动类"错误

不是所有失败都值得重试:

错误类型 重试? 理由
ECONNREFUSED ❌ 不重试 这是有效结论(端口关闭),重试只是浪费
timeout ✅ 重试 1 次 可能是抖动;重试后仍超时才是 filtered
EHOSTUNREACH ✅ 重试 1 次 可能是路由收敛中
AuthorizationError ❌ 绝不重试 越界是策略拒绝,重试等于绕过门禁
socket.gaierror ❌ 不重试 本课不接受域名目标(避免 DNS 外发)

最后一条是安全设计原则:越界错误绝不能进入重试路径,
否则"重试逻辑"会变成"绕过范围校验的旁路"。

5. API 速查

5.1 pentest_core 模块速查

名称 类型 作用 关键点
Scope dataclass 授权范围 黑名单优先;allow_raw_sockets/allow_write_actions 恒为 False
Scope.check(target) 方法 唯一的越界出口 不通过抛 AuthorizationError
Scope.load(path) 类方法 从 JSON 载入 缺 networks/ports 直接 ScopeError
parse_host_spec(spec) 函数 IP/CIDR → 主机列表 拒绝域名(避免 DNS 外发)
parse_port_spec(spec) 函数 "80,8000-8010" → 端口列表 有序去重,越界端口报错
Target(host, port) frozen dataclass 探测单元 frozen ⇒ 可进 set 去重
AuditLog.record(event, subject, result, **detail) 方法 写过程证据 可选 JSONL 落盘
tcp_probe(target, timeout, retries) 函数 只读探测 只读 banner,不发送任何字节
TokenBucket(rate, capacity).acquire() 类 限速 返回本次等待秒数
Framework.build_targets(hosts, ports) 方法 表达式 → 去重 Target 列表 只做解析,不校验
Framework.authorize(targets) 方法 批量校验 fail-fast:任一越界即中止
Framework.probe_many(targets) 方法 线程池 + 限速探测 结果顺序与输入一致
Framework.run_phases(phases, assets) 方法 阶段编排 空资产 ⇒ 下游 skipped
Finding dataclass 发现项 priority = f(severity, confidence)
build_report(...) / render_markdown(...) 函数 报告 必含范围/参数/覆盖/审计统计
LabServer({port: greeting}) 类 回环实验服务 仅用于自测

5.2 三态 + 退出码语义对照(最容易记混的两组)

概念 取值 判据 报告措辞
端口状态 open connect_ex == 0 有服务在监听
closed errno == 111 主机存活、该端口无服务
filtered timeout / errno == 11 不可判定(被过滤或链路问题)
error 其他 OSError / gaierror 本轮无有效结论
退出码 2 越界 / 参数非法 不可执行,修范围重跑
4 存在 filtered / error 无结论,需换路径复测
3 有 P0/P1 发现 进人工复核队列
0 干净完成 归档

优先级:2 > 4 > 3 > 0。

5.3 优先级公式

severity   ∈ {info=0, low=1, medium=2, high=3, critical=4}
confidence ∈ {info=0, low=1, medium=2, high=3}
priority_score = (severity + 1) × (confidence + 1)
  ≥ 20 → P0      ≥ 12 → P1      ≥ 6 → P2      其余 → P3

例:version_disclosure = low + high = 2×4 = 8 → P2;
cleartext_protocol = high + medium = 4×3 = 12 → P1;
banner_evidence = info + high = 1×4 = 4 → P3(事实记录,不丢弃)。

5.4 参数速查(报告必须回显这几个)

参数 默认 含义 调大的后果 调小的后果
timeout 0.5 s 单次 connect 等待 扫描变慢 假 filtered 增多
workers 8 同时在途连接数 本地端口耗尽、目标压力大 慢
rate 50 /s 每秒新增连接数 触发 IDS/客户投诉 慢但礼貌
retries 1 抖动类错误重试次数 超时类错误被掩盖 抖动被记为不可判定

6. 实战代码案例:一次完整的框架运行

下面的输出全部来自 04-pentest-framework.py 的实测输出(本机 Linux + Python 3.12,
实验服务:8000 伪装 SSH、8080 伪装 HTTP;授权范围回环 + 8000-8099;扫描端口 8000-8080)。

6.1 正常运行(有开放端口、无可判定缺口)

$ python3 code/04-pentest-framework.py lab --ports 8000-8080 --rate 500
[lab]   启动本机实验服务: [8000, 8080](仅回环)
[scope] networks=['127.0.0.1/32'] ports=[(8000, 8099)] ticket=LAB-SELF-001
[collect] 目标 81 个 → open 2 / closed 79 / filtered 0 / error 0  (throttle_wait=0.981s)
[detect]  插件 4 个 → findings 9
[exploit] skipped:本课不提供利用能力;输出人工验证清单,无网络行为
[report]  已写入 days/day-161-渗透测试框架/out/day161-lab-report.json / ...md
[exit]    3

findings 9 的构成(实测):7 条来自 4 个检测插件,2 条来自 exploit 占位阶段
的 needs_manual_verification(每台开放端口的主机 1 条):

优先级 插件 目标 severity/confidence
P1 cleartext_protocol 127.0.0.1:8080 high / medium
P2 version_disclosure 127.0.0.1:8000 low / high
P2 version_disclosure 127.0.0.1:8080 low / high
P3 banner_evidence 127.0.0.1:8000 info / high
P3 banner_evidence 127.0.0.1:8080 info / high
P3 high_port_service 127.0.0.1:8000 info / medium
P3 high_port_service 127.0.0.1:8080 info / medium
P3 needs_manual_verification 8000 / 8080 info / info

报告节选:

markdown
## 覆盖与结论可信度

- 目标数 81:open 2 / closed 79 / filtered 0 / error 0
- **可判定 81 条;不可判定 0 条**(filtered = 被过滤或链路问题,不能当作关闭)

## 开放端口与服务

| 目标 | 服务 | banner(截断) |
| --- | --- | --- |
| 127.0.0.1:8000 | ssh | SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.4 |
| 127.0.0.1:8080 | http | HTTP/1.1 200 OK Server: lab-nginx/1.18.0 |

6.2 越界必须立即终止(这是重点)

$ python3 code/04-pentest-framework.py scan --hosts 10.0.0.5 --ports 8000
⚠️ 未指定 --scope,使用默认实验范围(仅回环 8000-8099)。
[scope] networks=['127.0.0.1/32'] ports=[(8000, 8099)] ticket=LAB-SELF-001
⛔ AuthorizationError: 目标不在授权网段内: 10.0.0.5
   (已记入审计日志:scope_check denied + run_abort exit=2)
[exit] 2

注意 denied 这一条也被写进了审计日志——"我拒绝了一个请求"同样是证据。

6.3 结果不完整(存在 filtered)时退出码是 4

用不可路由的文档网段(192.0.2.0/24,RFC 5737)造出"不可判定"场景:
让范围文件允许它,但那里没有服务会应答。

$ cat /tmp/scope192.json
{"networks": ["192.0.2.0/24"], "ports": [[8000, 8002]], "ticket": "LAB-DEMO"}

$ python3 code/04-pentest-framework.py scan --hosts 192.0.2.1 --ports 8000-8002 \
      --scope /tmp/scope192.json --timeout 0.2 --tag demo-inconclusive
[scope] networks=['192.0.2.0/24'] ports=[(8000, 8002)] ticket=LAB-DEMO
[collect] 目标 3 个 → open 0 / closed 0 / filtered 3 / error 0  (throttle_wait=0.000s)
[detect] skipped:上游阶段无可用资产(全部 filtered/error)
[exploit] skipped:上游阶段无可用资产(全部 filtered/error)
[exit]    4

这里最值得看的是 skipped:不是"发现 0 个问题",
而是"检测阶段未执行"。同一个空 findings 列表,两种措辞,两种完全不同的含义。

(这也是开发期真实踩到的坑:早期 tcp_probe 忘了 sock.settimeout(timeout),
回环测试完全正常,一遇到不可达目标就卡住几十秒——详见坑 13。)

7. 常见陷阱(13 个,对照表;详细复现见 code/03-pitfalls.py)

# 陷阱 症状 正确做法
1 把 filtered 当 closed 报告谎称"仅 N 个端口开放" 三态分开统计,回显不可判定数量
2 先连接再判断范围 越界包已经发出去了 校验在 socket() 之前
3 把 ECONNREFUSED 当"主机不存在" 丢失"主机存活"情报 closed ⇒ 主机存活 + 该端口无服务
4 超时拍脑袋设 0.05s 假 filtered 遍地 按网络距离选值并回显
5 并发拉满无限速 事实上的拒绝服务边缘 令牌桶 + 由客户许可决定 rate
6 重试策略写反 抖动被误判 / 越界被重试 只重试抖动类;越界绝不重试
7 范围里写域名 解析即外发 只接受 IP/CIDR
8 空输入算出 0 个问题 "无结论"伪装成"没问题" 上游无资产 ⇒ 下游 skipped
9 重复探测不缓存 审计日志污染 Target 去重 + 结果缓存
10 直接写 banner 进报告 目标注入控制字符/Markdown 净化 + 截断 + 标注不可信
11 用 time.time() 计时 校时/改时钟导致负数 计时用 perf_counter
12 报告不回显范围与参数 结论不可复核 范围/参数/覆盖/审计统计必有
13 收了 timeout 却没 settimeout 回环正常、遇到不可达目标就卡死 sock.settimeout(timeout);用不可达目标验证耗时≈timeout

8. 局限与伦理边界(必须写进交付报告,不能省略)

能力上的局限

刻意不做(设计选择,不是遗漏)

不做的事 为什么不做
提供 exploit / 载荷 教材不承担武器化风险;授权测试的瓶颈在流程与证据,不在载荷
提供规避检测(encoder/混淆) 规避能力只对"未授权攻击"有意义;授权测试应留下痕迹
自动处置(封 IP、改配置) 误报会直接升级为生产事故;处置必须走独立的授权与变更流程
raw socket / SYN 扫描 需 root、扩大攻击面、行为特征与攻击工具难以区分
自动扩大范围("顺手扫一下邻居") 越界不是技术失误,是合规事故

9. 思考题

  1. 本框架把 filtered 单列并让退出码变成 4。如果客户只想要一个"扫描结果",
    为什么"本次无结论"这个结论反而更重要?请从责任边界角度回答。
  2. 范围校验选择 fail-fast(一个越界就整轮终止)而不是"跳过越界目标继续"。
    在什么情况下你会反过来选择"跳过并记警告"?这两种选择分别让谁承担风险?
  3. 插件注册表让"加检测项"变容易了,但也让"执行顺序"变得隐式依赖 priority。
    如果两个插件分别需要"先有 banner"和"先有服务推断",你如何用数据结构
    而不是文档约定来保证顺序?(提示:声明式依赖 vs 数字优先级)
  4. 审计日志会记录"我对哪个 IP 的哪个端口做了什么"。这份日志本身也是敏感数据:
    如果它泄露,攻击者能获得什么?你会在留存期限和脱敏粒度上怎么设计?
  5. 本课刻意不提供利用能力。假设某天你所在的团队要加上"验证性利用",
    你会要求哪些前置条件(授权、隔离环境、回滚方案、审批留痕)才允许合并?
    列出至少 4 条,并说明每条对应哪一类风险。
  6. rate 参数决定了扫描压力。假如客户说"我们的 WAF 会把超过 20 连接/秒
    的来源封禁 1 小时",你如何设计一个既能完成扫描、又不会被封的策略?
    (提示:不要试图规避 WAF —— 应该去和客户约定白名单或维护窗口。)