【T00ls十年庆】Spring视图注入
首先恭喜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&password=111&view=forward:/database.properties
(3) HTTP Header Injection [失败]
由于Spring对传入程序的CRLF进行了处理。将其转化为了空格,所以该漏洞没有成功实现。
4. 限制
该漏洞有以下两点限制
- 无法读取Jar外的文件
- 如果加了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
评论17次
谢谢分享,对javaweb源码一无所知,继续学xi
学xi了,刚好最近再看一下个一个序列化。表哥这篇文章又加深了spring mvc执行流程
那这个意思是只要是view这个参数,应该都可以利用了
完全看不懂,好厉害。
感谢楼主讲解,java安全这块刚开始学,框架审计反序列化什么的统统都走起
好文,楼主讲解的很清晰
谢谢分享,姿势很猥琐
文章不错,可惜利用场景并不多
谢谢分享,对javaweb源码一无所知,继续学xi。。
spring用得还是很多的。好文章,需要好好研究下,多谢分享
有没有版本限制?
不是上古版本应该都行
号复炸的样子~~自己搭个环境研究下
版本是否有限制 我也想知道
版本是否有限制 应该说列一下
可以,对spring的安全研究 可以有其他的维度
挺不错的一个洞
有没有版本限制?