背景
在一次对某客户的检测过程中,发现了一个由Spring应用开发程序员错误书写代码导致的安全问题。过后对该漏洞进行相应的研究,了解此漏洞背景知识是要先了解基础的MVC开发模式。
具体相关依赖知识在此就不进行赘述。
1. Spring MVC 的报错页面
在对客户网站的参数进行常规异常输入时,遇见了一个非常让人困惑的HTTP 500错误页面。
为什么一个参数能导致/Spring提供View的功能错误?
2. Debug回溯
以下为Exception回溯内容:
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
| HTTP Status 500 - Could not resolve view with name 'test' in servlet with name 'action' javax.servlet.ServletException: Could not resolve view with name 'test' in servlet with name 'action' org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1190) org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:992) org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:939) org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856) org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:920) org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:816) javax.servlet.http.HttpServlet.service(HttpServlet.java:624) org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:801) javax.servlet.http.HttpServlet.service(HttpServlet.java:731) filter.CompanyFilter.doFilter(CompanyFilter.java:50) filter.LoginFilter.doFilter(LoginFilter.java:89) filter.HttpRequestFilter.doFilter(HttpRequestFilter.java:24) com.eall.hr.web.SetCharacterEncodingFilter.doFilter(SetCharacterEncodingFilter.java:73) org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449) org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365) org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90) org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83) org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383) org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362) org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:259)
|
于是下载了Spring的源代码进行分析,Could not resolve view with name
此串字符在Spring中只有3至4次出现的地方,所以较好定位报错地点。
而且报错页面没有被关闭,所以通过上图也能直接找到引发错误的原因为以下代码行。
org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1190)
render()函数源代码:
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
| protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; String viewName = mv.getViewName(); if (viewName != null) { view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else { view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } } if (logger.isTraceEnabled()) { logger.trace("Rendering view [" + view + "] "); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "]", ex); } throw ex; } }
|
最终调用到的就是render函数。
先需要了解一下Spring MVC的整个调用流程。
doGet() -> processRequest() -> doService() -> doDispatch() -> processDispatcheRequestResult() -> render()
在render()函数中,最终会追踪到函数createView()。
createView()函数原型:
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
| protected View createView(String viewName, Locale locale) throws Exception { if (!canHandle(viewName, locale)) { return null; } if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); String[] hosts = getRedirectHosts(); if (hosts != null) { view.setHosts(hosts); } return applyLifecycleMethods(REDIRECT_URL_PREFIX, view); } if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); InternalResourceView view = new InternalResourceView(forwardUrl); return applyLifecycleMethods(FORWARD_URL_PREFIX, view); } return super.createView(viewName, locale); }
|
可以看到Spring对两种前缀(forward:与redirect)进行了特殊处理。
InternalResourceView:
根据视图名到指定的位置获取对应的模板文件
RedirectView:
根据视图名跳转
在处理forward:
时会再调用一次InternalResourceView,而InternalResourceView是Spring中用来加载Jar包中内部资源用的,所以可以用来做Jar包内的任意文件读取。
不过以上InternalResourceView受配置影响:
例:
1 2 3
| <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/"/> </bean>
|
如果此时调用InternalResourceView实际上会在前面加上前缀/WEB-INF/
,所以在配置suffix的情况下,可能就不能读取任意Jar包内的文件了。
3. 危害
所以如上所述的这些东西到底能造成什么危害呢?
初步想法为:
1 2 3 4
| 1. 权限认证Bypass 2. 文件读取 3. 重定向 4. HTTP Header Injection
|
写了一个本地测试有漏洞的代码Demo验证以上想法。
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
| package chaitin; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.servlet.ModelAndView; import java.util.Map; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Controller public class UserLoginController { @GetMapping("/login") public ModelAndView login(HttpServletResponse response, @RequestParam(name = "username", required = true, defaultValue = "admin") String username, @RequestParam(name = "password", required = true, defaultValue = "******") String password, @RequestParam(name = "view", required = true, defaultValue = "UsersLogin") String view, Model user) { user.addAttribute("username", username); user.addAttribute("password", password); System.out.println(Class.class.getClass().getResource("/").getPath()); if (username.equals("admin") && password.equals("123123")) { response.addCookie(new Cookie("AdminStatus", "true")); } return new ModelAndView(view); } }
|
(1) 权限认证Bypass [成功]
假设采用了装饰器(Decorator)来进行敏感功能的统一权限认证,直接使用view的forward:
是能够直接绕过权限认证装饰器,对敏感功能进行直接访问。
(2) 文件读取 [成功]
测试URL:
http://127.0.0.1:8080/login?username=admin&password=111&view=forward:/database.properties
由于Spring对传入程序的CRLF进行了处理。将其转化为了空格,所以该漏洞没有成功实现。
4. 限制
该漏洞有以下两点限制
- 无法读取Jar外的文件
- 如果加了suffix可能,无法读取想要的文件
对于第1点限制,有一个未经验证的想法。是否能组合CVE-2018-1271在Windows环境下对目录外的文件进行读取呢?但是由于手头边Windows环境还没有搭建好,可能需要过段时间才能进行测试:p
1
| /resources/%5c%5c..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/windows/win.ini
|
5. Exploit!
References
https://o2platform.files.wordpress.com/2011/07/ounce_springframework_vulnerabilities.pdf
https://danielmiessler.com/study/mvc/