news 2026/10/2 2:48:55

基于pytest的自动化渗透测试:子任务断言与Google式记分法实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于pytest的自动化渗透测试:子任务断言与Google式记分法实践

做防守方这些年,我最怕的不是攻击手法有多新,而是“懂行的人太少”。红队报告交上来,密密麻麻几十页,核心结论往往就一句话:“我们发现了高危漏洞”。拿去问开发:怎么复现?怎么证明修好了?报告上没有。拿去问老板:这次渗透测试做完,我们的安全水位到底涨了没?你也答不出来。这不是某个团队的问题,是整个渗透测试行业的常态——测试结论严重依赖执行者个人经验,过程不可复现,结果不能自动断言。

直到我在 arXiv 上刷到一篇有意思的预印本,讲的是把渗透测试拆成“pytest 能断言的子任务”,再用类似 Google 记分法的加权模型给整个攻击面打分。这篇东西的落点其实很妙:表面上看是给攻击方做自动化武器,论文里却明确把防守方当作最重要的用户——你完全可以借用这套框架,把红队的思路搬到自己的 CI 里,定期跑一遍“自我渗透”。这篇文章就围绕这个思路展开,聊聊子任务怎么拆、断言写在哪、“Google 式记分法”到底在记什么分,以及我们自己落地时会踩哪些坑。

1. 先说清楚这篇论文想解决的问题:渗透测试的结果为什么不能“自动复验”

1.1 传统渗透测试报告的信任危机

我见过太多“一次性渗透测试”的尴尬局面。测试团队进场,拿着 Kali 跑一圈扫描器,再人工测几个核心接口,最后丢出一份 PDF。报告里写“存在 SQL 注入风险”,但开发复现的时候发现当时的注入 payload 已经失效了;写“某接口存在越权”,但没人记录当时使用的 cookie 和请求顺序。三个月后再看,这份报告基本就是废纸。

更麻烦的是结论之间的横向比较。这轮测试发现 5 个高危、3 个中危,下一轮说发现 2 个高危、6 个中危,那安全水位到底是变好了还是变差了?CVSS 分数看着客观,但打分的人不同,同一个漏洞能差出三四分。这种不可复现、不可量化的问题,本质上是整个流程缺少了“自动化测试框架”里的那层断言——你没有把“漏洞存在”这个结论,变成机器可执行的、可反复验证的判定条件。

1.2 子任务状态机:把攻击路径变成可测试的离散步骤

这篇 arXiv 预印本的核心思路,是把一次渗透测试看作一条攻击路径上的有限状态机。状态机里的每一个节点,都是一个可以独立判定达没达成的子任务。

举个例子,完整攻击链可能是:探测开放端口 → 发现 Spring Boot Actuator 端点 → 通过 /actuator/env 拿到明文密钥 → 用密钥登录内部管理后台 → 在后台找到命令执行点。传统渗透测试里,这是一段“流畅的叙述”;但在论文的框架里,它被拆成五个离散子任务,每一个都有明确的前置条件(precondition)和后置条件(postcondition)。

前置条件决定了这个子任务“能不能开始测”,比如“只有确认 8080 端口开放,才去测 Actuator”。后置条件就是你要断言的结论,比如“HTTP 请求 /actuator/env 的响应码不是 404,且响应体里包含关键词”。只有当后置条件为真,状态机才会推进到下一个阶段。任何一个子任务后置条件不满足,攻击路径就断在这里。

这套做法的好处很直接:渗透测试不再是“一个人讲完一个故事”,而是“一条流水线上一堆可以独立运行、独立复验的测试用例”。

1.3 为什么偏偏选 pytest,而不是自研框架或 Burp 插件

论文里把 pytest 作为执行引擎,我一开始也觉得有点“大炮打蚊子”,仔细想想其实很合理。

第一,pytest 的断言机制就是为这种“验证某个条件是否成立”的场景设计的。assert 一个状态码、assert 一个响应体、assert 一个时间差,这些都是原生能力。用 fixture 管理会话、cookie、认证 token,也非常顺手。

第二,生态不需要你重新发明轮子。pytest 有插件体系,有 conftest.py 做共享配置,有 parametrize 做参数化用例,有 marker 做用例分级。Allure 报告可以直接把测试结果变成图表,这对安全团队汇报来说是天然的加分项。

第三,CI/CD 集成几乎零成本。你不需要维护一个独立的渗透测试平台,只需要在 Jenkins 或者 GitLab CI 里加一个 stage,跑一遍 pytest,然后等 exit code。这套逻辑和普通代码测试完全一致,安全团队和开发团队说的是同一种语言。

2. 子任务拆解的实操逻辑:断言到底应该写在哪

2.1 拆解层次:从侦察到持久化的五种状态迁移

真正动手把渗透测试拆成子任务的时候,最大的难点不是“怎么断言”,而是“按什么粒度拆”。拆得太粗,断言不够稳定;拆得太细,用例数量爆炸,维护成本比收益还高。

论文给了一个可操作的框架:按攻击阶段拆,每个阶段内部再按“状态迁移”定义子任务。我按自己的实践经验,一般会分成五层:

  • 侦察层(Reconnaissance):确认目标资产存在、端口开放、服务指纹识别。断言的通常是“某个端口 TCP 连接成功”“某路径返回非 404”“响应头里包含某中间件特征”。
  • 扫描层(Scanning):在侦察结果基础上做漏洞探测。断言的一般是“某请求响应中匹配到漏洞特征”“某接口返回未经授权的数据”。
  • 利用层(Exploitation):验证漏洞是否真的可被利用。断言往往会升级,比如“成功读取到 /etc/passwd 的前几行”“命令执行返回了 whoami 的输出”。
  • 横向移动层(Lateral Movement):验证从当前节点是否能触达内部其他资源。断言通常是“某个内网 IP 的端口可达”“某类凭据能被重复使用”。
  • 持久化层(Persistence):验证攻击痕迹是否能长期留存。断言可能是“计划任务文件已写入并保存”“服务在重启后仍然存活”。

每一层的输出,恰好是下一层的输入。这就很像流水线里工序之间的交接:上一道工序的后置条件,就是下一道工序的前置条件。

2.2 断言的本质:不是判断“有没有漏洞”,而是判断“状态变没变”

这是论文里最值得细品的一个观点,也是我踩过坑之后才真正理解的:断言的本质不是判断“目标有没有漏洞”,而是判断“目标系统在测试前后的状态有没有发生允许范围内的变化”。

很多新手写安全自动化测试时,习惯把断言写成“目标必须有漏洞”,比如 assert "sql injection" 出现在响应里。这有两个问题:一是扫描器误报率高,你断言一个特征,结果特征在其他场景本身就该存在;二是你把自己和漏洞扫描器绑死了,没能体现出渗透测试的“证据链”属性。

更好的做法是断言状态变化。比如验证一个越权漏洞,不是断言“接口返回了 user_id=1 的信息”,而是断言“使用低权限账号 A 的 session 请求高权限接口时,HTTP 状态码从 401/403 变成了 200,且响应体里出现了本应只有高权限账号 B 可见的数据”。前者是特征匹配,后者是状态迁移验证,后者的误判率会低一个数量级。

2.3 示例:一个真实的子任务断言设计

我这里写一个实际可跑的示例,主题是检测 Spring Boot Actuator 配置不当暴露。这个问题在真实渗透测试中常见,而且非常适合做成断言子任务。

# conftest.py import pytest import requests @pytest.fixture(scope="session") def target_host(): """测试目标地址,建议通过环境变量注入,比如 TARGET_HOST=http://10.0.0.5""" import os return os.environ.get("TARGET_HOST", "http://127.0.0.1:8080") @pytest.fixture(scope="session") def http_session(): session = requests.Session() # 设置超时和通用头,避免被目标 WAF 直接拦截 session.headers.update({"User-Agent": "Mozilla/5.0 (SecurityAssertion/1.0)"}) return session
# test_subtask_actuator_exposure.py import pytest import requests class TestActuatorExposure: """子任务:未授权访问 Actuator 端点""" # 参数化要探测的端点 @pytest.mark.parametrize("endpoint", ["/actuator/env", "/actuator/health", "/actuator/beans"]) def test_actuator_endpoint_not_authorized( self, target_host, http_session, endpoint ): # 前置条件:目标服务在监听 probe = http_session.get(target_host, timeout=5, allow_redirects=False) assert probe.status_code not in [502, 503], "目标服务不可达,跳过子任务" # 核心断言:未认证请求不应返回 200 resp = http_session.get( f"{target_host}{endpoint}", timeout=5, allow_redirects=False, ) assert ( resp.status_code == 200 ), f"未授权访问 {endpoint} 未成功,响应码为 {resp.status_code}" # 关键断言:响应里确实泄露了敏感信息 if endpoint == "/actuator/env": assert resp.json(), "响应体为空,无法证明存在信息泄露" keys = resp.json().keys() assert ( len(keys) > 0 ), "env 端点响应内容为空,疑似只是框架默认页"

这段代码里有两个值得提的细节。第一,前置条件先请求根路径排障,否则目标都宕机了,后面的断言失败没有意义。第二,断言分两级,先断言“未授权拿到了 200”,再断言“响应体里确实有数据”,两级都通过,这个子任务才算达成。这就是把“渗透测试人员的判断”翻译成“机器可执行的断言”的过程,也是这套框架能落地的原因。

3. Google 式记分法的内核:防守方终于有了统一度量衡

3.1 四个评分维度:严重度、可达性、业务影响、持久性

论文里把“Google 式记分法”作为一个卖点,我认为重点不是山寨某家公司的内部评分,而是吸收了 Google 安全评审里那种“多维加权”的思路。它不像 CVSS 那样试图构造一个绝对指标,而是针对自动化子任务输出一个可比较、可行动的分数。

拆开来看,我给子任务评分时一般会给四个维度:

维度权重说明示例
Severity(严重度)40%该子任务达成后对机密性/完整性/可用性的损害读取到云厂商密钥=10;探测到服务器版本=2
Reachability(可达性)30%达成该子任务所需的前置条件是否容易被触达无需认证=3;需要拿到内网主机权限=1
Business Impact(业务影响)20%该子任务涉及的资产是否是核心资产涉及支付交易链路=5;涉及后台报表=2
Persistence(持久性)10%达成后攻击结果是否容易被清除写入 cron 持久化=4;临时拉起一个端口=1

每个维度打分后,按权重加权求和,得到一个 0 到 10 的分数。这个分数不应该用来“报告里有面子”,而是直接映射到防守动作:8 分以上的子任务,今天必须出修复方案,并且要在修复后把对应子任务重新跑一遍断言;4 到 7 分的,本周内进迭代;3 分以下的,进观察清单。

3.2 关键路径权重:像搜索引擎那样看待“必经节点”

我理解的“Google 式记分法”还有第二层含义,就是关键路径权重。一个子任务即使本身的严重度不高,但如果它是很多条攻击路径的必经节点,那它的实际风险就比孤立看高很多。

打个比方,就像搜索引擎里某个网页本身内容一般,但大量高质量页面都链接到它,那它的排序就会上升。攻击图里也一样:一个不起眼的“测试接口未关闭”如果同时是三条横向移动链路的起点,那它的价值就要被放大。

论文里提到可以借鉴 PageRank 的思路对攻击图做迭代加权计算:子任务 A 被多少个子任务指向、A 又指向多少个子任务,这些关系会迭代收敛出一个稳定的权重。落到工程实现上,你不需要真的去算 PageRank,我建议的做法是给每个子任务标注“依赖下游子任务 ID 列表”,然后做一个简单的递归权重累加。你很快会看到哪些节点是“枢纽节点”,这些枢纽节点通常就是防守方最应该优先处理的地方。

3.3 分数怎么用:从“被攻陷”到“距离业务风险还有几步”

记分法对防守方的真正价值,不是给你一个“安全得分 87 分”的仪表盘,而是让你能回答那个我开头提到的问题:我们的安全水位涨了没。

假设你设置了 30 个断言子任务。第一轮跑完,有 12 个子任务达成,攻击链最深走到“拿到数据库只读权限”,业务风险分是 6.2。你修复了其中 8 个子任务,第二轮跑完,只有 4 个子任务达成,攻击链最深只到“读取到服务器版本号”,业务风险分降到了 3.1。这个对比就很直观:不是“漏洞数少了几个”,而是“攻击链最深走到哪一步”发生了肉眼可见的变化。

这套方法还有一层额外的好处——分数的变化能暴露测试本身的退化。如果某次升级后,原本失败的子任务意外通过了,但你并没有做过相关修复,那很可能不是变安全了,而是应用版本变化导致攻击路径变了。这种信号比任何报告都有价值。

4. 防守方落地:把红队的武器搬进自己的 CI

4.1 环境准备:pytest 基础与目录设计

想把这套框架落地,建议先从最小闭环开始,不要上来就铺几百个用例。我的建议是:单独建一个 security-tests 仓库,不要和业务测试混在一起,否则权限模型和运行频率都很难独立控制。

建议的目录结构:

security-tests/ ├── conftest.py ├── requirements.txt ├── tests/ │ ├── recon/ # 侦察层子任务 │ ├── scan/ # 扫描层子任务 │ ├── exploit/ # 利用层子任务 │ ├── lateral/ # 横移层子任务 │ └── persistence/ # 持久化层子任务 ├── scoring/ │ ├── calculator.py # 记分逻辑 │ └── attack_graph.py # 关键路径权重 └── reports/ └── allure-results/

requirements.txt 里主要装 pytest、requests、allure-pytest。如果你们有更复杂的目标协议,可以再加 paramiko(SSH)、sqlalchemy(数据库校验)、selenium(Web 端链路)等,但第一版别加太多,跑通为先。

运行方式非常简单:

TARGET_HOST=https://staging.example.com pytest -v --alluredir=reports/allure-results

建议先在 staging 或灰度环境跑,等用例库稳定了再考虑对生产环境做只读类型的探测。写进 CI 时,用一个专门的定时器或者开发合并后的触发器,而不是每次提交都全量跑,避免引入过长的流水线耗时。

4.2 第一个可断言的渗透测试用例

除了前面那个 Actuator 示例,我再给一个更贴近业务层的例子:验证“登录接口是否存在账号枚举”。这个用例很经典、执行成本低、结论也容易向非安全同事解释。

# tests/scan/test_login_enumeration.py import pytest import requests @pytest.mark.parametrize("username", ["admin", "not_exist_user_9527"]) def test_login_response_difference(target_host, http_session, username): """子任务:判断登录失败响应是否存在可枚举差异""" resp = http_session.post( f"{target_host}/api/login", json={"username": username, "password": "WrongPwd_2025"}, timeout=5, ) # 记录响应特征:状态码、消息体、响应时长 result = { "username": username, "status_code": resp.status_code, "body": resp.text, "elapsed_ms": resp.elapsed.microseconds / 1000, } print(result) # 本用例只输出数据,是否构成枚举漏洞由断言子任务 decide 层判断

这段代码其实只算“数据采集用例”,真正的断言我会放在下一个用例里,把不同用户名的响应结果拉齐做差:

def test_enumeration_signal( target_host, http_session, fixture_known_usernames ): # 用一个绝对不存在的用户名做基线 baseline_resp = http_session.post( f"{target_host}/api/login", json={"username": "definitely_not_exists_0001", "password": "WrongPwd_2025"}, timeout=5, ) baseline_text = baseline_resp.text baseline_code = baseline_resp.status_code # 对目标用户名发起同样的请求 target_resp = http_session.post( f"{target_host}/api/login", json={"username": "admin", "password": "WrongPwd_2025"}, timeout=5, ) assert ( target_resp.status_code != baseline_code or "user not found" in target_resp.text and "user not found" not in baseline_text ), "未发现账号枚举信号,或者响应完全一致"

写这种断言时要注意一个细节:不要把两个请求放在同一个用例里就完事,要记录基线请求的完整上下文(UA、时间戳、甚至源 IP),因为很多系统会有全局限流或 IP 封禁策略,基线请求被封了,结论就失真了。

4.3 把结果接进 Allure 与安全看板

用例跑完只是第一步,真正让团队愿意用起来的是报告。Allure 是 pytest 生态里很顺手的报告方案,跑完 pytest 后用 allure generate 就能出一份带用例执行历史、失败趋势和步骤截图(通过钩子抓取)的报告。

allure generate reports/allure-results -o reports/allure-report allure open reports/allure-report

我通常会额外写一个很简单的 python 脚本,读 pytest 的 JUnit XML 输出,把我们前面定义的子任务 ID 和分数映射到安全看板。这样每轮测试一结束,看板上的攻击链深度和业务风险分就会自动刷新。这件事本身不复杂,但需要让 scoring 模块和测试用例一一对应。我的经验是:用例名直接用“子任务 ID_描述”格式命名,比如 test_subtask_0402_actuator_env_exposure,这样脚本解析时省掉大量映射 work。

5. 自动化渗透测试的边界,以及论文没讲清楚的部分

5.1 断言脆弱性:一次误报足以让整个机制停摆

任何跑过自动化安全测试的团队,都会遇到断言脆弱性的问题。你断言“响应体里包含 password”,结果目标页面有一位员工在博客里写“password is hard”。你断言“未授权返回 200”,结果目标的网关默认对未匹配路由返回 200 空页面。误报一旦出现,团队第一反应就是“这工具不准”,然后所有人不再看报告。

我的处理办法是:断言宁可写得保守,也不写激进,最重要是确认“我们到底在证明什么”。一个粗粒度但稳定的断言,远好过“看起来很精确但三天两头误报”的断言。论文把断言作为核心卖点,但真实世界里断言本身就是最大的不稳定因素,需要在落地时大量依靠 baseline 对比和灰度观察来校正。

5.2 动态环境里的对抗因素

自动化断言最怕的就是目标系统“会变”。测试环境里的动态令牌、每次启动随机生成的密码、前端加密逻辑、WAF 的 JS 挑战,都会让原本正常的子任务瞬间全部失败。论文里没有详细讨论这层对抗因素,但实际工程里非常折磨人。

我的应对建议有三个。第一,所有用例前置步骤尽量通过 fixture 统一封装,比如获取动态令牌、完成 JS 挑战、初始化会话,都放在 conftest.py 里,不要散落在各个用例中。第二,在 CI 里设置复杂的重试策略:网络抖动导致的失败重试一次,认证过期导致的失败重新登录后再跑一次。第三,给测试环境做“快照基线”,保证每次测试之前环境状态可控,避免上一次测试留下的脏数据影响本轮断言。

5.3 分数被游戏化的风险

只要引入分数,就一定会有人“刷分”。开发团队可能为了让分数好看,直接给某个子任务相关的功能下线;防守团队可能为了 KPI,只挑容易的断言用例来扩大分母。论文里把记分法包装得很美好,但它没法解决组织里的激励问题。

我的经验是,不要让单个分数成为团队 KPI,而是让“分数变化趋势”和“攻击链最深到达点”成为讨论对象的原始数据。避免把安全分数和绩效完全挂钩,否则分数很快就会变成政治工具。你要的是团队把精力放在修复子任务上,而不是修饰数字。

5.4 授权边界与合规问题

如果把整套框架接进 CI,等于让自动化工具持续扫描你们自己的资产。表面上看是自家业务没问题,但实际有几个容易忽略的边界。

第一,资产归属要清楚。如果你们使用了第三方 CDN、云厂商托管服务,自动化探测可能会覆盖到不属于你管辖的组件,合规上要留个心眼。第二,免责声明和授权文件是必须的,至少要在项目 README 里明确说明“这套自动化扫描仅允许在已获得授权的环境中运行”。第三,payload 里的某些字符串要控制,不要引入真实的攻击载荷,否则流量镜像被审计时会产生不必要的麻烦。我一般只用探测性质的数据,比如一个唯一的随机字符串、一次无害的 HEAD 请求,以判定状态迁移为目的就够用了。

这轮实践做下来,我个人最大的体会是:安全测试断言的真正敌人不是“未知漏洞”,而是“不可复现的过程”。把渗透测试拆成 pytest 可断言的子任务,再加一套统一记分法,核心价值不是让攻击方省事,而是让防守方终于可以每天、每周用同样的尺子去量自己有没有变好。代码层面、评分模型层面都有很多糙活儿要处理,但方向是对的。如果团队里有一两个愿意折腾 pytest 的同事,这门手艺值得尽快铺开。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:48:39

局域网文件共享神器:MeFile大文件传输与密码保护实战指南

直接先坦白:我在日常办公和组网维护里,最怕听到的一句话就是"传个文件呗"。一百多GB的素材包、几十个G的项目备份、一整台虚拟机镜像,微信传不动,QQ传超时,网盘限速到怀疑人生,U盘又要来回跑。后…

作者头像 李华
网站建设 2026/10/2 2:48:27

Mac上Pycharm集成Git完整指南:从安装配置到分支管理

1. 项目概述与核心价值1.1 为什么我建议你在Mac上把Git和Pycharm一起用先说结论:Git是每个写代码的人绕不过去的基础工具,而Pycharm是目前Mac上最顺手的Python IDE之一,两者配合起来,等于给你的代码上了一道“时间保险”。这个组合…

作者头像 李华
网站建设 2026/10/2 2:47:53

YOLOv5小麦麦穗检测全流程:从标注训练到Flask网页部署

简介:本资源是一套面向计算机视觉初学者与农业AI应用开发者的毕业设计级项目,聚焦小麦麦穗的自动化检测需求,解决农业生产中作物生长状态评估、产量预估与智能监测等实际问题。压缩包共37个文件,含14个Python源码(覆盖…

作者头像 李华
网站建设 2026/10/2 2:47:38

Windows图片自动上传百度网盘:官方同步空间与Python脚本方案

每天打开Windows电脑,总会看到一堆新产生的图片:手机相册导出的照片、截图工具抓的图、相机SD卡里的原片。这些图如果只躺在本地硬盘上,硬盘一坏,几年的记录全没了。我早就把备份这件事定成了刚需:自动上传到百度网盘。…

作者头像 李华
网站建设 2026/10/2 2:46:20

416×416脑部MRI肿瘤二分割数据集:从数据校验到可视化跑通

简介:本资源面向医学图像分割方向的初学者与算法实践者,提供一套416416分辨率的人脑MRI肿瘤二分类分割数据集及配套可视化脚本,可用于训练与验证U-Net等分割网络,帮助解决医学影像中前景标注获取困难的问题。压缩包共2000个文件&a…

作者头像 李华
网站建设 2026/10/2 2:46:17

Spring Boot集成MyBatis实战:从核心原理到排查方法

其实Spring和MyBatis这套组合,几乎是国内Java后端开发的“国民级配置”。凡是做业务系统、后台管理的朋友,大概率都跟它打过交道。为什么这样说?因为Spring负责把对象之间的依赖关系管起来,MyBatis负责把你跟数据库打交道这件事做…

作者头像 李华