ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于 Python Flask 的 Web 漏洞扫描系统:信息搜集与漏洞检测实战

基于 Python Flask 的 Web 漏洞扫描系统:信息搜集与漏洞检测实战 简介项目基于Python Flask框架开发是一套Web漏洞扫描系统毕业设计源码面向计算机、通信、人工智能、自动化等专业学生与开发者适用于课程设计、大作业或毕业设计支撑项目。系统涵盖信息搜集与漏洞扫描两大核心模块代码经过调试可直接运行答辩评审分98分具备一定完整性和参考价值。压缩包共156个文件大小2.88MB以43个Python文件构成后端核心逻辑19个CSS、18个JS与14个HTML实现前端管理界面另有7个Markdown说明、Dockerfile与YAML部署配置、SQL数据库脚本以及DOCX使用文档方便对照代码与文档快速理解项目结构。资源内提供完整源码包、可视化页面和多类型辅助文件从登录认证到扫描任务执行均有对应代码模块配合使用文档可快速上手也便于毕业设计答辩演示。目前已有34人学习下载适合需要系统掌握Flask Web开发、安全扫描流程的读者可在现有基础上修改扩展自由补充扫描规则或界面功能。1. 基于 Python Flask 的 Web 漏洞扫描系统毕设里最难的不是 Flask是这两条链路很多人选“基于 Python Flask 的 Web 漏洞扫描系统”当毕设是因为觉得“Web 漏洞扫描”听起来比“图书管理系统”有分量。但实际动手你会发现Flask 只是最不费劲的部分路由、表单、数据库读写一周能写完。真正让这个题目值钱、也让大部分人卡到崩溃的是信息搜集怎么做得全、漏洞扫描怎么测得准——这两条链路才是系统的核心。这篇笔记按“源码怎么拆、怎么跑、怎么改”的视角把模块拆分、信息搜集、漏洞扫描、踩坑记录和验证技巧一次讲透。适合正在做毕设的学生也适合想用 Flask 搭一个最小可用扫描器的后端开发。2. 系统拆分与模块选型把信息搜集和漏洞扫描分开代码才写得下去大多数人的第一个错误是把所有逻辑塞进 Flask 路由里用户在前端点“开始扫描”后端函数里既做 DNS 解析又发 HTTP 请求又写数据库。这种做法在演示时能跑但答辩时被问“某一个环节挂了怎么办”“想加一种新漏洞怎么加”就答不上。常见的做法是拆成三个独立模块Web 层负责接收任务和展示结果信息搜集模块和漏洞扫描模块各自独立运行模块之间只通过数据库表和任务状态通信。2.1 模块划分信息搜集、漏洞扫描、任务管理三层先看一下整个系统的数据流再决定代码放哪里。用户提交一个域名或 URL系统先进入信息搜集阶段枚举子域名、探测端口、识别 Web 指纹。这些都是网络请求操作互相之间没有依赖适合拆成独立的函数组。信息搜集结束后系统拿到一串“看起来像 Web 服务”的 URL把这些 URL 交给漏洞扫描模块。扫描模块再逐个请求投递 payload比对响应。我在做这个题目时采用的目录结构如下这个结构也是毕设答辩时最容易讲清楚的结构。每个模块一个文件不要把一个模块拆到十几个文件里。vulnscanner/ ├── app.py # Flask 入口注册蓝图、初始化数据库 ├── config.py # 路径、超时、并发、payload 文件路径 ├── requirements.txt ├── modules/ │ ├── __init__.py │ ├── models.py # SQLAlchemy ORM 模型 │ ├── recon.py # 信息搜集子域名、端口、指纹 │ ├── scanner.py # 漏洞扫描SQLi 和 XSS 检测器 │ └── utils.py # 公共请求会话、日志、超时工具 ├── templates/ # Jinja2 页面 ├── static/ # 前端静态资源 └── scans/ # 扫描报告输出目录逻辑说明app.py 里不写任何检测逻辑只做三件事读取 config、注册蓝图、调 create_all 建表。recon.py 和 scanner.py 互不 import两者都通过 models.py 里的任务表判断自己该做什么。这样的好处是你单独跑python modules/recon.py example.com也能用不需要启动 Flask。我在调试信息搜集模块时就是这么干的比每次都从浏览器点按钮快得多。参数说明scans/ 目录建议放在项目根目录而不是 static 下避免扫描报告被当成静态文件直接对外暴露。毕业设计答辩现场经常有人现场演示报告路径暴露会让你的系统看起来很不专业。config.py 里所有超时和并发参数都集中管理后文讲到的 timeout、线程数、payload 路径都在这里改不要散落在各个函数里写死数字。2.2 Flask 只当壳真正干活的是 requests、socket 和 BeautifulSoupFlask 在系统里的角色是任务分发和结果展示。一个常见的误解是“用 Flask 写扫描器”实际上扫描器部分跟 Flask 没有任何关系把 Flask 换成 Django 或者换成命令行脚本扫描逻辑一行都不用改。我在实现时用 Flask 的 Blueprint 把 API 和页面分成两组页面走/API 走/api/scan这样前后端可以分开改。# app.py 中的蓝图注册片段 from flask import Flask from modules.api import api_bp from modules.views import view_bp def create_app(): app Flask(__name__) app.config.from_pyfile(config.py) app.register_blueprint(view_bp) # 页面路由 app.register_blueprint(api_bp, url_prefix/api) # 接口路由 return app逻辑说明scan 任务的提交、查询、删除全部走 /api 前缀页面模板只通过 fetch 调接口不直接操作任务表。这是 Flask Web 开发里比较常见的做法后期加报告导出功能时只需要在 api_bp 里加一个路由不会碰到页面代码。视图层和接口层分开之后你甚至可以先用 Postman 把接口全部调通再做页面调错时更容易定位是前端问题还是后端问题。技术选型上信息搜集部分我用的是 requests 加 socket页面解析用 BeautifulSoup这三个库是 python 环境里最基础的依赖不需要额外装扫描框架。为什么不直接调用现成的扫描框架因为毕设的核心评价点在“你理解不了解漏洞检测原理”用框架等于把最有分量的部分外包了。答辩时老师问一句“这个漏洞是怎么发现的”如果你只会说“框架自动扫的”这一题就已经扣分了。自己实现检测逻辑哪怕实现得简单也至少能讲清楚请求和响应的比对过程。2.3 数据库设计与任务状态机给每个扫描任务一个状态扫描系统必须要回答三个问题当前任务跑到哪一步了、发现了什么、结果存在哪。所以数据库至少要有四张表targets 存目标信息scan_tasks 存任务状态subdomains 存子域名枚举结果vulns 存漏洞结果。下面是 models.py 的核心部分。from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class ScanTask(db.Model): __tablename__ scan_tasks id db.Column(db.Integer, primary_keyTrue) target db.Column(db.String(255), nullableFalse) # 用户提交的域名或 URL status db.Column(db.String(16), defaultpending) # pending/running/finished/failed created_at db.Column(db.DateTime, defaultdatetime.now) finished_at db.Column(db.DateTime, nullableTrue) class VulnResult(db.Model): __tablename__ vulns id db.Column(db.Integer, primary_keyTrue) task_id db.Column(db.Integer, db.ForeignKey(scan_tasks.id)) url db.Column(db.String(500), nullableFalse) # 漏洞所在 URL vuln_type db.Column(db.String(32)) # sqli / xss param db.Column(db.String(64)) # 漏洞参数名 payload db.Column(db.Text) # 触发漏洞的 payload detail db.Column(db.Text) # 响应特征描述说明status 字段是任务调度的核心。前端轮询 /api/scan/status 时只查这个字段后端线程在处理时把 pending 改成 running全部测完改成 finished异常则改成 failed。状态机是毕设答辩的加分项比把“扫描中”写死在页面上要专业得多。参数说明url 字段必须存完整 URL不要只存域名因为漏洞报告里每个条目都要能单独点击跳转。如果只存域名报告页就要靠拼接去猜原始路径很容易拼错。初始化数据库的步骤放在 app 启动时执行。第一次跑之前先装依赖再建库python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install -r requirements.txt python -c from app import create_app; appcreate_app(); app.app_context().push(); from modules.models import db; db.create_all()逻辑说明这里用 app_context 推入上下文是因为建表操作依赖 Flask 的配置直接 import db 然后 create_all 会因为缺少 app 上下文报错。这一步经常有人漏掉后面的避坑章节会再补一个它的连带坑。参数说明venv 是 python 项目的基本隔离手段不要用系统 python 直接装依赖否则后期装新库时很容易把环境弄乱。这个教训在 python 新手里极其常见装了一堆全局包之后项目和系统库互相踩版本最后只能重装 python 环境。3. 信息搜集模块子域名枚举、端口探测和指纹识别的可跑代码信息搜集的目的是生成一份“目标资产清单”。对毕设来说不需要做到企业级资产测绘但至少要做到三件事知道目标的子域名知道哪些端口开着知道 Web 服务用的什么框架。这三件事分别对应三个独立函数全部放在 modules/recon.py 里扫描模块在后面直接引用它们的结果。三个函数的输入都统一为目标域名输出分别是列表或字典方便后续模块读取。3.1 子域名枚举字典加 DNS 解析二十多行就能跑子域名枚举的常见做法是用一个字典跑穷举对每个候选子域名做 DNS 解析解析出 IP 就认为存在。这种方法在高校毕设里足够用了。不要一上来就接搜索引擎接口和证书透明日志先把正向枚举做通再作为扩展点提一句即可。import socket from concurrent.futures import ThreadPoolExecutor def enum_subdomains(domain, dict_path./dict/subnames.txt, concurrency20): with open(dict_path, encodingutf-8) as f: words [line.strip() for line in f if line.strip()] found set() def check(word): sub f{word}.{domain} try: ip socket.gethostbyname(sub) found.add((sub, ip)) except socket.gaierror: pass # 解析失败表示子域不存在忽略 with ThreadPoolExecutor(max_workersconcurrency) as pool: pool.map(check, words) return sorted(found)逻辑说明socket.gethostbyname 是阻塞调用单个解析通常几毫秒但遇到不存在的域名要等系统 DNS 超时所以必须用线程池并发跑。pool.map 会等待所有任务完成所以函数返回时 found 已经收集完整。参数说明concurrency 设在 20 左右比较合适太高容易让本机 DNS 查询队列溢出丢结果太低则几千词的字典要跑好几分钟dict_path 指向一个每行一个单词的纯文本字典字典里可以包含 www、api、admin、test 这类常见前缀前缀文件可以从网上找现成的也可以自己按毕设目标积累。注意gethostbyname 返回的是第一个 IP对有多 IP 的域名拿不到完整 A 记录。想做得细一点可以用 dnspython 库解析 A 记录但毕设场景用内置库已经够讲清楚“解析成功即存在”的判断逻辑。另一个小细节有些靶场环境 DNS 解析会超时很长如果跑完一批字典发现结果异常少先检查系统 DNS 是不是被改过用nslookup手动解析一个确定存在的子域名验证。3.2 Web 指纹识别用响应头和页面特征做多规则匹配指纹识别的本质是“看到什么特征猜它是什么框架”。特征来源有三个响应头里的 Server、X-Powered-By页面里的 meta generator还有特定路径的文件如 /wp-login.php 存在说明是 WordPress。最省事的实现是维护一个规则列表每条规则带一个 judge 函数。import requests import re FINGERPRINT_RULES [ { name: WordPress, headers: {X-Powered-By: php}, body_regex: rwp-content|wordpress, path: /wp-login.php, }, { name: ThinkPHP, headers: {X-Powered-By: PHP}, body_regex: rThinkPHP|thinkphp, path: /index.php, }, ] def fingerprint(url, sessionNone): if session is None: session requests.Session() try: resp session.get(url, timeout(5, 10), allow_redirectsTrue) except requests.RequestException: return [] text resp.text[:200000] hits [] for rule in FINGERPRINT_RULES: score 0 for k, v in rule[headers].items(): if resp.headers.get(k, ).lower() v.lower(): score 1 if rule[body_regex] and re.search(rule[body_regex], text, re.I): score 1 if rule[path]: try: p session.get(url.rstrip(/) rule[path], timeout(3, 5)) if p.status_code 200: score 1 except requests.RequestException: pass if score 2: hits.append(rule[name]) return hits逻辑说明评分逻辑是每个命中的特征加 1 分score 大于等于 2 才输出指纹。为什么不用单特征命中因为 X-Powered-By 为 PHP 的站点一半以上是普通 PHP 页面不是 ThinkPHP单靠响应头会把误报率拉高到没法看。path 探针的作用是二次确认比如 WordPress 的 /wp-login.php 返回 200 时权重很高。参数说明text 截断到 200000 字符是防止目标首页是超大文件或者流式响应匹配这么多内容已经足够覆盖绝大多数框架特征allow_redirectsTrue 是为了拿到最终真实页面的响应头很多站点会把根路径重定向到 /index.php不跟随重定向就拿不到最终的指纹。3.3 端口探测TCP connect 就够了别碰 SYN 扫描端口探测在毕设里的定位是“为漏洞扫描提供端口层面的入口提示”不需要扫全端口。常见做法是只扫一个常见端口列表用 socket 的 connect_ex 发一个 TCP 连接尝试连接成功就认为端口开着。SYN 半开扫描需要构造原始数据包在 Windows 下还需要 Npcap 驱动纯 Python 实现还要用 scapy这些对毕设来说都是不必要的复杂度。COMMON_PORTS [21, 22, 23, 25, 53, 80, 443, 3306, 3389, 6379, 8080, 8443] def scan_ports(host, portsNone, timeout1.0): ports ports or COMMON_PORTS open_ports [] for port in ports: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) result s.connect_ex((host, port)) s.close() if result 0: open_ports.append(port) return open_ports逻辑说明connect_ex 的返回值是错误码0 表示连接成功。这个函数逐个端口串行探测COMMON_PORTS 只有 12 个端口最坏情况等 12 秒演示时可以接受。参数说明timeout1.0 是连接超时目标主机不可达时每个端口要等 1 秒所以想再快可以把 timeout 降到 0.5但内网环境丢包率稍高时误报率也会上升。需要并发时可以用线程池包一层但毕设串行已经够用串行的好处是打印日志时顺序清晰答辩演示时能一条条看到探测进度。端口探测的结果还要跟 HTTP 服务识别联动对 80、443、8080、8443 这几个端口直接请求 http 或 https 的根路径把响应状态码和 Server 头存下来供漏洞扫描模块选择目标。这一步本质上就是 python 爬虫的请求逻辑在信息搜集模块里复用一个带 Session 的 utils 函数即可不用重复写请求代码。这样信息搜集模块算下来只有三个函数、两个数据文件字典和规则维护成本很低。4. 漏洞扫描模块SQL 注入与 XSS 的检测逻辑、判定参数和调度漏洞扫描模块是整个系统的核心也是答辩时老师追问最多的地方。不要想着把 OWASP Top 10 全做一遍毕设能讲清楚两个漏洞的完整检测闭环已经很扎实SQL 注入和 XSS。这两个漏洞的检测思路有本质区别放在一起恰好能展示你对检测原理的理解。扫描器的输入是信息搜集阶段得到的 URL 列表和指纹结果输出直接写进 vulns 表。4.1 检测流程参数从哪里来payload 往哪里投拿到 URL 后第一件事是解析参数带问号的 URL 直接拆参数名不带参数的页面要解析页面里的a链接和form表单。这一步用 BeautifulSoup 就能做逻辑不复杂但决定了扫描覆盖率。如果参数提取不全后面检测器再厉害也扫不到那些入口。from urllib.parse import urlparse, parse_qsl from bs4 import BeautifulSoup def extract_params(url, htmlNone): params [] parsed urlparse(url) if parsed.query: params.extend([k for k, _ in parse_qsl(parsed.query)]) if html: soup BeautifulSoup(html, html.parser) for a in soup.find_all(a, hrefTrue): p urlparse(a[href]) if p.query: params.extend([k for k, _ in parse_qsl(p.query)]) for form in soup.find_all(form): for inp in form.find_all(input, {name: True}): params.append(inp[name]) return list(set(params))逻辑说明parse_qsl 会把 URL 里的 kv 拆成元组列表我们只取参数名。从 HTML 提取时同时看 a 标签和 input 标签这是最常见的两个参数入口。参数说明返回的是去重后的参数名列表同一个参数在多个 URL 出现时只测一次避免重复请求浪费扫描时间如果页面有分页参数 page1size20两个参数都会被提取但实际扫描时建议只测 page 和 size 这种业务参数跳过 token 和 session 这类明显不参与 SQL 拼接的字段可以在 extract_params 里加一个过滤名单。扫描调度采用线程池加任务队列每个 URL 一个任务线程池大小限制在 5 到 8 之间这样不会把个人电脑跑满也不会对目标站点产生太大压力。每个任务内部依次执行 SQL 注入检测和 XSS 检测两个检测器共用同一个 requests.Session保持 Cookie 上下文一致避免因为会话断开导致误报。任务结束把状态改为 finished整个流程通过 scan_tasks 表串起来。4.2 SQL 注入检测报错特征和时间差两种判定SQL 注入的检测思路是给参数塞入 SQL 语法片段观察目标数据库的响应有没有异常。报错注入适用于开了错误回显的站点时间盲注适用于页面无任何报错信息但对数据库查询耗时敏感的站点。两种判法互为补充一个都不能少。import time import re SQLI_PAYLOADS [ {payload: , keyword: sql syntax|SQL syntax|mysql_|ORA-|sqlite}, {payload: ) OR 11, keyword: sql syntax|ORA-|syntax error}, {payload: AND SLEEP(3)-- -, type: time, sleep: 3}, ] def detect_sqli(base_url, param, session, timeout8): for item in SQLI_PAYLOADS: params {param: item[payload]} if item.get(type) time: start time.time() session.get(base_url, paramsparams, timeouttimeout) cost time.time() - start if cost item[sleep] - 0.5: return {param: param, payload: item[payload], type: time, detail: fcost{cost:.2f}s} else: r session.get(base_url, paramsparams, timeouttimeout) if re.search(item[keyword], r.text, re.I): return {param: param, payload: item[payload], type: error, detail: r.text[:200]} return None逻辑说明时间盲注判定的阈值写作cost sleep - 0.5比如 sleep 是 3那么只要请求耗时超过 2.5 秒就判定疑似命中留 0.5 秒给网络延迟误差。为什么不直接大于 3因为 SLEEP(3) 在实际查询里可能因为多行记录串联执行而略微超过 3 秒但几乎不可能低于 2.5 秒。参数说明timeout8 必须大于 sleep 值否则请求会在 sleep 完成前被超时中断永远测不出时间差这个 8 是“请求最迟 8 秒返回”的兜底不是期望耗时。timeout 和 sleep 的差值至少要留 3 秒以上网络差的场景甚至要留 5 秒。报错注入的关键字匹配用的是正则覆盖 MySQL、Oracle、SQLite 三类报错特征。“sql syntax|SQL syntax|mysql_|ORA-|sqlite”这组特征在误报和漏报之间比较平衡。注意这里的请求用的是 GET 参数拼接如果目标站点的参数是 POST需要改成 session.post 并把参数放进 data检测逻辑完全一样。还应该注意报错注入的响应文本量很大时正则匹配不要用贪婪跨行用 re.I 忽略大小写就够了。4.3 XSS 检测反射点判定和编码绕过的处理反射型 XSS 的判定标准是“payload 或 payload 的关键特征出现在响应里”说明输入被原样输出了。但也有站点会做转义把 转成 所以判断时要区分“原样反射”和“转义后反射”。毕设里可以只对原样反射报高危转义后的作为低危忽略这样误报率比较低。XSS_PAYLOADS [ scriptalert(1)/script, img srcx onerroralert(1), svg/onloadalert(1), ScRiPtalert(1)/ScRiPt, ] def detect_xss(base_url, param, session, timeout8): for payload in XSS_PAYLOADS: r session.get(base_url, params{param: payload}, timeouttimeout) ctype r.headers.get(Content-Type, ) if text/html not in ctype.lower(): continue if payload in r.text: return {param: param, payload: payload, type: reflected} return None逻辑说明Content-Type 的判断是为了排除 JSON 接口和纯文本接口它们反射字符串不算 XSS。payload 列表里大小写混写的那条是为了覆盖“只做小写转义”的低水平过滤逻辑。参数说明ScRiPt这条在现实站点里命中率不高因为很多过滤规则是区分大小写的浏览器却能识别把它放进去的目的是展示你理解编码绕过答辩时可以讲清楚为什么要保留这条。一个容易忽略的点requests 会自动做 gzip 解码resp.text 拿到的是解码后的文本。如果你自己用 socket 裸发 HTTP 请求必须手动处理 Content-Encoding: gzip否则 payload 在压缩流里怎么都匹配不到。用 requests 就避开了这个坑这也是扫描器里所有请求都走 Session 而不是直接开 socket 的原因。写完后把 detect_sqli 和 detect_xss 的返回结果统一写入 vulns 表再更新 scan_tasks 的状态字段整个扫描闭环就完成了。5. 避坑源码跑通时最容易翻车的五个地方这一章的每一条都是实际跑这类毕设源码时常见的坑按“现象→原因→解决”写。如果你是基于别人的源码二次开发先对照这一章把坑填了再动功能如果你自己从零写也能省下大量调试时间。这些问题在本地环境和答辩演示时出现的概率都很高值得逐条过一遍。5.1 数据库表建好了数据却写不进去现象启动时 db.create_all() 没报错页面也能打开但提交扫描任务后列表一直是空的刷新后数据消失。原因SQLite 的数据库文件路径用的是相对路径Flask 的当前工作目录跟启动位置不一致时文件会被创建在不同目录。更隐蔽的是扫描线程和 Flask 主进程同时写同一个 SQLite 文件会触发 database is locked 报错表现同样是数据写不进去。解决config.py 里用绝对路径拼数据库文件位置把SQLALCHEMY_DATABASE_URI改成sqlite:///加os.path.join(BASE_DIR, vulnscan.db)。并发写入问题给连接加check_same_threadFalse并把扫描逻辑里的 session 改为每次独立创建不要跨线程共享同一个 session 对象。判断路径对不对有个小技巧打印db.engine.url看数据库文件实际落在哪然后对比项目的 BASE_DIR一眼就能看出是不是路径漂移了。改完之后删掉旧的 db 文件重新建一次表别在旧文件上迁移毕设阶段不值得花时间在迁移上。5.2 扫描任务永远停留在 running卡死不动现象点击开始扫描后任务状态一直是 running等五分钟也不变日志没有异常输出CPU 占用也很低。原因requests 请求没有设 timeout或者 timeout 设得比目标服务器响应时间短。还有一个常见原因是 socket 端口探测的 timeout 设成 0等于无限等待。扫描线程被一个永不返回的请求占满线程池里的其他任务全部排队看起来就像是死锁。解决所有网络请求统一收口到 utils.py 里的一个函数强制设置timeout(5, 10)连接 5 秒、读取 10 秒socket 操作逐个settimeout(1.0)。然后给扫描线程池加一个总任务时长上限单个 URL 超过 60 秒直接标记 failed不要让它无限卡住。线程池的队列也建议设一个 maxsize满了就拒绝新任务避免内存被任务对象堆满。调试时在任务开始和结束各打一行日志停住时看最后一条日志停在哪就能定位是哪个请求没回来。5.3 指纹识别几乎全报 WordPress误报率惨不忍睹现象随便扫一个普通的 PHP 站点报告里能同时列出来 WordPress、ThinkPHP、DedeCMS 三种指纹明显不可能。原因早期版本只要匹配到 X-Powered-By: PHP 就输出“可能为 WordPress”单个特征命中即判成功。这种宽松规则在绝大多数 PHP 站点上都会误报因为 PHP 只是语言环境不是具体框架。指纹识别的核心是“多个独立特征同时指向同一个框架”只拿语言环境特征当数值必然翻车。解决改成多特征评分制每条规则至少命中 2 个特征才输出比如 WordPress 必须同时满足 X-Powered-By 是 PHP 和 body 出现 wp-content 两个条件。在 fingerprint() 里把 score 阈值从 1 提到 2误报率会立刻下降。改完用几个已知框架的站点回归一遍确认 WordPress、ThinkPHP 各自能被正确识别再调阈值。这个参数写在指纹匹配函数末尾的判断里是所有误报问题的总开关。给规则加一个最后修改时间字段答辩时能说清楚这套规则你一直在维护不是抄来的死配置。5.4 XSS payload 在本地能测出换到目标站点全部哑火现象用 SQLi-Labs 或自己写的测试页验证反射型 XSS检测器能报漏洞换成真实站点后同一个 payload 全无反应。原因真实站点做了三层干扰。第一HTML 实体编码把 转成 响应里不再有原始 payload 字符串第二站点有 WAF 拦截带
返回列表