【T00ls十年庆】Spring视图注入

2018-09-18 22:01:00 17 18197 6

首先恭喜T00LS十周年 :)

背景

在一次对某客户的检测过程中,发现了一个由Spring应用开发程序员错误书写代码导致的安全问题。过后对该漏洞进行相应的研究,了解此漏洞背景知识是要先了解基础的MVC开发模式。

具体相关依赖知识在此就不进行赘述。

1. Spring MVC 的报错页面

在对客户网站的参数进行常规异常输入时,遇见了一个非常让人困惑的HTTP 500错误页面。 为什么一个参数能导致/Spring提供View的功能错误?

2. Debug回溯

以下为Exception回溯内容:

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()函数源代码:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
  // Determine locale for request and apply it to the response.
  Locale locale =
      (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
  response.setLocale(locale);

  View view;
  String viewName = mv.getViewName();
  if (viewName != null) {
    // We need to resolve the view name.
    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 {
    // No need to lookup: the ModelAndView object contains the actual View object.
    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() + "'");
    }
  }

  // Delegate to the View object for rendering.
  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()函数原型:

protected View createView(String viewName, Locale locale) throws Exception {
  // If this resolver is not supposed to handle the given view,
  // return null to pass on to the next resolver in the chain.
  if (!canHandle(viewName, locale)) {
    return null;
  }

  // Check for special "redirect:" prefix.
  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);
  }

  // Check for special "forward:" prefix.
  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);
  }

  // Else fall back to superclass implementation: calling loadView.
  return super.createView(viewName, locale);
}

可以看到Spring对两种前缀(forward:与redirect)进行了特殊处理。


根据视图名到指定的位置获取对应的模板文件

根据视图名跳转


在处理forward:时会再调用一次InternalResourceView,而InternalResourceView是Spring中用来加载Jar包中内部资源用的,所以可以用来做Jar包内的任意文件读取。

不过以上InternalResourceView受配置影响:

例:

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
   <property name="prefix" value="/WEB-INF/"/>
</bean>

如果此时调用InternalResourceView实际上会在前面加上前缀/WEB-INF/,所以在配置suffix的情况下,可能就不能读取任意Jar包内的文件了。

3. 危害

所以如上所述的这些东西到底能造成什么危害呢?

初步想法为:

1. 权限认证Bypass
2. 文件读取
3. 重定向
4. HTTP Header Injection

写了一个本地测试有漏洞的代码Demo验证以上想法。

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&amp;password=111&amp;view=forward:/database.properties

(3) HTTP Header Injection [失败]

由于Spring对传入程序的CRLF进行了处理。将其转化为了空格,所以该漏洞没有成功实现。

4. 限制

该漏洞有以下两点限制

  1. 无法读取Jar外的文件
  2. 如果加了suffix可能,无法读取想要的文件

对于第1点限制,有一个未经验证的想法。是否能组合CVE-2018-1271在Windows环境下对目录外的文件进行读取呢?但是由于手头边Windows环境还没有搭建好,可能需要过段时间才能进行测试:p

/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/

TCV:1

关于作者

LonelyRain18篇文章171篇回复

多多交流

评论17次

要评论?请先  登录  或  注册