Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
}
catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type inannotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));}}}}
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null);
r.exec("calc.exe");
传参的代码改成这样
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 String[] {"calc.exe" }),};
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);
}
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}
半成品Common-Collections 1
前面做了那么多铺垫,就是为了cc链,本来想直接搞这个的,但是好多师傅建议把基础搞得差不多了再来啃会好点,总之现在就正式开始cc链的学习了
前言
在学习cc链之前需要先了解几个接口,因为在后面会涉及到
Transformer
Transformer是一个接口,它里面只有一个待实现的方法,它传入的参数也就是一个对象
TransformedMap
TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调
在这个类中封装了一个
decorate
方法,它是用来修饰Java中的标准数据结构Map,当向被修饰过的Map
中添加新元素时,就可以执行一个回调,其中,keyTransformer
是处理新元素的Key的回调,valueTransformer
是处理新元素的value
的回调。我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现Transformer
接⼝的类,也就是说keyTransformer
和valueTransformer
这两个类都应该有Transformer
接口,当对innerMap添加元素是会调用这两个类的transform
方法ConstantTransformer
ConstantTransformer
是实现了Transformer
接⼝的⼀个类,它的过程就是在他执行构造函数的时候传⼊⼀个对象,并在调用transform
⽅法时将这个对象再返回:所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。
InvokerTransformer
InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序列化能执⾏任意代码的关键。
在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:
再来看看他的
transform
方法大概意思就是会调用
input
对象的iMethodName
方法ChainedTransformer
ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
代码很简单,当传入的参数是数组的时候,会把数组内容放到
iTransformers
数组里面,这里没有给参数类型,是因为之前已经定义过了当调用他的transform方法并传入object对象的时候,回依次把返回的对象再放入循环里面,直到结束后返回这个对象。
简单梳理
了解这几个重要方法后,来简单梳理一下
首先我们需要一个数组,这个数组中包含两个对象,一个是
ConstantTransformer
对象,用来返回Runtime
对象;另一个是InvokerTransformer
对象,用来执行Runtime
对象中的exec
方法;然后将这个数组用ChainedTransformer
对象封装起来,让它构成一个回调;最后将这个ChainedTransformer
对象用TransformedMap.decorate
包装起来,包装成一个Map
,当我们向这个Map
中添加新元素时,就会调用ChainedTransformer
对象中的transform
方法,然后调用ConstantTransformer
对象中的transform
方法,最后调用InvokerTransformer
对象中的transform
方法,从而成功执行exec
方法理解demo
来看一看p神给的一段demo
来理解一下:首先实例化了一个
Transformer
类型的数组,参数名为transformers
,里面有两个参数,也是两个实例化后的对象,第⼀个是ConstantTransformer
,直接返回当前环境的Runtime
对象;第⼆个是InvokerTransformer
,执⾏Runtime
对象的exec
⽅法,参数是/System/Applications/Calculator.app/Contents/MacOS/Calculator
。然后把transformers
放到了ChainedTransformer
里面。具体实现:当调用了
ChainedTransformer
的transform
方法后,第一个传入的应该是实例化后的ConstantTransformer
对象,调用他的transform
方法返回了一个Runtime
类的实例,然后把Runtime
这个对象作为参数放进InvokerTransformer
方法里面的transform
方法中,接着就回执行Runtime
里面的exec
方法,参数也有。也就达到了命令执行的目的现在就只有一个问题了,如何去触发
ChainedTransformer
的transform
方法。这个问题容易联想到最开始我们提到的TransformedMap.decorate
。HashMap
解释一下这里为什么要用HashMap这个类,先看下Hashmap的定义
这里主要是因为
HashMap
可以添加新的元素,并且可以和TransformedMap.decorate
绑定使用最终demo
成品Common-Collections 1
对于上面一种构造cc链的方法,没有用到反射的知识,触发它也很受限制,只能我们手动去触发,但遇到真正需要它的环境这样并不现实,所以才需要构造一个完整的自动触发的成品cc链。
如何实现
我们前面说过,触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素。在demo中,我们可以手工执行
outerMap.put("test", "xxxx");
来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject
逻辑里有类似的写入操作。AnnotationInvocationHandler
这个类就是
sun.reflect.annotation.AnnotationInvocationHandler
,我们查看它的readObject
方法(这是8u71以前的代码,8u71以后做了一些修改,这个后面再说):
核心逻辑是这
Map.Entry<String, Object> memberValue : memberValues.entrySet() 和 memberValue.setValue(...)
memberValues
就是反序列化后得到的Map
,也是经过了TransformedMap
修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue
设置值的时候就会触发TransformedMap
里注册的Transform
,进而执行我们为其精心设计的任意代码。所以,我们构造
POC
的时候,就需要创建一个AnnotationInvocationHandler
对象,并将前面构造的HashMap设置进来:简单介绍一下,大概是因为这个
AnnotationInvocationHandler
内部类比较特殊,不能用new
,只能用反射的方法来获取,然后再利用他的构造方法获取对象就可以了AnnotationInvocationHandler
类的构造函数有两个参数,第一个参数是Annotation
类的子类,第二个参数是之前构造的outerMap
,简单了解下这里为什么要用Retention.class
:是由于
AnnotationInvocationHandler
类中的readObject
方法,这里因为要进入到if语句中,要让memberType
不为空,所以说就需要满足两个条件,这里涉及到Java
注释相关的技术,现在还不太了解,先这样用吧第一个条件是第一个参数必须是
Annotation
的子类,且其中必须含有至少一个方法第二个条件是被
TransformedMap.decorate
修饰的Map
中必须有一个键名为第一个参数中含有的方法名。而
Retention
正好有一个方法名为value
,所以可以用Retention.class
当第一个参数而第二个条件可以直接通过
HashMap
的put
方法来向Map
中添加一个key
是value
的元素:所有条件满足后,接着可以编写demo了:
这里报错了,报错信息大概猜得到是
java.lang.Runtime
的问题,其实原因也很简单,并不是所有的类都是实现了java.io.Serializable
接口的,也就是没有实现这个接口的类肯定是不允许序列化的,当传入ConstantTransformer
,并且参数是Runtime
类时,序列化的时候肯定会报错。这里
Runtime
对象的实现成了问题,那只能换成Runtime.class
,因为class
类是实现了Serializable
接口的,后面再利用反射,获取getRuntime
方法,然后利用这个方法创建对象就好了。传参的代码改成这样
其实和
demo
最大的区别就是将Runtime.getRuntime()
换成了Runtime.class
,前者是一个java.lang.Runtime
对象,后者是一个java.lang.Class
对象。Class
类有实现Serializable
接口,所以可以被序列化。然后不断利用反射来获得Runtime
对象。最后编写完整
poc
:而且在
transformers
数组里面还有其他参数,像什么InvokerTransformer
里面的getRuntim
和invoke
等,但是这里面传入的只是字符串,像字符串这种基本类型是不需要继承serialize
接口的,所以那些也会被序列化。运行过后是这样的
可以看到并没有弹出计算器,原因是因为在Java 8u71之后的版本里面,Java 官方修改了
sun.reflect.annotation.AnnotationInvocationHandler
中的readObject
函数,具体可以看下面这篇文章:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d改动后,不再直接使用反序列化得到的
Map
对象,而是新建了一个LinkedHashMap
对象,并将原来的键值对添加进去。 所以后续对Map
的操作都是基于这个新的LinkedHashMap
对象,而原来我们精心构造的Map
不再执行set
或put
操作,也就不会触发RCE
了所以需要把
java
版本改小一点,我这里改成了jdk8u65
,运行一下就出来了ysoserial中的LazyMap
在
ysoserial
中,它利用的是LazyMap
而不是TransformedMap
LazyMap
和TransformedMap
类似,都来自于Common-Collections
库,并继承AbstractMapDecorator
。LazyMap
的漏洞触发点和TransformedMap
唯一的差别是,TransformedMap
是在写入元素的时候执 行transform
,而LazyMap
是在其get
方法中执行的factory.transform
。其实这也好理解,LazyMap
的作用是“懒加载”,在get
找不到值的时候,它会调用factory.transform
方法去获取一个值先看一看他的
get
方法:也很好理解,
containsKey
是看当前的键名是否存在,所以当获取的key
是map
中不存在的key
时,就会调用factory.transform
方法去获取一个值,而上面的TransformedMap
则是在写入元素的时候执行transform
方法接着看一下这个
factory
是什么,在LazyMap
的构造函数中可以看到但是
LazyMap
的关键字是protected
,也就是不能直接new
,还要找个可以new
它的方法,找到LazyMap
中的decorate
方法,这里面写了一个Map
类型的decorate
方法:同样这里
factory
会传入到LazyMap
的构造函数里面,编写一下因为是
decorate
方法是Map
类型的,所以定义的变量也需要是Map
类型的,直接调用Lazymap
里面这个方法就可以了,显然这里第一个参数并不是必须要HashMap()
,随便一个Map
类型的实现类就可以了(因为Map
是抽象类)所以
1
这个键名之前没有传入,肯定是不存在的,也就会依次执行下去了现在就是把他改成序列化数据,让他反序列化后触发了
但是现在出现了一个问题,
AnnotationInvocationHandler
类中的readObject
方法中并没有直接调用map
中的get
方法,那么就不能触发后面的一系列操作,但好在AnnotationInvocationHandler
类中的invoke
方法中有调用到get
,而ysoserial
中就是选择的这一条比较复杂的路:通过AnnotationInvocationHandler
的invoke
方法去调用get
,来看看他的具体实现:之前在介绍
AnnotationInvocationHandler
类的时候有说到memberValues
就是反序列化后得到的Map
对象,但是如何去调用到它呢?这里就要用到动态代理
的知识了如何调用invoke
首先
AnnotationInvocationHandler
类 是实现了InvocationHandler
接口的,那它里面肯定有个invode
方法,也就是可以作为一个动态代理类,那我们就可以通过Proxy
的静态方法newProxyInstance
去动态创建代理了,当调用代理对象任何方法的时候,就可以进入到invoke
方法中,这里就不赘述动态代理的东西了接着我们对
AnnotationinvocationHandler
对象进行proxy
,构造这一段的demo
:入口点是
AnnotationinvocationHandler
的readObject
方法,因为里面可以调用代理的方法,所以需要用AnnotationinvocationHandler
对proxyMap
进行包裹给出最后的poc:
先看一下整个利用链:
梳理一下:序列化后,然后反序列化,反序列化的时候会调用
AnnotationinvocationHandler
的readObject
方法,在readObject
方法里面有个地方可以调用代理类的entrySet()
方法,注意这里定义的是Map
类型,所以我们定义proxyMap
的时候也要用Map
,由于使用了动态代理,当调用Map
类(代理类的类型)中的任意方法时,会调用第三个参数的invoke
方法,接着就是判断是否存在Map
中是否存在key
这个键名,因为我们没有传参,所以肯定是不存在的,然后就依次去调用执行就结束了LazyMap与TransformedMap的对比
前面详细分析了
LazyMap
的用法并且构造了poc
,但是LazyMap
仍然无法解决CommonCollections1
在Java
高版本(8u71以后)中的使用问题LazyMap
的漏洞触发在get
和invoke
中,完全没有setValue
什么事,这也说明8u71后不能利用的原因和AnnotationInvocationHandler
#readObject
中有没有setValue
没任何关系,主要还是跟逻辑有关系,主要它会新建一个map
对象,而没有用我们构造的那个map了
只不过我还是认为
TransformedMap
用起来更加方便一点,不太明白为啥ysoserial
要使用LazyMap
唠嗑
顺便记录一下为什么我在调试的时候发现弹出了多个计算器,其实是在调试的时候,因为
proxyMap
已经代理了map
,所以任何地方调用map的方法,都会触发invoke
方法,导致执行一次,在本地调试代码的时候,因为调试器会在下面调用一些toString
之类的方法,导致不经意间触发了 命令。还有个点就是运行的时候会报错,但是还是可以运行下去,报错的原因是因为反序列化后会按照我们构造的顺序去执行链子最后执行命令,但它很多函数本来并不是这样用的,所以
readobject
里面一些可能不恰当的用法会导致爆出warning
的错误,不过也并不影响执行