Python轻量级XSS检测脚本:从反射点探测到Payload验证
简介这是一个基于Python开发的XSS漏洞检测脚本资源面向安全测试初学者、开发者和CTF爱好者用来快速识别网页中的跨站脚本注入风险。整个压缩包体积仅3.19MB共87个文件包含37个核心Python源文件、33个编译后的pyc模块、7个txt字典文件、3个bat批处理脚本以及README、sh等辅助文件结构清楚便于按需取用。已有366人学习下载。资源在BruteXSS框架基础上集成了mechanize表单交互、colorama终端输出、多档payload字典small/medium/huge并配套Windows/Linux启动脚本开箱即用核心脚本会向目标URL注入XSS载荷结合页面回显判断漏洞是否存在可直接用于站点安全自查或漏洞验证。对于想搭建轻量级Web安全检测工具、学习XSS漏洞自动化原理的读者这套源码提供了完整且可扩展的参考实现。1. 一个 Python 脚本为什么还要专门设计做安全测试的朋友大概率都有过这种经历拿到一个授权站点参数几十个手工点一遍累得半死Burp 的 Intruder 又觉得杀鸡用牛刀。这时候用 Python 写一个轻量 XSS 漏洞检测脚本把候选参数批量过一遍快速锁定反射点和潜在注入点是效率最高的做法。这个标题里说的简单高效指的就是这个场景——不是要替代专业扫描器而是在你已有判断的基础上把重复劳动自动化。这个脚本解决两个具体问题一是探测参数点哪些存在回显二是验证回显点能否被 payload 触达。它的核心不是更多 payload而是更准的反射点判断。适合谁用做授权渗透测试的工程师、打 CTF 的选手、自学 Web 安全的初学者。你不需要分布式扫描集群一台笔记本、一个 requests 库、一个靶场环境就能把这套逻辑跑通。下面我按自己的实现思路把这套脚本从原理到代码再到踩坑完整拆给你。2. 从请求到回显XSS 检测脚本的三个核心设计决策2.1 为什么要先探测反射点而不是直接灌 payload很多人写 XSS 检测脚本第一步就是拿一堆 payload 往参数里灌然后看响应里有没有 payload 特征。这样做的最大问题是误报率高得离谱。因为目标参数可能压根不输出或者输出到了 JS 上下文或者被 HTML 实体编码你灌一百个 payload每一个都能在响应里找到字符串但每一个都不产生实际执行。我一般的做法是先做反射点探测。所谓反射点就是参数值被服务端接收后原样或经过变换出现在响应页面里的位置。先发一个唯一标记字符串比如xss_probe_20240601在返回的 HTML 里搜这个字符串看它出现在哪里、有没有被转义、被截断。这一步的信息量远比跑一轮 payload 大。探测结果通常分三类完全没回显、原样回显在 HTML 正文、回显进了标签属性或 script 块里。第三种才值得继续投入 payload前两种要么跳过要么需要不同的 payload 策略。这个判断逻辑写进脚本里能让整体效率提升好几倍因为减少了大量无效请求。2.2 Payload 设计通用标签、事件属性、编码绕过三层递进确定反射点之后payload 集合不需要贪多但要有层次。我习惯把 payload 分成三层。第一层是通用探测型目标是最小代价验证能不能弹窗。scriptalert(1)/script、img srcx onerroralert(1)这两个最典型前者验证直接脚本注入后者验证标签闭合后的属性注入。第二层是上下文适配型针对反射点出现在属性值里的场景用 onfocusalert(1) autofocus x这种闭合引号的方式出现在 script 块里就用/scriptscriptalert(1)/script闭合标签。第三层是编码绕过型img srcx onerroralert(1)的 HTML 实体编码变体、大小写混写、tab 键替代空格用来碰运气绕简单的 WAF 规则。这三层的请求次数差异很大。第一层每条 payload 只需要一次请求第二层需要先确认上下文再决定用哪条第三层则是逐条尝试。脚本里我给这三层设置了不同的优先级权重默认只跑前两层第三层做成可选开关避免把扫描时间拉长到不可接受。2.3 判定回显的标准不是出现了而是出现在了可执行位置新手最容易在这里翻车。用if self.probe_marker in response.text判断有没有反射大概率会收到一堆无效结果。真正要判断的是三个维度。第一回显位置。用正则或字符串查找定位到 probe 标记周围的 HTML 片段看它是在标签内、属性内、注释内还是 script 标签内。第二是否被转义。看响应里lt;scriptgt;这种实体编码是否出现如果出现了说明服务端做了输出编码原样 payload 打不进去需要走编码绕过或换注入点。第三长度是否被截断。有些场景服务端会截断参数长度导致 payload 写不全这需要检查 probe 标记附近有没有截断标志。这几个维度的判断直接在响应文本上做正则匹配就行不需要引入浏览器引擎。当然它判断不了真正的执行结果但作为候选漏洞排序已经完全够用。脚本输出里我会把反射点的上下文和转义情况一起打出来方便人工复核。3. 核心代码实现一个能直接跑的最小化脚本3.1 请求封装与反射点探测模块先写请求封装和反射探测。这一步是整个脚本的地基。import requests import re import urllib3 from urllib.parse import urlparse, parse_qs, urlencode urllib3.disable_warnings() class XSSProbe: def __init__(self, base_url, cookiesNone, timeout10, headersNone): self.base_url base_url self.session requests.Session() self.session.cookies.update(cookies or {}) self.session.headers.update(headers or { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) self.timeout timeout self.marker fxss_probe_{hex(id(self))[2:]} def probe_param(self, url, param): parsed urlparse(url) params parse_qs(parsed.query) params[param] [self.marker] target parsed._replace(queryurlencode(params, doseqTrue)).geturl() try: resp self.session.get(target, timeoutself.timeout, verifyFalse) except requests.RequestException: return None if self.marker not in resp.text: return None ctx self._locate_context(resp.text) escaped self._check_escaped(resp.text) return {url: target, context: ctx, escaped: escaped, status: resp.status_code}这里用了 session 对象而不是裸的 requests.get是为了保持 cookie 和 header 的一致性。parse_qs重新拼接 URL 的方式能保留其他参数不丢失。verifyFalse 关掉证书校验因为很多内网测试环境证书是自签的但要注意它会触发 urllib3 的警告所以第一步就把警告禁用掉了。判断回显位置的方法_locate_context很关键我单独拆出来说。3.2 定位回显上下文与转义判断def _locate_context(self, html): idx html.find(self.marker) if idx -1: return no_reflection start max(0, idx - 120) end min(len(html), idx len(self.marker) 120) fragment html[start:end] if re.search(rscript[^]*, fragment): return script_block if re.search(r[a-zA-Z][^]* re.escape(self.marker), fragment): return html_tag_attr if re.search(r!--, fragment): return html_comment return html_body def _check_escaped(self, html): idx html.find(self.marker) raw html[max(0, idx - 20):idx len(self.marker) 20] return lt; in raw or #60; in raw or amp; in raw这段用「取上下文片段再做正则分类」的方式比全文扫描快得多也更容易定位问题。需要注意的是正则里对 marker 做了 re.escape 处理防止 marker 里的特殊字符干扰匹配这是个容易忽略的细节。转义判断我这里只查了的实体编码实际上、、也可能被编码更严格的写法是把常见实体映射表全查一遍。但对于第一版探测只看尖括号编码就足以决定后续 payload 路线了。提示这里没有用 BeautifulSoup只用了标准库 re。原因是 HTML 上下文定位本质是模糊判断不需要严格解析 DOM正则足够快而且少一个依赖脚本复制到任何机器上都能跑。3.3 Payload 验证模块与结果输出探测完反射点进入 payload 验证阶段。PAYLOADS { script: scriptalert(1)/script, img_error: img srcx onerroralert(1), svg: svg onloadalert(1), attr_close: \ onfocusalert(1) autofocus x\, } def verify(self, url, param, context): results [] candidates [] if context in (html_body, html_tag_attr): candidates [script, img_error, svg] elif context script_block: candidates [/scriptscriptalert(1)/script] elif context html_tag_attr: candidates [attr_close] for name in candidates: parsed urlparse(url) params parse_qs(parsed.query) params[param] [self.PAYLOADS[name]] target parsed._replace(queryurlencode(params, doseqTrue)).geturl() try: resp self.session.get(target, timeoutself.timeout, verifyFalse) except requests.RequestException: continue if self.PAYLOADS[name].replace( , ) in resp.text.replace( , ): results.append({payload: self.PAYLOADS[name], url: target, type: name}) break return results这里的判断逻辑用的是忽略空格后的子串匹配避免服务端对空格做了压缩或替换导致漏报。不同类型的 payload 匹配逻辑其实有差异比如 attr_close 这种闭合类 payload真正要看的是闭合后的事件属性是否完整出现在响应里。实际写的时候我还会把响应结果里 payload 周围 100 字符的上下文一起存下来存成 JSON 文件方便后续人工验证。def save_report(self, results): import json from datetime import datetime report { scan_time: datetime.now().isoformat(), target: self.base_url, findings: results } with open(xss_result.json, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2)JSON 输出相比终端打印的好处是人工复核时字段完整、格式统一而且后续要接入其他工具链也更方便。ensure_asciiFalse是给中文响应上下文看的不然全变成\uXXXX就没法读了。4. 参数调优与 DVWA 靶场验证让脚本跑出可信结果4.1 在 DVWA 上跑通反射型 XSS 的前置条件代码写完了第一件事是找靶场验证。DVWA 是最常用的选择因为它的 XSS 模块分了 low、medium、high 三个安全级别正好用来验证脚本在不同过滤强度下的表现。需要做的准备有两个。第一把 DVWA 的安全级别调到 low先用最基础的环境验证脚本逻辑通不通第二登录后从浏览器开发者工具里复制 Cookie 值填到脚本的 cookies 参数里。DVWA 所有页面都要登录态不带上 Cookie请求会全部重定向到 login.php脚本在低安全级别下会把重定向页当成目标页出现各种莫名其妙的判定结果。跑之前建议先手工在浏览器里访问一次http://127.0.0.1/dvwa/vulnerabilities/xss_r/?nametest看清楚反射点的输出格式。DVWA 反射型 XSS 是把 name 参数直接拼进 HTML 的pre标签里这决定了后续 payload 触发成功的判断依据。4.2 单参数扫描的完整调用与预期输出if __name__ __main__: target http://127.0.0.1/dvwa/vulnerabilities/xss_r/?nameprobe scanner XSSProbe( base_urltarget, cookies{PHPSESSID: 你的会话id, security: low}, timeout8 ) param name probe_result scanner.probe_param(target, param) if probe_result is None: print(f[*] 参数 {param} 无反射跳过) else: print(f[] 发现反射点: 上下文{probe_result[context]}, 转义{probe_result[escaped]}) findings scanner.verify(target, param, probe_result[context]) if findings: print(f[!] 命中 {len(findings)} 条 payload) for item in findings: print(f payload: {item[payload]}) else: print([-] 未命中可执行 payload) scanner.save_report(findings)预期输出中probe_result 的 context 字段应该是html_bodyescaped 是 False。verify 阶段脚本会先用scriptalert(1)/script测试DVWA low 级别没有对script做过滤所以入库 httponly 被 script 标签闭合后会直接命中输出[!] 命中 1 条 payload。如果 context 显示为html_tag_attr说明你访问的 URL 和参数可能不对或者 DVWA 版本有差异回显被放到了 input 的 value 属性里。这时候脚本会切换成 attr_close payload同样能命中只是触发的原理不同。4.3 不同安全级别下的参数策略调整DVWA 的 medium 级别对 XSS 做了str_replace过滤把script替换成空字符串但只替换了一次。这时候 payload 集合里的 img_error 就能绕过因为onerror事件属性不在过滤黑名单里。high 级别用了 htmlspecialchars 做输出编码直接注入标签的路子基本堵死只能去找那些不做编码的输出点。这三个级别的对比正好说明了一个问题payload 集合的覆盖面比数量重要。脚本里如果只有 script 标签一类 payload在 medium 级别就会误判为无漏洞。我把这个情况做成了--level参数由使用者手动指定目标级别再决定加载哪几层 payload。自动化全量加载不是不行但会让请求数翻倍而且误报率明显上升。注意对 high 级别的 DVWA不要对脚本结果抱太高期望。存储型和其他入口可能还有绕过空间但反射型在 htmlspecialchars 下基本是安全的这种场景脚本的价值是证明核心入口已加固而不是硬找漏洞。5. 避坑与排查XSS 检测脚本最容易翻车的 5 个细节5.1 现象有反射但 payload 全部不执行这是最常见的挫败场景。probe 结果显示参数会原样回显但 script、img、svg 三条 payload 全部无反应。原因基本都出在输出编码上。DVWA 的 high 级别和不少真实站点都是用htmlspecialchars或等价函数做输出编码和被转成了实体导致浏览器不把它当标签解析。解决方法是看响应里 payload 周围是不是出现了lt;scriptgt;而不是等脚本的 escaped 字段变红——实际运行中要么升级编码绕过 payload 做针对性尝试要么接受这个点打不了换参数继续探测。5.2 现象target 页面无限重定向脚本把登录页当成响应页换了新目标站点probe 结果异常比如 context 变成了 html_body 但 URL 变成了 login.php。原因是目标有登录态校验请求未带 Cookie 或 Session 直接 302 到登录页。requests 默认跟随重定向最后拿到的是登录页内容而登录页的 HTML 里往往有隐藏 input 字段probe 标记被塞进了 value 里。解决分两步先从浏览器 Developer Tools 的 Network 面板复制完整请求头塞进 session.headers再把allow_redirectsFalse加进请求参数主动检查 302 响应发现跳转就记录auth_failed不要让脚本静默地跑下去。5.3 现象扫出来的漏洞在浏览器里无法复现脚本报告 payload 命中人工验证却一片空白。排查后发现是 payload 匹配逻辑太宽松——服务端可能做了空格压缩也可能打印了 payload 原文但不在可执行位置。我在这里吃过亏后来加了一个 rule命中结果必须同时满足两个条件。一是 payload 特征出现在响应文本中二是该特征周围 50 字符内不能再出现value或lt;等编码指纹。这个排除编码上下文的规则能过滤掉大概六成的误报。剩下的交给人工复核这是任何自动化脚本都替代不了的一步。5.4 现象并发稍微调高目标站点直接 502把脚本从单线程改成线程池后扫一个老旧的 PHP 站点时报了一堆 502 和超时。原因很直白——目标扛不住压力。很多测试目标本身就是低配服务器几十个并发同时发请求数据库连接池直接打满。解决方法是把并发控制在 3 到 5 个线程每个请求间隔 0.5 秒到 1 秒。扫描器是测试工具不是压测工具触发 502 既拿不到有效结果还可能把业务打挂。稳健的脚本设计里请求延迟和并发数必须暴露成参数让使用者自己按目标情况调。5.5 现象DOM 型 XSS 页面完全没有回显有些页面参数不经过服务端直接由前端 JS 读取 location.hash 或 URL 参数后操作 DOM服务端响应永远固定probe 标记自然不存在。这类场景下基于 requests 的脚本会判定为无漏洞但实际上是检测方式不对。严格检测 DOM XSS 需要无头浏览器执行 JS 后再检查 DOM 变化这是另一个量级的工具。脚本层面的折中方案是识别响应中的关键 JS 特征比如document.write、innerHTML、location.hash、eval(把这些特征打上可能存在 DOM XSS的标记交给人工复核。这是尽力而为弥补不了工具边界但至少不会漏报。6. 让脚本从跑得动进阶到少误报三个值得做的加法最后的落地技巧我推荐加一个「响应指纹确认」步骤。在 verify 阶段命中 payload 后不急着输出结果先把响应里 payload 坐标前后的 HTML 原文拉出来做一次规则复核。具体规则就一条坐标前 100 字符内如果出现开头的实体编码或value属性包围的引号就降级为 suspicious而不是 confirmed。这一个步骤能把误报率压掉一半以上代价只是多几百字节的内存比对值得加。第二个加法是把结果输出改成「文件式原始证据留存」。每次命中把响应里 payload 前后各 200 字写入evidence/目录文件名带上时间戳和目标参数名。好处是写测试报告时不用再重新验证一遍直接从证据目录里拿数据。我在实际项目里用这个方式给开发团队提工单沟通效率提升非常明显。安全测试的本质是拿证据说话不是拿口头结论说话。第三个是给参数名做优先级排序。超出我预期的是很多站点的漏洞集中在少数几个参数上比如搜索框、文件名、回调函数名。脚本里加一个--priority-params参数值为q,search,keyword,file,callback扫描时这些参数排在最前面。这个排序逻辑虽然简单但能让你在有限时间窗口内优先发现真正的漏洞点。我现在的工作流是手工拿 Bra 或开发者工具看一遍页面找出所有带参数的请求脚本自动过一遍反射点再用 payload 验证命中的参数。脚本负责的是从「大量参数」到「少量候选点」的收敛过程这个环节它比人可靠。每个用这套脚本的项目我都建议把扫描结果和人工复核记录一起归档时间久了会形成一套覆盖常见业务场景的 payload 策略库比任何公开的 payload 列表都更贴合你经手的站点。希望这套思路和代码能帮到你让你的测试效率更进一步。本文还有配套的精品资源点击获取