【漏洞公告】某平台一个有意思的CSRF
7/May 2017
0x00 背景
某日在给某集团平台提交报告的时候,出于习惯就想试试这个平台的安全性怎么样。
总的来说,平台前面架设了WAF,一些常规的测试Payload都会被拦截,因此不适合强攻。其实随着安全越来越受重视,很多开发同学的安全意识也越来越高,常见的XSS和SQL注入的错误一般都会再犯。但是对于跨站请求伪造CSRF一般都没有太在意。主要原因是这类漏洞的危害不会像注入那样明显,但是威胁等级并不比它们低。
0x01 案例
前面唠叨了这么多,接下来我们就说说这个具体的例子:
问题接口
我们发现退出登录,这个平台采用的GET请求,同时没有其他防护策略,比如token,比如referer校验。
这个接口存在两个问题:
- 不规范,我们知道在HTTP语义中,修改用户的登录态写操作要用POST或者PUT,而GET操作主要用于获取资源信息等。至于开发人员为什么要这么做?肯定就是为了图方便。所有违背规范的操作,都是为了主管的便利性。
- 重要的接口没有进行二次校验。接口功能是正常的,就是清空session中的用户登录态,但是在清除过程中,并没有确认请求是否用户发送的。
假设这个接口地址是:http://security.lxxxxxxxxxxh.com/account/logout ,这样的接口很好找,因为只要有登录态的网站,一般在显眼的地方都会存在这样退出功能的接口。
注入点
现在问题接口找到了,但是我们需要怎样才能诱骗其他用户来触发呢?
第一个办法,也是最简单的,就是找到你的目标用户,然后把URL发给他,等他点击。但是这里有两个明显的问题:
- SRC平台不是社交网站,一般不提供用户之间交流的渠道,因此我们能否把URL发出去都是一个问题
- 一个不熟悉的人给你发的URL你会点击么?何况URL名字这么明显,有个logout。
经验告诉我们GET型的CSRF最好的载体是IMG,好处在于隐蔽和简单,整个网页可以显示其他内容,但是其中一个IMG却悄悄的发送一个退出登录的请求。比如:
http://www.huangjacky.com/h.html
<html>
<head>
<title>精彩刺激好看的爱情动作片</title>
</head>
<body>
显示一些吸引用户的内容来吸引和误导用户
<img style="display:none" src=http://security.lxxxxxxxxxxh.com/account/logout>
</body>
</html>
这样只要将上面的网址配上吸引人的标题就可以发送给其他用户的。
这样做有多个好处:
- 内容吸引用户,提高用户点击的概率
- 独立的域名,很多用户都知道跨域保护,不会认为这个网站能影响到我。
但是这样还是不能保证百分百用户点击,也不能保证如何将入口发送给用户的渠道问题。
突破点
这个时候,我们发现平台默认使用的头像是QQ或微信的,但是它提供头像修改功能。头像就是IMG,那么如果设置成功后,平台任意一个显示我们头像的地方都可以触发这个CSRF。安全应急响应平台的管理总要处理我们漏洞吧,漏洞详情页面或者排行榜都可能出现我们的头像,传播渠道也解决了。
那么我们上传图片,然后抓包提交修改那一下,修改POST里面headImg为http://security.lxxxxxxxxxxh.com/account/logout ,修改成功了,亲测成功,等着管理员查看漏洞时候的懊恼的表情吧。
0x02 原理讲解
CSRF全称叫做跨站请求伪造,漏洞的利用主要是诱骗用户在浏览A网站的同时向B网站发送一个特定请求,让B网站去执行相关的操作。 从上面的描述我们可以看出来主要有两点:
- 从一个网站向另一个网站发送了请求。
- 在用户不知情的情况下请求发送了。而不是用户再主动进行点击。
有些知识的朋友就会问从一个网站向另外一个网站发送请求不是跨域了么?其实这是你们把跨域知识点弄错了,跨域问题主要存在脚本操作中,比如AJAX请求等。但是对于浏览器的script,img,iframe等标签操作是没有作用。不然CDN怎么能运行?CDN通常来说都是一个独立的域名。当然在现代的浏览器里面,如果网页是HTTPS协议的时候,如果你去请求一个HTTP的资源,这个是会禁止的,因为整个系统的安全性取决于最短的那块木板,如果这个被放行,整个网页HTTPS的作用也无效了。HTML代码因为HTTPS不会被劫持,但是里面嵌入的图片和脚本资源因为是HTTP的,是会被劫持的。
这这种情况下我们推荐开发的同学在编码的时候使用相对协议:
<script src=//huangjacky.com/static/js/sec.js></script>
<img src=//huangjacky.com/test.png />
但是为什么我们会遭受到CSRF攻击?
会话保持功能
现在所有的浏览器,为了保持用户浏览时的体验,当用户任何操作的时候,都会带上对应的Cookie。 那么上面例子当中,
- 我们在请求http://www.huangjacky.com/h.html 的时候,这个时候浏览器会带上本地保持的www.huangjacky.com的Cookie将请求发送给www.huangjacky.com的服务器,这个很好理解。
- 浏览器渲染得到HTML代码,发现里面有一张图片,就会去请求http://security.lxxxxxxxxxxh.com/account/logout 注意这个时候虽然网页停留在www.huangjacky.com,但是因为请求发送给security.lxxxxxxxxxxh.com,因此浏览器带的是用户在security.lxxxxxxxxxxh.com下的Cookie,这个一定要理解清楚。
服务器身份认证
这里说的身份认证不是帐号密码体系,而是在帐号体系登录之后,服务器是怎么识别用户?其实很简单绝大多数Web程序都是采用Cookie,其中某个字段是SessionID,比如PHP默认的SESSIONID,JAVA容器的JSESSIONID等。在API类的程序中一般在HTTP请求头里面自定义一个字段,带上唯一身份标识,那是因为API的客户端也都是程序可以自定义请求头。但是浏览器的请求基本上都是使用Cookie,这个有一些历史的原因吧。
这里插入一个问题:**Cookie和Session的区别?**请认真理解
通过上面说的,服务器通过客户端请求中的Cookie拿到了对应的Session信息之后,会去判断Session中的登录态和身份针对接口请求是否有权限?如果有就进行操作了, 这里并没有进行恶意检测。
0x03 攻击手法
针对存在CSRF的GET请求,我们一般利用上面例子中的隐藏IMG标签的手法。
但是如果是一个POST的请求我们怎么办?
POST /blog/del?t=11111111 HTTP/1.1
Referer: http://www.test.com/blog/index
Host: www.test.com
Content-Type: application/form-data
blog_id=1
我们可以使用FORM表单,然后通过javascript脚本去提交表单,因为最后请求是表单触发的,不存在跨域的问题。
<html>
<head>
<title>精彩刺激好看的爱情动作片</title>
</head>
<body>
显示一些吸引用户的内容来吸引和误导用户
<form id="test" style="display:none" action="http://www.test.com/blog/del" method="POST" enctype="form-data">
<input name="blog_id" value="1" />
</form>
<script>
var form = document.getElementByID("test");
form.submit();
</script>
</body>
</html>
用户在浏览网页的时候,script脚本就会执行,去提交一个已经预埋好参数的表单,整个过程不需要用户参与和交互,他只需要看片。
0x04 修复手法
不正确的修复方法
将GET请求改成POST
上面利用手法第二点我们已经演示了,POST一样是可以利用的,同时也是没有什么难度的。
校验请求的Referer
正常来说每一个请求在发送前,浏览器都会带上当前页面的URL作为Referer。因此我们只需要校验这个Referer是不是自己网站就好了。但是这个在具体实施过程中却存在三个问题:
- Referer可能为空,比如用户在地址栏手打URL,然后敲回车,那么这个请求,浏览器是不会带上Referer的。如果我们放开空Referer,那么这就可能被黑客绕过。
- 校验Referer的方式不对。很多开发人员会使用contains来判断Referer是否合法,那么Referer: http://www.test.com.hj.cn/index完全就可能是一个合法请求
- 像上面例子那样,如果请求伪造是来自本站的图片,那么所有的Referer都是合法,因此这个校验完全就被绕过了。
正确修复方法
POST+Referer校验
当然这个方法我不推荐,因为需要注意的细节比较多。
- GET请求可以允许外部Referer,因为从百度搜索跳转过来的用户的Referer肯定是baidu.com,也允许空Referer,但是GET操作不能进行敏感操作,所有操作都根据规范转到其他方式上面。
- POST请求进行严格校验Referer,必须拦截空Referer的情况
Token校验
上面所有攻击手法当中,我们必须要事先知道接口中每一个参数的值,然后通过隐藏的方式进行预埋,等正常用户在浏览的时候触发。那么如果每一个用户在登录成功的时候,服务器都会随机生成一个字符串,存在Session里面,以后用户所有的请求到服务器来之前,服务器都会先校验一下字符串是否一致,然后才会接口操作。
<?php
$token = isset($_GET['token'])?$_GET['token']:'';
if($token != $_SESSION['token']){
die('WRONG token');
}
?>
前端怎么知道这个token呢?我们服务器在生成HTML的时候,可以将它设置成javascript的一个变量,所有的请求在发送前都需要读取这个变量并将它加入到URL中。
推荐。
0x05 检测手法
- 观察请求中所有的参数,看是否有一个参数的格式是随机字符串。如果没有,进入下一步;如果有,尝试修改1位或者删除这个参数,然后发送请求。对比得到的响应包,如果不一致,那么基本就是有token防护,这个接口已经没有戏了,放弃CSRF吧。如果一致,进入下一步。
- 尝试修改Referer头或者修改Referer的值,分别发送请求,判断返回的包是否和正常请求一样,如果一样,那么就是存在CSRF;如果不一样,那么看这个接口的请求方法是不是GET,网站是否存在插入图片的地方,这个就是靠缘分了。
上面说到了,允许空Referer不安全的,因为在正常的浏览器情况,如果请求发送出来后存在跨域的情况,那么Referer就是空的。通常在CSRF中可以利用的跨域有:
- 从HTTP域名发送请求到HTTPS域名下
- 如果域名协议都一样,那么我们可以在A域名下面由javascript生成整个form表单并提交,而不是由HTML预埋,这个时候form表单的协议是javascript: 伪协议,因此请求也是跨协议的,那么Referer也是空。
好了介绍就这么多,主要是入门扫盲的文章。上面检测手法,已经集成到本人的CSRFChecker工具中了。