文章目录
xss-labs 是一套基于 PHP 的 XSS 靶场,以闯关的形式让我们体验各种 XSS 漏洞利用方式。虽然已经是很多年前的了,但其中基本的 XSS 漏洞依然很有意义。
一共有 20 关,每一关的目标都是实现弹窗(alert(1)
),如果成功就会自动进入下一关。
环境搭建
克隆这个代码仓库的代码,其中是 xss-labs 的 PHP 源文件。只要放在 PHP 环境下即可,推荐使用 XAMPP 或者 Docker。
部署完成后,访问 index.php,就可以看到入口了:
Warm Up
前两关是对于 XSS 基本原理的应用,没有任何的防御。
Level 1:文本解析为 HTML
URL 为 level1.php?name=test
时,传入的参数是 test,显示的是「欢迎用户 test」。显然,这个页面会将我们传入的名字显示出来。
传入参数 <h1>test</h1>
作为 name
,发现确实显示为了一级标题。看来传入的 name 直接被作为 HTML 显示了。于是尝试传入 <script>alert(1);</script>
作为 name
,成功弹窗。
?name=<script>alert(1);</script>
Level 2:input 标签 value 注入
这关多了一个文本框。如果依然尝试传入 <script>alert(1);</script>
,会发现在 h2 元素中符号被转义了(查看网页源代码能看到),比如 <
被转义成了 <
等,所以不会被解析成 HTML。
然而,可以看到后面的文本框 input
元素,其 value 值并没有被转义。
<h2 align=center>没有找到和<script>alert(1)</script>相关的结果.</h2><center>
<input name=keyword value="<script>alert(1)</script>">
既然如此,我们可以闭合 value 的内容的后引号,然后闭合这个 input 标签,接下来再加入我们想要注入的 <script>alert(1);</script>
。只需要在其之前加上 ">
即可。
?keyword="><script>alert(1);</script>
字符过滤绕过
Level 3:htmlspecialchars()
的弱点
本题要求 PHP 版本低于 8.1.0。在 8.1.0 版本中此漏洞已经修复。
HTML 标准规定了一个叫做实体(Entity)的概念。在 HTML 中,<h1>
会被解释为标题标签,那我们如何以文本形式显示出 <h1>
这四个字符呢?答案是使用字符实体,将 <
用 <
替代,将 >
用 >
替代。在 HTML 中,如果我们写 <h1>
,就会显示为 <h1>
而不会被解析。
在 PHP 中,后端需要渲染出一个 HTML 给前端,对传来的字符串就要进行这样的处理,将 <
这样的字符转换为 <
这样的实体。Level 1 就是没有进行这种处理的下场。
一个通常的做法就是:使用 PHP 自带的 htmlspecialchars()
函数处理字符串,进行字符转义。具体用法可以参考官方文档。
在 8.1.0 及以上的 PHP 版本中,这个函数默认会转义 <
、>
、&
、'
、"
这五个字符,基本可以防范这里的 XSS 攻击。
但是,8.1.0 以下版本的 PHP 默认只会转义 <
、>
、&
、"
这四个字符,不会转义单引号 '
。这就给这个函数带来了巨大的安全隐患。
回到这题,我们仍然尝试 ?keyword=<script>alert(1)</script>
,打开页面源码,观察这个 input 输入框:
<input name=keyword value='<script>alert(1)</script>'>
没错,我们的字符被转义了。
然而,可以发现这里 value 的值用的是单引号。既然单引号不会被转义,我们可以闭合 value 这个字符串。
但是,<>
都会被转义,似乎不能闭合这个标签。有什么办法能够不用 <script>
标签来注入 JavaScript 代码呢?答案是使用触发器,比如 onfocus
或者 onmouseover
。
<input name=keyword value='' onmouseover=alert(1) ''>
所以,本题的 payload 是:
?keyword=' onmouseover=javascript:alert(1) '
Level 4:没有过滤双引号
还是先试试 ?keyword=<script>alert(1)</script>
,发现 HTML 源码里是这样的:
<input name=keyword value="";scriptalert(1);/script">
似乎代码的编写者使用了一个自定义的字符过滤函数,过滤了 <>
(替换为空),但是却没有过滤 "
。使用 onmouseover
即可。
?keyword=" onmouseover=alert(1) "
Level 5:href 的危险
在这一关,当我们尝试之后发现,传入的字符串里 script
会被替换为 scr_ipt
,on
会被替换为 o_n
。也就是说,之前两种运行 js 脚本的办法这关里都无法使用。还有没有其他办法呢?
答案是肯定的。可以利用 JavaScript 的 URI。看看下面这个链接:
<a href=javascript:alert(1)>hack</a>
不同于许多人的印象,<a>
标签的 href 值并非只能是 URL,而是 URI。URI(Uniform Resource Identifier)可以视为 URL 的超集,其不仅包含以 protocol://address
开头的 URL,也包含 protocol:content
这种形式的地址(参见 RFC 3986)。我们经常用到的有:javascript:
后接 js 代码,这样的 URI 打开后会运行一段 js 代码;mailto:
后接一个邮箱,这样的 URI 打开后会开启系统中的邮箱类应用程序,创建发送给目标地址的一封邮件。
所以,这题我们也可以利用这个运行 js 代码。
?keyword="> <a href=javascript:alert(1)>hack</a>
Level 6:很蠢的字符过滤
还是使用上一题的伎俩,会发现 href
被替换为 hr_ef
。
事实上,这题使用了很蠢的字符过滤:区分大小写地搜索敏感词。而 HTML 并不区分大小写。所以将 href 写为 hREF 之类的即可。
?keyword="> <a hREF=javascript:alert(1)>hack</a>
Level 7:字符串替换,但只做一次
尝试过后,会发现这关里 script
这个字符串会被过滤,即替换为空字符串。
然而,将所有搜索到的这个字符串替换为空后,对于拼接得到的新字符串,没有再一次进行检测。这是一个非常常见的安全问题。我们只要「双写」关键词即可。例如,scriscriptpt
被扫描替换一次后,会变成 script
。
?keyword="> <scriscriptpt>alert(1);</scriscriptpt>
Level 8:URI 中的实体
从这关开始,input 的 value 也严格地使用了 htmlspecialchars()
函数进行转义,从而无法注入。好在提供了另一个注入点:a 标签的 href 属性。
尝试之前的 javascript:alert(1)
,发现 javascript
被替换成了 javascr_ipt
:
<center><BR><a href="javascr_ipt:alert(1)">友情链接</a></center>
这里要用到 href 属性的一个特性:href 传入的 URI 中,也可以使用 HTML 字符实体。在打开链接时,字符实体也会被转换为对应的字符。
HTML 实体有两种写法,第一种是之前提到的 &entity_name;
形式,比如 $lt;
表示小于号;第二种是 &#entity_number;
形式,其中 entity_number 是字符的实体编号,比如 <
也能表示小于号。使用第二种方式,任何字符(包括 ASCII 字符)都有其实体表示。可以使用这个工具来转换。
对于这题,为了绕过 javascript
这个词的屏蔽,我们将 i 写为其字符实体 i
即可。
javascript:alert(1)
Level 9:单纯的「必须包含」
如果使用上一题的 payload,会提示链接不合法。如果使用一个合法的链接,比如 http://test
,则可以成功添加。
仅过尝试可以发现,后端只检测传入的链接字符串是否包含 http://
,并不要求其在开头。非常弱智的检测方式(而且甚至没有考虑 https)。那么我们只要在注入的 js 代码里加上注释,里面写上 http://
就行了。
javascript:alert(1)//http://
字段注入
下面这四关和 level 2 差不多,区别只在于能够注入的字段不同。Level 2 中我们在 input 标签的 value 中注入,而下面几关分别在不同的注入点进行注入。
Level 10:隐藏表单字段注入
这关乍一看似乎并没有可用的注入点。其实查看源码可以看到这个隐藏的表单:
<h2 align=center>没有找到和well done!相关的结果.</h2><center>
<form id=search>
<input name="t_link" value="" type="hidden">
<input name="t_history" value="" type="hidden">
<input name="t_sort" value="" type="hidden">
</form>
我们尝试请求的时候给出 t_link
、t_history
和 t_sort
几个参数,发现 t_sort
的 value 字段对应其参数。举例来说,尝试这个 payload:
?keyword=test?t_link=tlink&t_history=thist&t_sort=tsort
看到 HTML 的对应部分变成了这样:
<h2 align=center>没有找到和test?t_link=tlink相关的结果.</h2><center>
<form id=search>
<input name="t_link" value="" type="hidden">
<input name="t_history" value="" type="hidden">
<input name="t_sort" value="tsort" type="hidden">
</form>
看来可以用 t_sort
的 value 这个字段来注入。
仅过尝试,对这个参数的处理上过滤了 <>
,没有过滤双引号。所以我们可以直接闭合双引号,添加 onmouseover
属性来达到我们的目的。
但是依然有一个问题:这个元素是 hidden 的,意味着不会显示出来,不管是 focus 还是 mouseover 都不能触发。隐藏的状态是由最后的 type="hidden"
设置的,那我们在其之前加入一个空的 type
就可以覆盖掉后面的设置。也就是说,注入之后的 input 会变成这样:
<input name="t_sort" value="" onmouseover=javascript:alert(1) type "" type="hidden">
Payload 如下;
?keyword=test&t_sort=" onmouseover=javascript:alert(1) type "
Level 11:Referer 注入
如果是完成了前面一关跳转过来的,可以看到源码里的 t_ref
字段是跳转来的 URL(也就是请求的 Referer):
<form id=search>
...
<input name="t_ref" value="http://localhost/xss-labs/level10.php?keyword=test&t_sort=%22%20onmouseover=javascript:alert(1)%20type%20%22" type="hidden">
</form>
只要存在可以自定义的字段,我们就可以注入。一种方法是创建一个文件名为 " onmouseover=javascript:alert(1) type ".html
的 HTML 文件,在其中重定向到 level 11 的页面。
更简洁的做法是直接修改请求的 Referer 字段。
关于 Referer,推荐阅读:HTTP Referer 教程 - 阮一峰的网络日志
。
Level 12:UA 注入
和前面的类似,查看源码可以看到有一个字段 value 被设置成了浏览器 UA。
浏览器 UA 是完全可以自定义的,随便下载一个自定义 UA 的浏览器插件,在里面将 UA 设置为 payload,然后直接访问 level 12 即可。
" onmouseover=javascript:alert(1) type "
Level 13:Cookie 注入
和 level 12 一样,有一个字段 value 设为了一个 Cookie。
直接打开浏览器 F12 工具,找到这个对应的名为 user
的 Cookie,设置为 payload,然后刷新网站即可。
" onmouseover=javascript:alert(1) type "
Level 14 嵌入了一个网站,这个网站已经打不开了。似乎是个查看图片 exif 信息的网站,猜测可能可以将 payload 写在 exif 信息里进行注入吧。
Level 16:空格过滤
这题似乎也是过滤,使用了综合的过滤规则。经过一番探究,script
和 /
都会被替换为
,每个空格也都会被换成
,这正是本题最麻烦之处。
HTML 中,有什么字符可以代替空格呢?答案是换行符(%0A
)。
?keyword=<img%0Asrc=1%0Aonerror="alert(1)">
Special
Level 15:Angular ng-include
在 URL 中指定的 src(默认是 1.gif)会被引用进来,使用的是 ng-include。这是一个 Angular 框架的功能,特点是,如果引入的是 HTML 文件,不会执行其中 <script>
标签内的代码。(有一点安全意识,但不多……)
一种方法是写一个能够弹窗的简单 HTML,然后 include 进来,比如:
<html>
<h1>Hacked</h1>
<img src=1 onerror="alert(1)"></img>
</html>
假设放在网站根目录,payload 就是:
?src="/alert.html"
Level 17 到 Level 20 都是 Flash 的利用,现在 Flash 已经退出历史舞台了,所以这几关已经失去意义。
总结
XSS 攻击历久弥新,目前 XSS 漏洞依然广泛存在。其核心在于找到注入点并通过 payload 让浏览器执行 js 代码。
注入点不仅可以来源于用户提供的文本(请求参数或后端存储的字段),也可以是 Referer、UA、Cookie 等。
执行 js 代码一般有四种方式,在使用时可以根据条件灵活选择:
- 通过
<script>
标签。 - 通过元素的 onmouseover 属性。
- 通过
<img>
的 onerror 属性,如<img src=1 onerror="alert(1)">
。 - 通过 URI,如
javascript:alert(1)
。
一般来说,使用 PHP 的 htmlspecialchars()
函数能够防御很大一部分 XSS(注意过滤单引号)。许多字符过滤上的漏洞都是没有使用 htmlspecialchars()
而使用自己编写的字符过滤函数引起。