XSS-Labs:从零开始的跨站脚本漏洞探究
在搞定了之前的网络拓扑和虚拟化实验室之后,近期我把视线转向了 Web 前端安全领域。作为应用层安全中绕不开的大山,XSS(Cross-Site Scripting,跨站脚本攻击)一直是我想要深入系统学习的板块
为了不再纸上谈兵,我在本地的虚拟机环境里搭建了经典的 xss-labs 漏洞靶场。XSS 的本质其实很简单,总结起来就是一句话:“服务端未对用户输入进行严格过滤,导致恶意数据在前端被浏览器误当作代码执行”
这里我依旧使用虚拟机+docker的方式去部署靶场,具体流程简单概括如下:
如果你不会部署基础的docker环境,那么请参考我的这篇博客:虚拟机+docker部署靶场
(这里我用的镜像是下图的utrzhegu/xsslabs靶场):

步骤一:拉取该镜像
docker pull utrzhegu/xsslabs:0.1步骤二:查看已经下载的镜像
docker images
步骤三:启动镜像,并重命名为xsslabs,将镜像映射在8088端口(这里你也可以根据你自己情况调整)
docker run -d --name xsslabs -p 8088:80 utrzhegu/xsslabs:0.1浏览器输入虚拟机的IP地址+端口号(发现成功访问靶场🎉)

下次可以输入以下命令启动靶场
docker start xsslabs也可以像我一样,设置开机自启动
docker update --restart=always xsslabs成功部署完成之后,接下来我们开始解题
第一关(level 1)

漏洞分析:通过观察可以确认,程序将 URL 中的 name 参数直接拼接进了 HTML 页面中,这意味着,只要我们构造一段合法的 HTML 标签或 JavaScript 脚本代替 test,浏览器在解析 DOM 树时就会被“欺骗”,从而执行我们的恶意代码
先尝试把test值改成aaa试试
按下F12,打开开发者窗口,查看网页前端源码

这种位置是新手最容易理解的 XSS 场景,叫:HTML 正文回显
然后测试它会不会把 HTML 当成标签解析,我们输入<h1>aaa</h1>

说明 <h1> 被浏览器当成 HTML 标签解析了,这很关键
然后再尝试最基础的 XSS Payload
第一关大概率没有过滤,所以可以直接在name后面试:
name=<script>alert(1)</script>发现页面成功弹窗,即为解题成功

漏洞原因:后端直接把用户传入的 name 参数输出到了 HTML 页面中,没有进行 HTML 实体转义,导致浏览器把用户输入解析成了可执行脚本
防御方式:输出到 HTML 页面之前,应当进行 HTML 实体编码,例如将:
< 转义为 <
> 转义为 >
" 转义为 "
' 转义为 '这种防御方式我们会在第三关(level3)进行绕过
第二关(level2)
我们进入第二关之后,继续沿用第一关的alert弹窗命令,查看有无效果

发现并没有出现弹窗,于是打开F12,查看网页源代码

这说明,我们输入的恶意代码不是单纯出现在页面正文里,而是出现在了一个 HTML 标签的属性里面
我们这里的输入的 <script> 被包在 value="..." 里面,也就是说,它只是输入框的内容,不一定会被浏览器当成真正的脚本标签执行
所以第二关的核心思路是:
先从 value="..." 里面逃出来
再插入真正的 HTML/JS 代码我们需要先用一个双引号 " 把前面的 value=" 结束掉
例如输入:
">页面结构就可能变成:
<input value="">这样我们就从属性值里面出来了,然后再接一个脚本:
"><script>alert(1)</script>最终页面就变成了:
<input value=""><script>alert(1)</script>">最终在HTML里执行的源代码如下:

所以我们在搜索框输入:

即可触发弹窗

第三关(level3)
继续尝试沿用第二关的闭合思路,输入"><script>alert(1)</script>,发现这一关行不通,按下Ctrl+U,查看实际的网页源码

浏览器看到:
<script>alert(1)</script>不会把它当成真正的 <script> 标签,而是把它当成普通文字显示出来
并且它过滤了 <、>、"
所以我们不能继续用 <script> 标签了
这一关的新思路是:
既然不能新建 <script> 标签
那就利用已有的 input 标签,给它添加一个事件属性
比如:
onclick="alert(1)"意思是:当用户点击这个input输入框时,执行 alert(1)
得知了这个原理,那我们尝试下面这个 payload(这里有一个细节,需要注意上图的源码,这里是单引号闭合的,所以这里也需要输入单引号)
' onclick='alert(1)我们输入后,服务器会拼接变成:
<input name="keyword" value='' onclick='alert(1)'>将这个onclick的想法输入到搜索框,点击后即可弹窗

Tips:提交后,记得点击输入框触发
第四关(level4)
我们继续沿用之前第二关(level2)的想法,发现输入"><script>alert(1)</script>之后并没有成功,这说明本关过滤了尖括号,因此不能通过新建 <script> 标签执行 JavaScript
依旧Ctrl+U查看源码,发现以下几点

但是,我们发现"没有被删掉,而这就是第四关(level4)的突破口
我们可以尝试将第三关(level3)的这个 Payload 进行加工(将单引号变成双引号)
" onclick="alert(1)提交后,源代码会变成:
<input name=keyword value="" onclick="alert(1)">此时 onclick 成为 input 标签的事件属性,点击输入框即可触发 JavaScript,完成 XSS
第五关(level5)
初步尝试沿用第二和第三关的想法,发现行不通(原因如下图的文字说明)

那我们换一种方式,用 javascript: 链接
<a href="javascript:alert(1)">点我</a>思路很简单,创建一个超链接,当点击这个链接时,执行 javascript: 后面的代码
所以 Payload 可以写成:
"><a href="javascript:alert(1)">点我</a>为什么这个 Payload 可以绕过?
原始结构是:
<input name=keyword value="你的输入">我们加上制作的超链接之后,代码拼接后变成:
<input name=keyword value="">
<a href=javascript:alert(1)>点我</a>
">拆开看是这样的:
"
关闭 value 属性
>
关闭 input 标签
<a href=javascript:alert(1)>点我</a>
创建一个超链接,点击后执行 JavaScript提交之后页面上会出现一个“点我”的链接
你需要点击这个链接,才会触发弹窗

点击后,即可成功跳转

第六关(level6)
尝试使用第五关的思维,我们输入"><a href=javascript:alert(1)>点我</a>,但是Ctrl+U查看源码后,却发现它返回的结果确是"><a hr_ef=javascript:alert(1)>点我</a>

从源码的这张图可以判断出(也就是它在过滤关键词 href):
< 没有被删
> 没有被删
a 标签没有被删
javascript: 没有被删
alert(1) 没有被删
href 被改成了 hr_ef结合这些,我们发现既然它过滤的是小写的,它会不会只过滤小写 href?
所以我们把"><a href=javascript:alert(1)>点我</a>的小写href替换成全大写的HREF,再带入试试
"><a HREF=javascript:alert(1)>点我</a>惊奇的发现居然出现了我们制作的超链接

点击超链接之后,即可成功弹窗

第七关(level7)
我们尝试重复使用第六关的步骤,发现并没有弹出链接,所以我们Crtl+U,查看源码后发现图片里的情况

也就是说,它看到危险关键词就直接删除
既然它会删除:href,script等关键词,那么我尝试双写试试
那我们可以故意把关键词“套娃”写进去,让它删掉中间那一段以后,剩下的刚好又拼成正确关键词
输入以下内容:
"><a hrhrefef="javascrscriptipt:alert(1)">点我</a>输入完成之后,发现出现了我们熟悉的超链接,点击后即可顺利进入下一关

第八关(level8)
我们看到这里有一个搜索框,那么第一时间想到的肯定是输入javascript:alert(1)看看结果怎么样,点击“添加友情链接”后,发现跳转到了一个错误的URL

查看源码后发现,出现了关键词过滤改写,而且这里是不需要再闭合input标签,也不需要重新插入一个a标签,而是应该直接控制href的值,所以我们尝试使用特殊值,看看能不能绕过
在 HTML 里,浏览器会解析 HTML 实体编码,那尝将期中一个字母转成HTML编码再带入试试(这里我是用的是burp suite自带的转码工具进行转码)

将这个字母s转码后的结果(s)带入到我们之前尝试的语句中可得
javascript:alert(1)再次点击“友情链接”,即可得到正确结果,进入下一关

第九关(level9)
我们显示尝试使用我前一个关卡的思路,输入javascript:alert(1),查看源码后发现

先做一个非常简单的测试,在输入框输入:
http://test.com查看源码后,发现,带有http的语句可以被执行

那就说明,这里的要求输入里必须包含http://
那么我们就可以想到,构造既能执行 JavaScript,又能通过合法性判断的 Payload
javascript:alert(1)//http://test.com最终成功弹窗

*注:这里我一开始尝试使用普通的javascript:alert(1),发现还是有关键词匹配的,会将script替换成scr_ipt,所以这里还是要用转码后的格式
第十关(level10)
打开这一关发现啥也没有,查看源码可发现,有3个属性为 hidden 的隐藏 value

虽然页面上没有输入框,但我们可以手动输入参数,判断后端是否会接收 URL 参数
(这一步不是为了攻击,而是为了确认,隐藏 input 的 value 值是否来自 URL 参数)
&t_link=aaa
&t_history=bbb
&t_sort=ccc将以上内容拼接到网站链接后,再次查看源码,可发现有一个value接受了我们输入的测试参数ccc

因为它是hidden,所以我们点不到它,也看不到它
但是我们可以通过注入,把它变成普通文本框(要注意这里的palyload需要拼接在我们之前发现的&t_sort=后,因为只有这一个才能传入参数)
&t_sort=" type="text" onclick="alert(1)这样原本隐藏的输入框就可能变成一个可点击的文本框,你点击它,就触发 onclick
最后也是成功触发弹窗

第十一关(level11)
这一关我们发现还是依旧啥也没有,所以我们还是Ctrl+U打开网页源代码看看

所以这里我们看到的不是普通 URL 参数控制,而是:
请求头 Referer → 后端读取 → 输出到 hidden input 的 value 里那 Payload 应该怎么构造?
现在注入点是:
<input name="t_ref" value="你的 Referer" type="hidden">所以我们仍然可以用双引号逃逸:
" type="text" onclick="alert(1)最终希望它变成:
<input name="t_ref" value="" type="text" onclick="alert(1)" type="hidden">意思是:
"
闭合 value 属性
type="text"
把 hidden 输入框改成普通输入框
onclick="alert(1)
添加点击事件
后面原本的 "
帮我们闭合 onclick 属性然后页面上会出现一个输入框,点击它就弹窗
所以我们的 Payload 就是:
Referer: " type="text" onclick="alert(1)接下来会需要用到Burp Suite的截包工具(如果你没有Burp Suite,那么还请前往这篇链接进行安装,点我前往安装Burp Suite文章)
接下来我们使用 Burp Suite 进行截包,并使用 Burp Suite 的浏览器进行操作(这样会更直观)

在新打开的浏览器中,输入你第11关(level11)的靶场网址(你的IP地址会和我的不一样,注意区分)
http://192.168.153.141:8088/level11.php?keyword=good%20job!按照下图的步骤,在HTTP请求头里添加 Referer

随后点击“Forward”放行这个数据包,即可出现搜索框,点击即可弹窗

第十二关(level12)
有了上一关的经验,这次我们依旧查看源码,看看网页端写了什么

源码里出现了:
<input name="t_ua" value="浏览器UA内容" type="hidden">这说明后端做了类似这样的事情:
读取 HTTP 请求头里的 User-Agent
↓
输出到 hidden input 的 value 属性中所以第 12 关的注入点不是 URL 参数,而是:User-Agent:这里
那么就很简单了 Payload 就和第 11 关一样,里面,所以还是用双引号闭合
目标是把原来的:
<input name="t_ua" value="你的User-Agent" type="hidden">
变成:
<input name="t_ua" value="" type="text" onclick="alert(1)" type="hidden">这样原本隐藏的输入框会变成普通文本框,点击它就触发 onclick
操作步骤还是和第11关一样,这里我就使用文字来进行简单概述一下
1. Burp → Proxy → Intercept
2. 打开 Intercept on
3. 浏览器刷新:
http://192.168.153.141:8088/level12.php
4. Burp 拦截请求
5. 找到 User-Agent 这一行
6. 把它改成:
User-Agent: " type="text" onclick="alert(1)
7. 点 Forward
8. 回到浏览器,页面应该出现一个小输入框
9. 点击输入框,触发弹窗
最后点击“Forward”放行这个数据包,即可出现搜索框,点击即可触发弹窗

第十三关(level13)
这次打开页面,发现有一行报错,翻译成中文是
警告 :无法修改标头信息 - 标头已由(输出始于 /var/www/html/level13.php:1)发送,位于 /var/www/html/level13.php 第 16 行根据前几关的思路,我们发现这似乎已经形成了一个规律:
level10:
t_link / t_history / t_sort
↓
隐藏字段
↓
通过 URL 参数控制
level11:
t_ref
↓
ref 很像 Referer
↓
实际来自 Referer 请求头
level12:
t_ua
↓
ua 是 User-Agent 的常见缩写
↓
实际来自 User-Agent 请求头那这里的第十二关,我们通过Crtl+U查看源码可发现一个名叫t_cook的参数
这是一个强线索,但是需要实验来进行验证
我们首先先使用第十关(level10)的思想,我们直接在浏览器的地址栏输入以下内容
http://192.168.153.141:8088/level13.php?t_cook=aaa但是再次查看源码后发现并没有传入参数,那么也就说明它大概率不是从URL GET参数来的
然后再测试 Cookie,这时用 Burp 拦截请求,在请求头里加:
Cookie: user=aaa
经过上述的步骤,这里也是成功验证了t_cook就是cookie
在已经确认Cookie: user=aaa能控制t_cook,再把 Cookie 改成:
Cookie: user=" type="text" onclick="alert(1)在点击我们构造的 Payload 点击事件后,即可成功触发XSS弹窗

第十四关(level14)
在这一关,我们发现这里的网页源码中访问了一个网站(但截至当前时间节点,该网站似乎已经关站了,所以无奈之下,只能跳过)

结合早期前辈的访问记录,这里我简单说一下这一关的原理和步骤

你可以把它理解成:图片表面是一张正常图片,但图片的“属性信息”里可以写文字;如果网站把这些文字直接输出到网页,就可能触发 XSS

也就是说,不是你在地址栏里写 payload,而是把 payload 写进图片的属性里(如上图所示)
这些信息就叫图片元数据,常见类型包括:
EXIF
IPTC
XMP比如图片属性里可能有:
标题:xxx
作者:xxx
备注:xxx
版权:xxx比如图片属性里可能有:
标题:xxx
作者:xxx
备注:xxx
版权:xxx如果我们把这些字段改成:
"><img src=x onerror=alert(1)>下图红色箭头指向的这些破图标很关键,它说明:
EXIF 字段里的 <img src=x ...> 被浏览器当成了真正的 img 标签解析
但现在出现了破图标,说明浏览器真的创建了<img>元素
然后因为 src=x 加载失败,就会触发:
onerror=alert(1)也就是下图的弹窗

以上图片出处为:先知社区
那这个漏洞是怎么产生的?
假设有一个图片信息查看网站,它会读取图片元数据,然后显示到网页上
正常情况它应该这样显示:
<td>"><img src=x onerror=alert(1)></td>这样浏览器只会把它当成普通文字
但如果网站偷懒,直接输出:
<td>"><img src=x onerror=alert(1)></td>浏览器就会真的解析 <img> 标签
因为:
<img src=x onerror=alert(1)>里面的 src=x 是一个不存在的图片地址,所以图片加载失败,触发 onerror,然后执行:
alert(1)这就是 EXIF XSS 的本质
第十五关(level15)
查看源码后发现,这儿有个陌生的东西ng-include
这里需要补充一个概念:ng-include 是什么?
可以先简单理解成:
ng-include = 把另一个页面的内容加载进当前页面比如 Angular 里可以写:
<span ng-include="'test.html'"></span>意思是:把test.html的内容加载到这里
可是现在页面没有输入框,也没有明显参数,所以不要急着打 payload
先看源码:

它是不是在等我们通过URL参数给ng-include传一个页面地址?
常见参数名可能是src
所以我们试试在后面拼接:
src='level1.php'
#注意这里有单引号,因为 ng-include 后面接的是 Angular 表达式,不是普通字符串
#这里的 'level1.php' 就是一个字符串,Angular 知道你要加载这个页面发现居然下面拼接出来了第一关的页面

然后再构造 XSS
既然它可以加载别的页面,那我们可以加载前面已经知道有 XSS 的页面
比如 level1的漏洞是:
level1.php?name=alert弹窗语句但是我们并不能直接把第一关的<script>alert(1)</script>拿来直接使用
因为 level15 是通过 AngularJS 的 ng-include 把 level1 的返回内容“动态插入”到当前页面里
动态插入 HTML 时,浏览器/Angular 往往不会执行里面的 <script> 标签
结合之前的第十四关(level14),我们可以使用
<img src=x onerror=alert(1)>所以第 15 关可以使用以下的代码:
level15.php?src='level1.php?name=<img src=x onerror=alert(1)>'输入后,即可成功触发弹窗:

需要注意,这里不能和文件包含漏洞弄混淆,区别如下:
从思路上,它像文件包含;
从漏洞类型上,它更准确地叫 AngularJS ng-include 导致的前端 XSS第十六关(level16)
首先还是依旧Ctrl+U查看源码

这个位置其实很像我们在第 1 关测试的:
<h2>test</h2>所以第一反应应该是:
<script>alert(1)</script>但是第 16 关肯定不会这么简单。它会过滤一些东西
再看源码,发现它变成了奇怪的东西,我们的<script>标签变成了
既然<script>标签不行,那么我们尝试前一关的:
<img src=x onerror=alert(1)>
但是输入之后,依旧是失败的
查看源码后发现:第 16 关没有把 <、>、onerror 删除,但它把普通空格替换成了
目前可以推断:
< 没有被过滤
> 没有被过滤
img 没有被过滤
src 没有被过滤
onerror 没有被过滤
alert 没有被过滤
但是普通空格被替换成了 空格可以用回车来代替绕过,回车的url编码是%0a,<>的url编码是,再配合上之前的<img>标签
<img
src=x
onerror=alert(1)>将此内容进行URL转码

最终可得以下URL编码(有点长):
%3c%69%6d%67%0a%73%72%63%3d%78%0a%6f%6e%65%72%72%6f%72%3d%61%6c%65%72%74%28%31%29%3e输入后,即为成功:

第十七关(level17)
打开后发现我们这个浏览器貌似不支持Flash插件,所以我们需要安装一个Flash补丁(纯净版本的,无广告,BTW,安装好后,需要依赖Flash插件的4399,7K7K等网页小游戏也能游玩了)
下载链接:点我前往下载 密码:gsh3
(1)下载好了之后,按照以下步骤完成安装

(2)下图三个勾保持全选,并点击NEXT

(3)下图的含义我都标注出来了(我不希望创建快捷方式,所以我值选择第一个)
第一个你也可以选择不打勾(这个独立安装指的是能直接打开本地SWF小游戏用的)

(4)这里直接按照下图,选择NEXT

(5)再然后点击INSTALL,等待进度条跑完即可

(6)最后安装好后,点击QUIT退出

配置Edge浏览器(这里用Edge示例,其他浏览器也一样)
(1)打开浏览器的“设置”,搜索“Internet Explorer”选项

按照上图进行操作
(2)按照下图的图片文字来操作

(3)刷新原网页,即可出现

查看源码后,不难发现

<embed> 你可以先把它理解成:
<embed src="要加载的资源地址">它是 HTML 里用来嵌入外部资源的标签。以前常见用途是嵌入 Flash、PDF、音频、视频之类的内容
<embed src="xsf01.swf" width="100%" height="100%">意思就是:
在当前网页里嵌入 xsf01.swf 这个 Flash 文件a=b 有什么用?它的作用是让你发现:
arg01 和 arg02 被拼进了 embed 的 src 属性里我们当前的 URL 是:
level17.php?arg01=a&arg02=b源码里变成:
<embed src=xsf01.swf?a=b width=100% heigth=100%>这说明:
arg01 的值 a
arg02 的值 b
被拼成了:
xsf01.swf?a=b也就是:
xsf01.swf?arg01的值=arg02的值所以 a=b 本身不是漏洞,它是一个线索
因为我们知道了:
arg02 的值会出现在 HTML 标签的 src 属性里而且源码中 src 没有引号:
<embed src=xsf01.swf?a=b width=100% heigth=100%>如果属性没有引号,浏览器遇到空格就会认为这个属性结束了
比如正常是:
src=xsf01.swf?a=b如果我们让 arg02 变成:
b onmouseover=alert(1)那么页面就会变成:
<embed src=xsf01.swf?a=b onmouseover=alert(1) width=100% heigth=100%>这时候:
src=xsf01.swf?a=b就结束了
后面的:
onmouseover=alert(1)变成了一个新的事件属性。
鼠标移动到这个嵌入区域上时,就会执行 alert(1)
所以最终的payload如下:
http://192.168.153.141:8088/level17.php?arg01=a&arg02=b%20onmouseover=alert(1)其中:%20= 空格
一句话总结:
embed 是嵌入外部资源的标签;a=b 的作用是暴露 arg01 和 arg02 被拼进了 embed 的 src 属性里。真正的漏洞点是 src 属性没有加引号,导致可以用空格逃出属性并添加事件
最后由于浏览器的内置保护,导致不能执行恶意代码,但是最终是能成功的

可能作者也考虑到了这一点,所以我们点击前往下一关的按钮即可
剩余内容正在更新中
Ciallo~(∠・ω< )⌒☆

