背景

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

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 {
// 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()函数原型:

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 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)进行了特殊处理。


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:是能够直接绕过权限认证装饰器,对敏感功能进行直接访问。

Spring权限认证Bypass

(2) 文件读取 [成功]

测试URL:
http://127.0.0.1:8080/login?username=admin&password=111&view=forward:/database.properties

文件读取

(3) HTTP Header Injection [失败]

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

4. 限制

该漏洞有以下两点限制

    1. 无法读取Jar外的文件
    1. 如果加了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/

Comments

⬆︎TOP