Learning about XSS
XSS概念
XSS(跨站脚本攻击)是一种常见的网络安全漏洞,攻击者将恶意的客户端脚本(通常是JavaScript)注入到用户访问的合法网站中,当用户浏览器执行这些脚本时,攻击者就能窃取用户数据(如Cookie)、冒充用户身份、劫持会话或在页面上执行任意操作。
XSS 攻击的类型
Stored XSS
前端–>后端–>数据库–>后端–>前端
存储型XSS,也叫持久型XSS,主要是将XSS代码发送到服务器(不管是数据库、内存还是文件系统等),然后在下次请求页面的时候,服务器从数据库中读取数据,没有做XSS相关处理,直接输出给浏览器,导致攻击代码被执行。
讲恶意的数据包含在HTTP响应中,在后续HTTP响应中会继续执行
这一类典型案例:留言板XSS
Reflected XSS
前端–>后端–>前端
此类型不会被存在资料库中,主要透过用户发出恶意的请求,倘若后端没有过滤而直接将结果回传前端的话,就有可能执行到恶意的代码
攻击者构造一个包含脚本的URL,并让正常用户打开这个
url,这样XSS代码出现在请求URL中,作为参数提交到服务器,服务器对参数没有进行XSS相关处理,直接解析并响应。响应结果中包含XSS代码,最后浏览器解析并执行。攻击过程中恶意数据被服务器“反弹”给用户的浏览器,故称为反射型XSS。
特点:反射型XSS主要服务端没有对外来数据进行XSS处理,就直接发送给浏览器,导致出现XSS攻击。
1 | <script>alert("hey!");</script> |
DOM-Based XSS
- 前端–>浏览器
- 这种情况不依赖于是否将恶意程序放置于服务器,而是修改页面的DOM节点,效果上说也属于反射型XSS。
- 文档对象模型(DOM)是一个网络文档的编程接口。 它代表页面,以便程序可以改变文档的结构、风格和内容。 DOM 将文档表示为节点和对象;这样,编程语言就可以与页面交互。 网页是一个既可以在浏览器窗口中显示,也可以作为HTML 源代码的文档。
- 客户端JavaScript可以访问浏览器的DOM文本对象模型是利用的前提,当确认客户端代码中有DOM型XSS漏洞时,并且能诱使(钓鱼)一名用户访问自己构造的URL,就说明可以在受害者的客户端注入恶意脚本。
- 直接交给浏览器执行
反射型和DOM型的差异理解:
- 总结一下就是:反射型的
xss会把写好的内容直接放进HTML代码里,就是会接触服务器,而DOM型是通过网页本身的JavaScript进行渲染触发的,DOM型XSS的payload不直接输出于相应体中,而是通过一段js处理的方式,把这个payload以某种方法给带出来,再在前端加载的时候实际触发。
XSS靶场wp-1
0x00
1 | <script>alert(1)</script> |
0x01
标签</textarea>会让输入的内容作为文本,而不会被JavaScript解释器处理,所以把前面的标签截断
1 | </textarea><script>alert(1)</script> |
0x02
提交变量value
先闭合让script语句单独出去作为一个单独的HTML语句执行
1 | "><script>alert(1)</script> |
后面的可以注释掉
0x03
stripbracketsRe的作用是将指定的符号替换 这里是被替换为空
HTML编码只能放在标签内,所以如果用script标签就不能采取这种方式
编码有十进制和十六进制,是十六进制的可以显示完成,而十进制的会直接弹窗,也就是说实际上是注入成功了的,但是靶场没有设置这个检查
1 | <img src=0 onerror=alert("hack")> |

1 | <img src=0 onerror="alert(1)"> |
onerror事件在加载外部文件(文档或图像)发生错误时触发。
随意命名的图像或者文档本来就是不存在的,所以一定会出现这个报错
反引号也可以
1 | <script>alert`1`</script><!-- |
0x04
编码方式同上
<img src=0 onerror=alert(1)>
0x05
单行注释符被替换
但是可以用多行注释符闭合
--!><script>alert(1)</script>
0x06
input = input.replace(/auto|on.*=|>/ig, '_')
不能闭合了,所以只能用图片报错来
这里的on匹配不是直接和等号匹配的,on.*=/ig 会匹配 on 字符后面除了换行符以外的任何字符后的等号
这里感觉和C语言的输入替换有点像,和%*c那个一样,但是C的可以用于处理换行符,这里利用的是不能替代换行符
type="image" src="x" onerror
="alert(1)"
0x07
function render (input) {
const stripTagsRe = /<\/?[^>]+>/gi
input = input.replace(stripTagsRe, '')
return `<article>${input}</article>`
}
html即使没有右边尖括号依旧可以执行
过滤了 <任意大于 0 个的除了右尖括号以外的字符> 和 </任意大于 0 个的除了右尖括号以外的字符> 这样的结构
<img src="x" onerror="alert(1)"
0x08
直接用会被替换
HTML 标签在文字与右尖括号之间的空格不会影响解析,因此使用空格绕过
换行符也可以
</style ><script>alert(1)</script>
或者
</style
><script>alert(1)</script>
0x09
必须包含http的部分
function render (input) {
let domainRe = /^https?:\/\/www\.segmentfault\.com/
if (domainRe.test(input)) {
return `<script src="${input}"></script>`
}
return 'Invalid URL'
}
然后这里需要加入的网址本身就是错误的地址可以直接用onerror,如果是可用的网址,那么就双写
0x0A
function render (input) {
function escapeHtml(s) {
return s.replace(/&/g, '&')
.replace(/'/g, ''')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\//g, '/')
}
const domainRe = /^https?:\/\/www\.segmentfault\.com/
if (domainRe.test(input)) {
return `<script src="${escapeHtml(input)}"></script>`
}
return 'Invalid URL'
}
src:指定资源内容,(包括路径或者文件内容之类的)
这里访问直接就是对应的js文件
https://www.segmentfault.com.haozi.me/j.js
使用 @ 会以 @ 前的字符串作为用户名来访问 @ 后面的网址
是个新知识,但是这里用@没成功,不知道为什么
0x0B
function render (input) {
input = input.toUpperCase()
return `<h1>${input}</h1>`
}
<h1>这个是标题格式
所以就是呈现不需要闭合前面的
大小写不影响标签的执行,只是输出会改变大小写,所以直接加
- HTML 标签和属性名称不区分大小写
但是JavaScript解释器是需要分析大小写的所以编码绕过
<img src=1 onerror="javascript:alert(1)">
0x0C
function render (input) {
input = input.replace(/script/ig, '')
input = input.toUpperCase()
return '<h1>' + input + '</h1>'
}
用img标签或者双写
payload
<img src=1 onerror="alert(1)">
理论上双写应该可以,但是没成功,为什么
0x0D
function render (input) {
input = input.replace(/[</"']/g, '')
return `
<script>
// alert('${input}')
</script>
`
}
这里input直接拼到alert上,通过回车避开前面的(',再用注释符去掉闭合的')
1 | alert(1) |
这个注释符可以闭合单行注释也可以闭合多行注释,闭合之后就无视之后的部分
0x0E
1 | function render (input) { |
如果字母和左尖括号放在一起,字母前会被加上下划线,哇正则表达式你也是学到了
标签不能用回车来绕过,会无法读取,这里用到了古英文
1 | <ımg src="x" onerror="alert(1)"/> |
ſ 大写后为 S(ſ 不等于s)
也有可能是Unicode编码
0x0F
1 | function render (input) { |
括号没有被过滤,可以正常被闭合
1 | ');alert(1)(' |
闭合前后,相当于三个命令的执行,一个为空一个为alert函数,一个是console.error函数
0x10
Window 对象描述 Window 对象表示一个浏览器窗口或一个框架。在客户端 JavaScript 中,Window对象是全局对象,所有的表达式都在当前的环境中计算。也就是说,要引用当前窗口根本不需要特殊的语法,可以把那个窗口的属性作为全局变量来使用。
1 | function render (input) { |
所以这里input直接给需要执行的命令,不需要任何标签
0x11
1 | // from alf.nu |
JS 解析的优先级是「先处理转义符 → 再执行表达式 → 最后处理语法错误」,这也是 alert 能执行的关键 也就是说转义符会被直接无视 等于没有黑名单 直接照着前面闭合就可以了
会对特殊字符进行转义,但是注入的位置在js的字符串中,因此转义过的字符也会被正常解析
也就是说,这里转义与否只会在html呈现中有体现,不影响实际执行内容,所以正常闭合,然后该注释掉的注释掉,注意这个是在文字中间所以,注意语法完整
1 | ");alert(1)// |
0x12
1 | // from alf.nu |
转义转义符
payload呈现的就是把已经转义的直接交上去,当成是已经被转义的内容。
1 | \");alert(1)// |
相当于拼凑一个转义的结果,促使执行的时候就按照转义转义符之后的内容写
拼接的结果:
1 | <script>console.log("\\");alert(1)//");</script> |
双引号的部分就可以被解析出来
XSS challenge

抓包内容里有对应脚本
前端动效部分不用管
1 | <script> |
eval() 是一个功能强大的内置函数,主要用于将字符串解析为有效的表达式、数值或语句并执行,随后返回执行结果。它广泛用于 Python、JavaScript、PHP 等编程语言中,常用于动态计算算术表达式、转换数据类型(如字符串转字典/列表)或执行代码,但因其可执行任意代码,存在严重的安全风险。

要求中还表示需要包含alert(document.domain),同时有长度限制
长度检查有q和qs
注入位置是innerHTML:
innerHTML操作的是元素的内部内容。outerHTML操作的是包括元素自身在内的整个 HTML 结构。
JS 的解析逻辑
按行扫描 行内必须是合法 JS 新的一行是新的语句开始点
- HTTP 协议规范规定:一行头必须以 CRLF 结尾
- 这一行结束了,下一行当成新的语句重新解析
在JavaScript中,使用innerHTML属性将一个字符串(假设为qs)作为HTML解析并插入到DOM树中
- 页面初始加载时,浏览器会解析HTML文档,构建DOM树。
- 在页面加载后,JavaScript代码执行,获取到DOM元素(
contentDOM),然后设置其innerHTML属性为字符串qs。 - 浏览器会将字符串
qs当作HTML来解析,并生成相应的DOM节点,然后插入到contentDOM元素中,成为其子节点。 - 通过
innerHTML动态插入的<script>标签,默认不会执行
这样的句式,实际上执行的也是alert(document.domain)
对特殊字符进行编码可以防止过滤
1 | ?q=<svg onload=eval(uri)>&first=yes\r\nalert(document.domain) |
1 | ?q=%3csvg%20onload%3deval(uri)%3e&first=yes%0d%0aalert(document.domain) |
XSS靶场wp-2
level-1
这一关是script标签的注入
level-2
这一关GET提交两个变量,value的内容可以作为突破口,因为在相关结果展示的时候会有字符编译

但是value部分就可以依靠">闭合,让这个script语句放出来

level-3

HTML的解析原则

被编译力,但是发现单引号不会被编译,所以还是可以直接闭合
htmlspecialchars()函数把预定义的字符转换为 HTML 实体。预定义的字符是:
1 | - & (和号)成为 & |
onclick事件:
当鼠标点击时执行JavaScript代码,也就是先上传代码然后执行
添加鼠标点击
先用单引号闭合,然后相当于加上一个新的语句
payload:
1 | 'onclick='alert(1) |
这里前一个闭合原本value那个单引号,后一个接上后面的
level-4

这里可以发现提交的双引号不会被转义,但是尖括号被忽略了
就导致无法直接结束这个语句写入script语句,那么就和上一个题一样
level-5

验证发现不是字符位数的问题,应该是关键字检测替代

onclick和script都在其中
不只是onclick,只要是on类型的都会被处理,包括onerror onload onlaodstart onpageshow都不能用
JavaScript伪协议:
将JavaScript代码添加到客户端的方法是把它放置在伪协议说明符javascript:后的URL中
也就是说javascript:后加的内容可以被JavaScript解释器执行,但是这里还是在HTML里,所以还是需要放到其他标签里,诶
1 | "><iframe src="javascript:alert(1)"></iframe> |
这里两种,第二种就是插入链接,然后点击链接就会被JavaScript解释器执行,如果是正常链接那就执行链接跳转,如果不是链接就不跳转
iframe 是一种使用HTML 结构的内嵌框架。 借助此框架,您可以将一个HTML 文档插入另一个HTML 文档中并使其显示为网页。 它以 <iframe> 标记表示。
第一种执行之后弹窗返回1,也会生成一个内嵌HTML,但是没有返回执行成功,不确定是不是我没有对那个框架做进一步操作的原因
level-6
这关还是关键词的替换
所以a href这个就没法直接用了
大小写绕过
level-7
这关甚至都不保留了,而且大小写绕过也会被处理

替换成空,那就双写绕过

做错了,javascript:alert(1)这个地方的script也被处理了
所以还要再双写一次
level-8

都会被转义,然后有一个纯天然链接
script会被替代,大小写绕过不行
处理的内容都是HTML解释器的内容,HTML字符实体转化,就是相当于编码之后再交
level-9

直接交显示不合法
直接尝试没试出来,所以去看了源码
发现保留了对关键字的替换,对于链接合不合法固定的比较死,只是固定”http://“,所以在绕过的基础上加一个http://
是白名单,但是要注意把前后链接截断来
level-10
删除了输入窗口

尝试对三个变量做改动,发现只有t_sort变量是可控的,所以闭合t_sort变量,但是还是没有变化,把type改成text

1 | ?t_sort=" type="text" onclick="alert('xss') |
这时候有显示之后才可以click触发onclick
level-11
这一关和上一关一样都有隐藏元素

起始状态下会显示上一关的url,提交一次之后发现,t_sort和t_ref都有变化,但是上传上一关的内容,就会发现t_sort变量被实体化了

t_ref的value的值是前一个页面的地址,通过$_SERVER['HTTP_REFERER']也可以获取前一个页面的地址,所以referer请求头也成了输出参数。
抓包改referer请求头,这里直接抓一开始跳转到11的页面,因为在这个时候是在读取referer的

这里使用的payload:(不改type也可以,因为不需要点击触发,不需要可以点击的点)
1 | " type="text" onmousemove="alert(1) |
在跳转前还出现了输入框,可以用onclick
1 | "type="text" onclick="alert(1) |
level-12
对比和上一关的相似处,判断带了什么内容过来
这一关是User-Agent头
payload一样
level-13
cookie头
进阶xss的学习
xss进阶技巧也要求系统掌握大部分xss的绕过方法
浏览器渲染过程
同源策略
XS-Leak
